1use super::Side;
23use super::view::{ChoicesView, KeyBindingsView, RowView, SlotView, SwitchView};
24use crate::ctx::{Ctx, GameData};
25use crate::frame::color::EngineColor;
26use crate::frame::draw::PanelLook;
27use crate::frame::glyph_buffer::{BoxStyle, Cell, GlyphBuffer, Rect};
28use crate::prelude::*;
29use crate::screens::{cut, print_centred};
30use crate::widgets::ask;
31use shuyi_assets::font::CellWidths;
32use shuyi_assets::lang::text_width;
33
34const PANEL: Rect = Rect::new(2, 1, 96, 29);
38const LABEL_DX: i32 = 4;
40const HEADING_DX: i32 = 2;
43const LABEL_W: i32 = 23;
46const BELOW_PANEL: i32 = 2;
48const SWITCH_Y: i32 = PANEL.y + 1;
50const COLUMNS_Y: i32 = PANEL.y + 2;
53const SLOT_W: usize = 15;
56const SLOT_PITCH: i32 = 18;
59const SLOT_MIN_W: usize = 9;
62const EMPTY_SLOT: &str = "·";
64const CHOICE_W: i32 = 12;
66const CHOICE_H: i32 = 4;
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70struct Layout {
71 panel: Rect,
73 label_x: i32,
75 slots_x: i32,
78 pitch: i32,
80 name_w: usize,
82}
83
84impl Layout {
85 fn new<A: Copy + PartialEq>(
91 view: &KeyBindingsView<A>,
92 width: u16,
93 height: u16,
94 widths: &CellWidths,
95 ) -> Self {
96 let (width, height) = (i32::from(width), i32::from(height));
97 let w = PANEL.w.min(width - 2 * PANEL.x).max(0);
98 let h = (height - PANEL.y - BELOW_PANEL).max(0);
99 let panel = Rect::new((width - w) / 2, PANEL.y, w, h);
100 let label_x = panel.x + LABEL_DX;
101 if w >= PANEL.w {
102 return Self {
103 panel,
104 label_x,
105 slots_x: label_x + LABEL_W,
106 pitch: SLOT_PITCH,
107 name_w: SLOT_W,
108 };
109 }
110 let narrow = |with_fixed| {
111 let slots_x = label_x + LABEL_W.min(widest_label(view, with_fixed, widths) + 2);
112 let slots = to_i32(view.columns.len().max(1));
113 let pitch = SLOT_PITCH.min((right(panel) - slots_x) / slots).max(0);
114 Self {
115 panel,
116 label_x,
117 slots_x,
118 pitch,
119 name_w: to_usize(pitch - SLOT_PITCH + to_i32(SLOT_W)),
120 }
121 };
122 let with_fixed = narrow(true);
123 if with_fixed.name_w >= SLOT_MIN_W {
124 with_fixed
125 } else {
126 narrow(false)
127 }
128 }
129
130 fn slot_x(&self, i: usize) -> i32 {
132 self.slots_x + to_i32(i) * self.pitch
133 }
134
135 fn heading_x(&self) -> i32 {
137 self.panel.x + HEADING_DX
138 }
139
140 fn message_row(&self) -> i32 {
142 self.panel.y + self.panel.h
143 }
144
145 fn right(&self) -> i32 {
147 right(self.panel)
148 }
149}
150
151fn right(panel: Rect) -> i32 {
153 panel.x + panel.w - 1
154}
155
156fn to_i32(n: usize) -> i32 {
158 i32::try_from(n).unwrap_or(i32::MAX)
159}
160
161fn to_usize(n: i32) -> usize {
163 usize::try_from(n).unwrap_or(0)
164}
165
166fn fixed_text<A>(row: &RowView<A>) -> String {
168 format!("+ {}", row.fixed.join("/"))
169}
170
171fn widest_label<A: Copy + PartialEq>(
174 view: &KeyBindingsView<A>,
175 with_fixed: bool,
176 widths: &CellWidths,
177) -> i32 {
178 let width = |row: &RowView<A>| {
179 let label = text_width(&row.label, widths);
180 if row.fixed.is_empty() || !with_fixed {
181 label
182 } else {
183 label + 2 + text_width(&fixed_text(row), widths)
184 }
185 };
186 to_i32(view.rows().map(width).max().unwrap_or(0))
187}
188
189#[derive(Debug, PartialEq, Eq)]
192struct Rows {
193 headings: Vec<i32>,
195 actions: Vec<i32>,
197 restore: i32,
199}
200
201fn place<A>(view: &KeyBindingsView<A>) -> Rows {
204 let mut y = COLUMNS_Y + 1;
205 let (mut headings, mut actions) = (Vec::new(), Vec::new());
206 for (i, group) in view.groups.iter().enumerate() {
207 if i > 0 {
208 y += 1;
209 }
210 headings.push(y);
211 y += 1;
212 for _ in &group.rows {
213 actions.push(y);
214 y += 1;
215 }
216 }
217 Rows {
218 headings,
219 actions,
220 restore: y + 1,
221 }
222}
223
224#[derive(Debug, PartialEq, Eq)]
227struct Scroll {
228 by: i32,
230 above: bool,
232 below: bool,
234}
235
236const FIRST_ROW: i32 = COLUMNS_Y + 1;
238
239fn scroll<A: Copy + PartialEq>(view: &KeyBindingsView<A>, placed: &Rows, last: i32) -> Scroll {
243 let end = placed.restore;
244 if end <= last {
245 return Scroll {
246 by: 0,
247 above: false,
248 below: false,
249 };
250 }
251 let focused = view
252 .rows()
253 .zip(&placed.actions)
254 .find_map(|(row, &y)| row.focused().then_some(y))
255 .or_else(|| view.restore.focused.then_some(end));
256 let by = focused.map_or(0, |y| y - last).max(0);
259 Scroll {
260 by,
261 above: by > 0,
262 below: end - by > last,
263 }
264}
265
266fn shown(
269 placed: &Rows,
270 scroll: &Scroll,
271 last: i32,
272) -> (Vec<Option<i32>>, Vec<Option<i32>>, Option<i32>) {
273 let on = |y: i32| Some(y - scroll.by).filter(|y| (FIRST_ROW..=last).contains(y));
274 (
275 placed.headings.iter().map(|&y| on(y)).collect(),
276 placed.actions.iter().map(|&y| on(y)).collect(),
277 on(placed.restore),
278 )
279}
280
281pub fn paint<G: GameData>(ctx: &Ctx<G>, view: &KeyBindingsView<G::Action>, buf: &mut GlyphBuffer) {
283 let c = |e| ctx.palette.engine(e);
284 let (black, bg) = (c(EngineColor::Background), c(EngineColor::Panel));
285 let (text, dim) = (c(EngineColor::Text), c(EngineColor::TextDim));
286 let bar = c(EngineColor::Focus);
287 let at = Layout::new(view, buf.width(), buf.height(), buf.widths());
288 buf.fill_rect(buf.bounds(), Cell::new(' ', text, black));
289 let look = PanelLook::new(BoxStyle::Single, text, bg)
290 .border(c(EngineColor::Border))
291 .title(c(EngineColor::Highlight));
292 buf.panel(at.panel, &look, Some(&view.title));
293 if let Some(switch) = &view.switch {
294 paint_switch(ctx, buf, switch, at.label_x);
295 }
296
297 for (j, heading) in view.columns.iter().enumerate() {
298 let heading = cut(heading, to_usize(at.pitch - 2), buf.widths());
300 buf.print(at.slot_x(j) + 1, COLUMNS_Y, &heading, dim, bg);
301 }
302 let placed = place(view);
303 let last = at.panel.y + at.panel.h - 2;
304 let scrolled = scroll(view, &placed, last);
305 let (headings, actions, restore) = shown(&placed, &scrolled, last);
306 let mut rows = view.rows().zip(&actions);
307 for (group, heading_y) in view.groups.iter().zip(&headings) {
308 if let Some(y) = *heading_y {
309 buf.print(at.heading_x(), y, &group.heading, bar, bg);
310 }
311 for _ in &group.rows {
312 if let Some((row, &Some(y))) = rows.next() {
313 paint_row(ctx, buf, view, row, &at, y);
314 }
315 }
316 }
317 match restore {
318 Some(y) if view.restore.focused => {
319 let label = format!(" {} ", view.restore.label);
320 buf.print(at.label_x - 1, y, &label, bg, bar);
321 }
322 Some(y) => {
323 buf.print(at.label_x, y, &view.restore.label, text, bg);
324 }
325 None => {}
326 }
327 let border = c(EngineColor::Border);
328 if scrolled.above {
329 buf.print(at.right(), FIRST_ROW, "▲", border, bg);
330 }
331 if scrolled.below {
332 buf.print(at.right(), last, "▼", border, bg);
333 }
334
335 if let Some(message) = &view.message {
336 let row = at.message_row();
337 print_centred(buf, row, message, c(EngineColor::Warning), black);
338 }
339 if let Some(question) = &view.question {
340 ask::glyph::paint(&ctx.palette, question, buf);
341 }
342 if let Some(choices) = &view.choices {
343 let focused_y = view
344 .rows()
345 .zip(&actions)
346 .find_map(|(row, &y)| y.filter(|_| row.focused()));
347 if let Some(y) = focused_y {
348 paint_choices(ctx, buf, choices, &at, y);
349 }
350 }
351 let bottom = i32::from(buf.height()) - 1;
352 print_centred(buf, bottom, &view.help, dim, black);
353}
354
355fn paint_switch<G: GameData>(ctx: &Ctx<G>, buf: &mut GlyphBuffer, switch: &SwitchView, x: i32) {
358 let c = |e| ctx.palette.engine(e);
359 let (bg, bar) = (c(EngineColor::Panel), c(EngineColor::Focus));
360 let mut x = x;
361 for (side, name) in [
362 (Side::Keyboard, switch.keyboard.as_str()),
363 (Side::Controller, switch.controller.as_str()),
364 ] {
365 if side != switch.shown {
366 buf.print(x, SWITCH_Y, name, c(EngineColor::TextDim), bg);
367 } else if switch.focused {
368 buf.print(x - 1, SWITCH_Y, &format!(" {name} "), bg, bar);
369 } else {
370 buf.print(x, SWITCH_Y, name, c(EngineColor::Highlight), bg);
371 }
372 x += buf.text_cells(name) + 4;
373 }
374}
375
376fn paint_row<G: GameData>(
381 ctx: &Ctx<G>,
382 buf: &mut GlyphBuffer,
383 view: &KeyBindingsView<G::Action>,
384 row: &RowView<G::Action>,
385 at: &Layout,
386 y: i32,
387) {
388 let c = |e| ctx.palette.engine(e);
389 let (bg, text, dim) = (
390 c(EngineColor::Panel),
391 c(EngineColor::Text),
392 c(EngineColor::TextDim),
393 );
394 let label_fg = if row.focused() {
395 c(EngineColor::Highlight)
396 } else if row.lost_key {
397 c(EngineColor::Bright)
398 } else {
399 text
400 };
401 let room = to_usize(at.slots_x - at.label_x - 1);
403 let label_w = buf.print(
404 at.label_x,
405 y,
406 &cut(&row.label, room, buf.widths()),
407 label_fg,
408 bg,
409 );
410 if !row.fixed.is_empty() {
411 let used = usize::from(label_w) + 2;
412 let fixed = cut(&fixed_text(row), room.saturating_sub(used), buf.widths());
413 buf.print(at.label_x + i32::from(label_w) + 2, y, &fixed, dim, bg);
414 }
415 let note_x = row.unmapped.then(|| note_x(view, row, at, buf.widths()));
416 for (j, slot) in row.slots.iter().enumerate() {
417 let x = at.slot_x(j);
418 if row.focus == Some(j) {
419 let shown = match slot {
420 SlotView::Capturing(prompt) => prompt.as_str(),
421 SlotView::Bound(name) => name.as_str(),
422 SlotView::Empty => "",
423 };
424 let w = at.name_w;
425 let shown = cut(shown, w, buf.widths());
426 let pad = w.saturating_sub(text_width(&shown, buf.widths()));
427 let bar = format!(" {shown}{}", " ".repeat(pad));
428 buf.print(x, y, &bar, bg, c(EngineColor::Focus));
429 } else if let SlotView::Bound(name) = slot {
430 buf.print(x + 1, y, &cut(name, at.name_w, buf.widths()), text, bg);
431 } else if note_x.is_none_or(|n| n == after_slots(row, at)) {
432 buf.print(x + 1, y, EMPTY_SLOT, dim, bg);
433 }
434 }
435 if let Some(x) = note_x {
436 let fg = if row.blocks_leaving {
437 c(EngineColor::Warning)
438 } else {
439 dim
440 };
441 let note = cut(&view.not_mapped, to_usize(at.right() - x), buf.widths());
442 buf.print(x, y, ¬e, fg, bg);
443 }
444}
445
446fn note_x<A>(view: &KeyBindingsView<A>, row: &RowView<A>, at: &Layout, widths: &CellWidths) -> i32 {
451 let len = to_i32(text_width(&view.not_mapped, widths));
452 let after = after_slots(row, at);
453 if after + len <= at.right() {
454 return after;
455 }
456 let first = at.slots_x + 1;
457 match row.focus {
458 Some(j) if at.slot_x(j) < first + len => at.slot_x(j) + at.pitch - 1,
459 _ => first,
460 }
461}
462
463fn after_slots<A>(row: &RowView<A>, at: &Layout) -> i32 {
465 at.slot_x(row.slots.len()) + 1
466}
467
468fn paint_choices<G: GameData>(
472 ctx: &Ctx<G>,
473 buf: &mut GlyphBuffer,
474 choices: &ChoicesView,
475 at: &Layout,
476 slot_y: i32,
477) {
478 let c = |e| ctx.palette.engine(e);
479 let (bg, bar) = (c(EngineColor::Panel), c(EngineColor::Focus));
480 let below = slot_y + 1;
481 let y = if below + CHOICE_H < at.panel.y + at.panel.h {
482 below
483 } else {
484 slot_y - CHOICE_H
485 };
486 let x = at.slot_x(choices.slot).min(at.right() - CHOICE_W);
487 let rect = Rect::new(x, y, CHOICE_W, CHOICE_H);
488 let look = PanelLook::new(BoxStyle::Double, c(EngineColor::Text), bg).border(bar);
489 buf.panel(rect, &look, None);
490 let inner = usize::try_from(CHOICE_W - 3).unwrap_or(0);
491 for (i, choice) in (0..).zip(&choices.lines) {
492 let (x, y) = (rect.x + 1, rect.y + 1 + i);
493 if usize::try_from(i).is_ok_and(|i| i == choices.focused) {
494 buf.print(x, y, &format!(" {choice:<inner$}"), bg, bar);
495 } else {
496 buf.print(x + 1, y, choice, c(EngineColor::Text), bg);
497 }
498 }
499}
500
501#[cfg(test)]
502mod tests;