Skip to main content

shuyi_ui/screens/
key_bindings.rs

1//! The Key bindings screen: every action of the game's rows
2//! ([`KeyRow`]) with its [`SLOTS`] key slots for the layout in use, or, on
3//! its controller [`Side`], its [`SLOTS`] button slots. A row at the top switches between
4//! the two. Confirm on a slot captures the next key or button pressed into
5//! it; one taken from another slot moves; leaving is blocked while a
6//! required action has no key or no button; Restore defaults asks first.
7//!
8//! The screen edits a copy of the layout's [`LayoutBindings`] and of the
9//! [`PadBindings`] and hands them to [`Ctx`] when it closes, so it is steered
10//! with the keys and buttons the player had when they opened it: moving
11//! every cursor key elsewhere can't trap them here.
12//!
13//! No controller button is fixed, so the screen works with only a
14//! controller: a button goes in the slot when it is let go, holding any
15//! button backs out of a capture, and when the controller was used last
16//! Confirm on a slot offers [`CHOICES`] (change it or clear it). A second
17//! button pressed while the first is held makes the slot a button chord
18//! of the two (ticket 1045, game ADR-0079). A button of the capture still held
19//! when it ends does nothing when let go: on a pad whose buttons act when
20//! let go (the Playdate's B), it would otherwise back out of the screen.
21//!
22//! On a platform with no keyboard ([`Keyboard::Absent`], the Playdate) the
23//! screen shows the controller side alone: no switch, and leaving asks
24//! only that every required action has a button.
25//!
26//! Capture is the one place that reads raw key presses
27//! ([`FrameInput::pressed_chords`]) and raw button presses
28//! ([`FrameInput::pressed_buttons`]); which keys back out or clear a slot is
29//! asked of `input` ([`is_capture_abort`], [`is_clear_slot`]), and a
30//! button's name of [`PadKind::button_name`], never named here.
31//!
32//! **The look is a skin.** This module decides what the screen
33//! shows and does, and says it as plain data: a [`KeyBindingsView`]
34//! ([`KeyBindingsScreen::view`]). [`glyph::paint`] draws that view as
35//! glyphs, unless the game gives another painter
36//! ([`KeyBindingsScreen::painted_by`]). Tests of what happened read the
37//! view; only the painters' tests read cells.
38
39pub mod glyph;
40pub mod view;
41
42use crate::prelude::*;
43pub use view::{
44    ChoicesView, GroupView, KeyBindingsView, RestoreView, RowView, SlotView, SwitchView,
45};
46
47use core::fmt;
48
49use shuyi_assets::keymap::{ActionSet, Button, ButtonChord, Chord, Layout, SLOTS, UiAction};
50
51use super::layout_picker;
52use crate::audio::MenuSound;
53use crate::ctx::{CHORD_JOIN_TEXT, Ctx, GameData, Keyboard};
54use crate::frame::glyph_buffer::GlyphBuffer;
55use crate::input::bindings::{LayoutBindings, PadBindings};
56use crate::input::fixed::{
57    capture_abort_key_name, clear_slot_key_name, is_capture_abort, is_clear_slot,
58};
59use crate::input::help_keys::{HelpKeys, SEPARATOR};
60use crate::input::keymap::Keymap;
61use crate::input::pad::PadKind;
62use crate::input::state::Device;
63use crate::screen::{FrameInput, Screen, Transition};
64use crate::widgets::{Ask, AskView};
65
66/// What a row of the Key bindings screen sits under.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum RowGroup {
69    /// The actions that must keep a key (or a button), under the screen's
70    /// own heading for them.
71    Required,
72    /// The other actions of the main key group.
73    Optional,
74    /// A key group of its own, whose keys may overlap the rest's
75    /// (game ADR-0068), under the heading with this text key.
76    Shared(&'static str),
77}
78
79/// One row of the Key bindings screen, as the game gives it: the action,
80/// the text key of its player-facing label (`Unit info`) and what it sits
81/// under. Rows of one group come together, in the order the screen lists
82/// them.
83pub type KeyRow<A> = (A, &'static str, RowGroup);
84
85/// Which bindings the screen shows and edits.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum Side {
88    /// The keys of the layout in use.
89    Keyboard,
90    /// The controller's buttons (one setup for both layouts).
91    Controller,
92}
93
94impl Side {
95    /// The text key of what a slot on this side shows while it waits.
96    fn prompt(self) -> &'static str {
97        match self {
98            Self::Keyboard => CAPTURE_PROMPT,
99            Self::Controller => CAPTURE_BUTTON_PROMPT,
100        }
101    }
102}
103
104/// The text key of what a slot shows while it waits for a key.
105pub const CAPTURE_PROMPT: &str = "key_bindings.press_a_key";
106
107/// The text key of what a slot shows while it waits for a controller
108/// button.
109pub const CAPTURE_BUTTON_PROMPT: &str = "key_bindings.press_a_button";
110
111/// Text key of the screen's title.
112pub const TITLE: &str = "key_bindings.title";
113/// Text key of the switch row's name for the keyboard side, which the
114/// layout's name follows.
115pub const KEYBOARD_SIDE: &str = "key_bindings.keyboard";
116/// Text key of the switch row's name for the controller side.
117pub const CONTROLLER_SIDE: &str = "key_bindings.controller";
118/// Text key of the heading over the required actions, on the keyboard
119/// side.
120pub const REQUIRED_HEADING: &str = "key_bindings.must_have_key";
121/// Text key of the heading over the required actions, on the controller
122/// side.
123pub const REQUIRED_BUTTON_HEADING: &str = "key_bindings.must_have_button";
124/// Text key of the heading over the optional actions.
125pub const OPTIONAL_HEADING: &str = "key_bindings.optional";
126/// Text key of the row under the actions that puts the shown side's
127/// defaults back.
128pub const RESTORE_DEFAULTS: &str = "key_bindings.restore_defaults";
129/// Text key of the help while a slot waits for a key.
130pub const CAPTURE_HELP: &str = "key_bindings.capture_help";
131/// Text key of the help for backing out of a capture with only a
132/// controller.
133pub const HOLD_TO_CANCEL: &str = "key_bindings.hold_to_cancel";
134/// Text keys of what Confirm on a slot offers when the controller was used
135/// last: put another key or button in it, or empty it.
136pub const CHOICES: [&str; 2] = ["key_bindings.change", "key_bindings.clear"];
137/// Text key of what shows when the key pressed for a slot is reserved.
138pub const RESERVED_MESSAGE: &str = "key_bindings.reserved";
139/// How long the row that just lost its key to another slot stays
140/// highlighted, in seconds (*tunable*).
141pub const MOVED_FLASH_SECS: f32 = 1.5;
142/// How long a controller button is held to back out of a capture, in
143/// seconds (*tunable*). Let go sooner, it goes in the slot.
144pub const HOLD_TO_CANCEL_SECS: f32 = 1.0;
145
146/// What paints the screen's view: [`glyph::paint`], or the game's own
147/// ([`KeyBindingsScreen::painted_by`]).
148pub type Paint<G> = fn(&Ctx<G>, &KeyBindingsView<<G as GameData>::Action>, &mut GlyphBuffer);
149
150/// The controller buttons being pressed for a slot: the first to go down,
151/// for how many seconds it has been held, and the second, if one went down
152/// while it was held (a chord).
153#[derive(Debug, Clone, Copy, PartialEq)]
154struct Held {
155    first: Button,
156    secs: f32,
157    second: Option<Button>,
158}
159
160impl Held {
161    /// What the buttons make: the first alone, or the chord of the two.
162    fn chord(self) -> Option<ButtonChord> {
163        match self.second {
164            Some(second) => ButtonChord::pair(self.first, second),
165            None => Some(ButtonChord::single(self.first)),
166        }
167    }
168
169    /// Whether `button` is one of them.
170    fn has(self, button: Button) -> bool {
171        self.first == button || self.second == Some(button)
172    }
173}
174
175/// Whether `action` is one the game takes for a move (up, down, left,
176/// right): what moves the focus.
177fn is_cursor<A: ActionSet>(action: A) -> bool {
178    matches!(
179        action.ui(),
180        Some(UiAction::Up | UiAction::Down | UiAction::Left | UiAction::Right)
181    )
182}
183
184/// The player-facing name of `action` (its row's label) in `ctx`'s
185/// language; its keymap name for an action not on the screen (Debug).
186pub fn label<'a, G: GameData>(
187    ctx: &'a Ctx<G>,
188    rows: &[KeyRow<G::Action>],
189    action: G::Action,
190) -> &'a str {
191    rows.iter()
192        .find(|&&(a, ..)| a == action)
193        .map_or(action.name(), |&(_, key, _)| ctx.text(key))
194}
195
196/// The blocked-leave message for `action` on `side` (`Give Cancel a key
197/// first`, `Give Cancel a button first`).
198pub fn blocked_message<G: GameData>(
199    ctx: &Ctx<G>,
200    rows: &[KeyRow<G::Action>],
201    side: Side,
202    action: G::Action,
203) -> String {
204    let key = match side {
205        Side::Keyboard => "key_bindings.blocked_key",
206        Side::Controller => "key_bindings.blocked_button",
207    };
208    ctx.text_with(key, &[("action", &label(ctx, rows, action))])
209}
210
211/// The Key bindings screen for one layout and the controller. Cancel saves
212/// and closes it, unless a required action has no key or no button.
213pub struct KeyBindingsScreen<G: GameData> {
214    /// The game's rows, in the order the screen lists them.
215    rows: &'static [KeyRow<G::Action>],
216    /// What paints the view.
217    paint: Paint<G>,
218    /// Whether the platform has a keyboard: without one only the
219    /// controller side is shown.
220    keyboard: Keyboard,
221    /// The layout being edited.
222    layout: Layout,
223    /// The keymap the screen was opened with: what steers it (`Game` keeps
224    /// using it until the edits are saved on leaving) and what its help
225    /// text names.
226    opened_with: Keymap<G::Action>,
227    /// Which bindings are shown.
228    side: Side,
229    /// The edited copy of the layout's keys, saved on leaving.
230    bindings: LayoutBindings<G::Action>,
231    /// The edited copy of the controller's buttons, saved on leaving.
232    pad: PadBindings<G::Action>,
233    /// Focused row: an index into `rows`, then Restore defaults
234    /// ([`restore_row`](Self::restore_row)) and the switch
235    /// ([`switch_row`](Self::switch_row)).
236    row: usize,
237    /// Focused slot (kept while on a row that has none).
238    slot: usize,
239    /// Whether the focused slot is waiting for a key or button.
240    capturing: bool,
241    /// The controller buttons held since they went down during a capture.
242    held: Option<Held>,
243    /// Buttons of a capture that ended while they were held: the screen
244    /// does nothing until they are let go.
245    releasing: Vec<Button>,
246    /// The focused line of [`CHOICES`], while they are open.
247    choice: Option<usize>,
248    /// The message under the panel, if any.
249    message: Option<String>,
250    /// The action that just lost a key to another slot, and the seconds its
251    /// row stays highlighted.
252    moved: Option<(G::Action, f32)>,
253    /// The "restore the defaults?" question, while it is open.
254    restoring: Option<Ask<bool>>,
255    /// Set when a capture ends with a cursor key or button held: cursor
256    /// moves are ignored until none is, because the one just pressed for
257    /// the slot (or held to back out) may be one under the opened-with
258    /// keymap, still down and repeating.
259    await_release: bool,
260}
261
262impl<G: GameData> fmt::Debug for KeyBindingsScreen<G> {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        // check-text: not player text, the type name for Debug
265        f.debug_struct("KeyBindingsScreen")
266            .field("layout", &self.layout)
267            .field("side", &self.side)
268            .field("row", &self.row)
269            .field("slot", &self.slot)
270            .field("capturing", &self.capturing)
271            .field("choice", &self.choice)
272            .field("restoring", &self.restoring.as_ref().map(Ask::key))
273            .finish_non_exhaustive()
274    }
275}
276
277impl<G: GameData> Clone for KeyBindingsScreen<G> {
278    fn clone(&self) -> Self {
279        Self {
280            rows: self.rows,
281            paint: self.paint,
282            keyboard: self.keyboard,
283            layout: self.layout,
284            opened_with: self.opened_with.clone(),
285            side: self.side,
286            bindings: self.bindings.clone(),
287            pad: self.pad.clone(),
288            row: self.row,
289            slot: self.slot,
290            capturing: self.capturing,
291            held: self.held,
292            releasing: self.releasing.clone(),
293            choice: self.choice,
294            message: self.message.clone(),
295            moved: self.moved,
296            restoring: self.restoring.clone(),
297            await_release: self.await_release,
298        }
299    }
300}
301
302impl<G: GameData> KeyBindingsScreen<G> {
303    /// Name reported by [`Screen::name`].
304    pub const NAME: &'static str = "key_bindings";
305
306    /// The screen listing `rows`, for the layout in use (the keymap file's
307    /// first if none is chosen yet), focused on the first action's first
308    /// slot, showing the side the player pressed last: the controller's
309    /// buttons after a button press, else the keys. With no keyboard
310    /// ([`Keyboard::Absent`]) it shows the controller's alone.
311    pub fn new(ctx: &Ctx<G>, rows: &'static [KeyRow<G::Action>]) -> Self {
312        // The loader refuses a keymap without layouts.
313        let layout = ctx.shown_layout().unwrap_or(Layout::named("none"));
314        let side = match ctx.device {
315            _ if ctx.platform.keyboard == Keyboard::Absent => Side::Controller,
316            Device::Keyboard => Side::Keyboard,
317            Device::Pad(_) => Side::Controller,
318        };
319        Self {
320            rows,
321            paint: glyph::paint,
322            keyboard: ctx.platform.keyboard,
323            layout,
324            opened_with: ctx.keymap.clone(),
325            side,
326            bindings: ctx.layout_bindings(layout),
327            pad: ctx.pad_bindings(),
328            row: 0,
329            slot: 0,
330            capturing: false,
331            held: None,
332            releasing: Vec::new(),
333            choice: None,
334            message: None,
335            moved: None,
336            restoring: None,
337            await_release: false,
338        }
339    }
340
341    /// The same screen with its view painted by `paint` instead of
342    /// [`glyph::paint`]: a game's look for a device of its own.
343    #[must_use]
344    pub fn painted_by(mut self, paint: Paint<G>) -> Self {
345        self.paint = paint;
346        self
347    }
348
349    /// Whether only the controller side is shown: the platform has no
350    /// keyboard, so there is no switch.
351    pub fn is_controller_only(&self) -> bool {
352        self.keyboard == Keyboard::Absent
353    }
354
355    /// The same screen editing `bindings` instead of the saved ones.
356    #[must_use]
357    pub fn with_bindings(mut self, bindings: LayoutBindings<G::Action>) -> Self {
358        self.bindings = bindings;
359        self
360    }
361
362    /// The edited keys (saved when the screen closes).
363    pub fn bindings(&self) -> &LayoutBindings<G::Action> {
364        &self.bindings
365    }
366
367    /// The edited controller buttons (saved when the screen closes).
368    pub fn pad_bindings(&self) -> &PadBindings<G::Action> {
369        &self.pad
370    }
371
372    /// Which bindings are shown.
373    pub fn side(&self) -> Side {
374        self.side
375    }
376
377    /// The focused action and slot; `None` on Restore defaults and on the
378    /// keyboard / controller switch.
379    pub fn focus(&self) -> Option<(G::Action, usize)> {
380        self.rows
381            .get(self.row)
382            .map(|&(action, ..)| (action, self.slot))
383    }
384
385    /// Whether the focus is on the keyboard / controller switch.
386    pub fn is_on_switch(&self) -> bool {
387        !self.is_controller_only() && self.row == self.switch_row()
388    }
389
390    /// [`row`](Self::row) on Restore defaults, the row after the actions.
391    fn restore_row(&self) -> usize {
392        self.rows.len()
393    }
394
395    /// [`row`](Self::row) on the keyboard / controller switch: above the
396    /// first action, so one up from it and one down from Restore defaults.
397    fn switch_row(&self) -> usize {
398        self.rows.len() + 1
399    }
400
401    /// Whether the focused slot is waiting for a key or button.
402    pub fn is_capturing(&self) -> bool {
403        self.capturing
404    }
405
406    /// The focused line of [`CHOICES`], while they are open.
407    pub fn choice(&self) -> Option<usize> {
408        self.choice
409    }
410
411    /// Whether the screen is asking before it restores the defaults.
412    pub fn is_asking_restore(&self) -> bool {
413        self.restoring.is_some()
414    }
415
416    /// The question asked before restoring the shown side's defaults.
417    pub fn restore_question(&self, ctx: &Ctx<G>) -> String {
418        Self::restore_view(ctx, &self.restore_ask(), self.layout, ctx.help_keys()).text
419    }
420
421    /// The question asked before restoring the shown side's defaults, as
422    /// it opens: yes on Confirm, no on Cancel.
423    fn restore_ask(&self) -> Ask<bool> {
424        let key = match self.side {
425            Side::Keyboard => "key_bindings.restore_keys",
426            Side::Controller => "key_bindings.restore_buttons",
427        };
428        Ask::yes_no(key, "key_bindings.restore_answers")
429    }
430
431    /// `ask`'s view, the keyboard's question naming `layout` and its
432    /// answers' keys those of `keys`.
433    fn restore_view(
434        ctx: &Ctx<G>,
435        ask: &Ask<bool>,
436        layout: Layout,
437        keys: HelpKeys<'_, G::Action>,
438    ) -> AskView<bool> {
439        let layout = layout_picker::label(ctx, layout);
440        ask.view_with_keys(ctx, keys, &[("layout", &layout)])
441    }
442
443    /// The message under the panel, if any.
444    pub fn message(&self) -> Option<&str> {
445        self.message.as_deref()
446    }
447
448    /// The names in `action`'s slots on the shown side; buttons as the
449    /// controller used last names them (an Xbox pad's names until one is
450    /// used).
451    fn slot_names(&self, ctx: &Ctx<G>, action: G::Action) -> [Option<String>; SLOTS] {
452        match self.side {
453            Side::Keyboard => self
454                .bindings
455                .slots(action)
456                .map(|c| c.map(|c| c.to_string())),
457            Side::Controller => {
458                let kind = match ctx.device {
459                    Device::Pad(kind) => kind,
460                    Device::Keyboard => PadKind::default(),
461                };
462                let join = ctx.text(CHORD_JOIN_TEXT);
463                let name = |b| kind.chord_name(b, join);
464                self.pad.slots(action).map(|b| b.map(name))
465            }
466        }
467    }
468
469    /// Whether `action` has nothing in any slot on the shown side.
470    fn is_unmapped(&self, action: G::Action) -> bool {
471        match self.side {
472            Side::Keyboard => self.bindings.is_unmapped(action),
473            Side::Controller => self.pad.is_unmapped(action),
474        }
475    }
476
477    /// The first row of the screen whose required action has nothing on
478    /// `side`.
479    fn first_unmapped(&self, side: Side) -> Option<G::Action> {
480        let unmapped = match side {
481            Side::Keyboard => self.bindings.unmapped_required(),
482            Side::Controller => self.pad.unmapped_required(),
483        };
484        let mut rows = self.rows.iter().map(|&(action, ..)| action);
485        rows.find(|action| unmapped.contains(action))
486    }
487
488    /// Counts the moved-row highlight down by `dt` seconds (a frame's:
489    /// finite and at least 0).
490    fn tick(&mut self, dt: f32) {
491        if let Some((_, left)) = &mut self.moved {
492            *left -= dt;
493            if *left <= 0.0 {
494                self.moved = None;
495            }
496        }
497    }
498
499    /// Moves the focus one row up or down, wrapping: the actions, Restore
500    /// defaults, the switch (unless there is none), and the first action
501    /// again.
502    fn move_row(&mut self, ctx: &mut Ctx<G>, down: bool) {
503        let rows = self.rows.len() + if self.is_controller_only() { 1 } else { 2 };
504        self.row = if down {
505            (self.row + 1) % rows
506        } else {
507            (self.row + rows - 1) % rows
508        };
509        self.message = None;
510        ctx.audio.menu(MenuSound::Move);
511    }
512
513    /// Left or right: on the switch, shows the keyboard (left) or the
514    /// controller (right); on a slot, moves the focus one slot, stopping at
515    /// the ends. Does nothing on Restore defaults.
516    fn move_across(&mut self, ctx: &mut Ctx<G>, right: bool) {
517        if self.is_on_switch() {
518            let side = if right {
519                Side::Controller
520            } else {
521                Side::Keyboard
522            };
523            self.show(ctx, side);
524            return;
525        }
526        if self.focus().is_none() {
527            return;
528        }
529        let to = if right {
530            (self.slot + 1).min(SLOTS - 1)
531        } else {
532            self.slot.saturating_sub(1)
533        };
534        if to != self.slot {
535            self.slot = to;
536            self.message = None;
537            ctx.audio.menu(MenuSound::Move);
538        }
539    }
540
541    /// Shows `side`, if it isn't shown already.
542    fn show(&mut self, ctx: &mut Ctx<G>, side: Side) {
543        if side != self.side {
544            self.side = side;
545            self.message = None;
546            self.moved = None;
547            ctx.audio.menu(MenuSound::Move);
548        }
549    }
550
551    /// Confirm: on a slot, starts capturing (after the controller was used,
552    /// opens the [`CHOICES`] instead); on Restore defaults, asks whether to
553    /// restore; on the switch, shows the other side.
554    fn confirm(&mut self, ctx: &mut Ctx<G>) {
555        self.message = None;
556        if self.is_on_switch() {
557            let other = match self.side {
558                Side::Keyboard => Side::Controller,
559                Side::Controller => Side::Keyboard,
560            };
561            self.show(ctx, other);
562            return;
563        }
564        if self.focus().is_none() {
565            self.restoring = Some(self.restore_ask());
566        } else if matches!(ctx.device, Device::Pad(_)) {
567            self.choice = Some(0);
568        } else {
569            self.capturing = true;
570        }
571        ctx.audio.menu(MenuSound::Select);
572    }
573
574    /// An action while the [`CHOICES`] are open: up and down move between
575    /// them, Confirm takes the focused one (capture, or empty the slot) and
576    /// Cancel closes them. Returns whether they closed.
577    fn answer_choice(&mut self, ctx: &mut Ctx<G>, action: G::Action) -> bool {
578        let Some(line) = self.choice else {
579            return true;
580        };
581        match action.ui() {
582            Some(UiAction::Up | UiAction::Down) => {
583                self.choice = Some((line + 1) % CHOICES.len());
584                ctx.audio.menu(MenuSound::Move);
585                return false;
586            }
587            Some(UiAction::Confirm) if line == 0 => {
588                self.capturing = true;
589                ctx.audio.menu(MenuSound::Select);
590            }
591            Some(UiAction::Confirm) => {
592                if !self.clear_slot(ctx) {
593                    ctx.audio.menu(MenuSound::Select);
594                }
595            }
596            Some(UiAction::Cancel) => ctx.audio.menu(MenuSound::Cancel),
597            _ => return false,
598        }
599        self.choice = None;
600        true
601    }
602
603    /// An action while the restore question is open: Confirm restores the
604    /// shown side's defaults, Cancel backs out; either closes the question
605    /// (and returns `true`). Nothing else does anything.
606    fn answer_restore(&mut self, ctx: &mut Ctx<G>, action: G::Action) -> bool {
607        let Some(ask) = &mut self.restoring else {
608            return true;
609        };
610        match ask.handle(action, &mut ctx.audio) {
611            Some(true) => {
612                let def = ctx.game.keymap_def();
613                match self.side {
614                    Side::Keyboard => self.bindings = LayoutBindings::defaults(def, self.layout),
615                    Side::Controller => self.pad = PadBindings::defaults(def),
616                }
617                self.moved = None;
618                ctx.audio.menu(MenuSound::Select);
619            }
620            Some(false) => ctx.audio.menu(MenuSound::Cancel),
621            None => return false,
622        }
623        self.restoring = None;
624        true
625    }
626
627    /// Empties the focused slot on the shown side. Returns whether it held
628    /// anything.
629    fn clear_slot(&mut self, ctx: &mut Ctx<G>) -> bool {
630        let Some((action, slot)) = self.focus() else {
631            return false;
632        };
633        if self.slot_names(ctx, action)[slot].is_none() {
634            return false;
635        }
636        match self.side {
637            Side::Keyboard => self.bindings.clear(action, slot),
638            Side::Controller => self.pad.clear(action, slot),
639        }
640        self.message = None;
641        ctx.audio.menu(MenuSound::Select);
642        true
643    }
644
645    /// Ends the capture with nothing changed.
646    fn abort_capture(&mut self, ctx: &mut Ctx<G>) {
647        self.end_capture();
648        ctx.audio.menu(MenuSound::Cancel);
649    }
650
651    /// Ends the capture, clearing what belongs to it.
652    fn end_capture(&mut self) {
653        self.capturing = false;
654        self.held = None;
655        self.message = None;
656    }
657
658    /// The capture took something that was in slot `from`, if any: ends it
659    /// and lights up the action that lost it.
660    fn captured(&mut self, ctx: &mut Ctx<G>, from: Option<(G::Action, usize)>) {
661        self.end_capture();
662        self.moved = from.map(|(lost, _)| (lost, MOVED_FLASH_SECS));
663        ctx.audio.menu(MenuSound::Select);
664    }
665
666    /// A key pressed while capturing. Returns whether capturing ended.
667    fn capture(&mut self, ctx: &mut Ctx<G>, chord: Chord) -> bool {
668        if is_clear_slot(chord) {
669            return false;
670        }
671        if is_capture_abort(chord) {
672            self.abort_capture(ctx);
673            return true;
674        }
675        let Some((action, slot)) = self.focus() else {
676            self.end_capture();
677            return true;
678        };
679        if self.side == Side::Controller {
680            // A slot waiting for a button takes no key.
681            return false;
682        }
683        let Ok(from) = self.bindings.bind(action, slot, chord) else {
684            self.message = Some(ctx.text(RESERVED_MESSAGE).to_owned());
685            ctx.audio.menu(MenuSound::Denied);
686            return false;
687        };
688        self.captured(ctx, from);
689        true
690    }
691
692    /// This frame's controller buttons while capturing: the first button
693    /// to go down is watched, and a second that goes down while it is held.
694    /// When either is let go, the first alone, or the chord of the two,
695    /// goes in a slot that waits for a button. The first held alone for
696    /// [`HOLD_TO_CANCEL_SECS`] backs out of the capture with nothing
697    /// changed.
698    fn capture_buttons(&mut self, ctx: &mut Ctx<G>, input: &FrameInput<G::Action>) {
699        let fresh = self.held.is_none();
700        let mut pressed = input.pressed_buttons().iter().copied();
701        if fresh {
702            self.held = pressed.next().map(|first| Held {
703                first,
704                secs: 0.0,
705                second: None,
706            });
707        }
708        let Some(mut held) = self.held else {
709            return;
710        };
711        if held.second.is_none() {
712            held.second = pressed.find(|&b| b != held.first);
713        }
714        let released = input.released_buttons();
715        if released.iter().any(|&b| held.has(b)) {
716            self.held = None;
717            let still_down = [Some(held.first), held.second].into_iter().flatten();
718            self.releasing = still_down.filter(|b| !released.contains(b)).collect();
719            if let Some(chord) = held.chord() {
720                self.tapped(ctx, chord);
721            }
722            return;
723        }
724        // The frame it went down in doesn't count.
725        if !fresh {
726            held.secs += input.dt();
727        }
728        self.held = Some(held);
729        if held.second.is_none() && held.secs >= HOLD_TO_CANCEL_SECS {
730            self.abort_capture(ctx);
731            self.releasing = vec![held.first];
732        }
733    }
734
735    /// `chord` (a button, or two) was pressed and let go while capturing:
736    /// on the controller side it goes in the focused slot.
737    fn tapped(&mut self, ctx: &mut Ctx<G>, chord: ButtonChord) {
738        let (Side::Controller, Some((action, slot))) = (self.side, self.focus()) else {
739            return;
740        };
741        // The focused action is rebindable and its slot exists.
742        if let Ok(from) = self.pad.bind(action, slot, chord) {
743            self.captured(ctx, from);
744        }
745    }
746
747    /// Cancel: saves and returns `true` if every required action has a key
748    /// and a button (only a button, with no keyboard); otherwise shows the
749    /// side that lacks one and names the first action on the screen that
750    /// does (the shown side first).
751    fn leave(&mut self, ctx: &mut Ctx<G>) -> bool {
752        let other = match self.side {
753            Side::Keyboard => Side::Controller,
754            Side::Controller => Side::Keyboard,
755        };
756        let sides = if self.is_controller_only() {
757            &[Side::Controller][..]
758        } else {
759            &[self.side, other][..]
760        };
761        let blocked = sides
762            .iter()
763            .find_map(|&side| Some((side, self.first_unmapped(side)?)));
764        if let Some((side, action)) = blocked {
765            self.side = side;
766            self.message = Some(blocked_message(ctx, self.rows, side, action));
767            ctx.audio.menu(MenuSound::Denied);
768            return false;
769        }
770        // If saving fails the keys and buttons still apply for this session.
771        if self.bindings != ctx.layout_bindings(self.layout) {
772            let _ = ctx.set_layout_bindings(self.layout, self.bindings.clone());
773        }
774        if self.pad != ctx.pad_bindings() {
775            let _ = ctx.set_pad_bindings(self.pad.clone());
776        }
777        ctx.audio.menu(MenuSound::Cancel);
778        true
779    }
780
781    /// The bottom help line, naming the keys the screen was opened with
782    /// (or their buttons, on a controller): the same as `ctx`'s until the
783    /// screen closes.
784    pub fn help(&self, ctx: &Ctx<G>) -> String {
785        let km = ctx.help_keys().of(&self.opened_with);
786        let on_pad = matches!(ctx.device, Device::Pad(_));
787        if self.capturing {
788            let what = ctx.text(match self.side {
789                Side::Keyboard => CAPTURE_HELP,
790                Side::Controller => CAPTURE_BUTTON_PROMPT,
791            });
792            let hold = on_pad || self.side == Side::Controller;
793            let hold = hold.then(|| ctx.text(HOLD_TO_CANCEL));
794            let back = (!on_pad).then(|| {
795                let key = capture_abort_key_name();
796                ctx.text_with("key_bindings.help_capture_back", &[("key", &key)])
797            });
798            let hints = [Some(what), hold, back.as_deref()];
799            return hints
800                .into_iter()
801                .flatten()
802                .collect::<Vec<_>>()
803                .join(SEPARATOR);
804        }
805        if self.restoring.is_some() {
806            return ctx.text_with_keys("key_bindings.help_restore", km, &[]);
807        }
808        if self.choice.is_some() {
809            return ctx.text_with("key_bindings.help_choice", &[]);
810        }
811        if self.is_on_switch() {
812            return ctx.text_with("key_bindings.help_switch", &[]);
813        }
814        let on_slot = self.focus().is_some();
815        if on_slot && on_pad {
816            return ctx.text_with("key_bindings.help_slot_pad", &[]);
817        }
818        if on_slot {
819            let clear = clear_slot_key_name();
820            return ctx.text_with_keys("key_bindings.help_slot", km, &[("clear", &clear)]);
821        }
822        ctx.text_with_keys("key_bindings.help_restore_row", km, &[])
823    }
824
825    /// The screen as it is now, as plain data for a skin to paint
826    /// ([`glyph::paint`]): the switch, the slot columns, every action with
827    /// what its slots hold, the restore row, the message, the question or
828    /// the choices open, and the help line, all in the player's language
829    /// and naming their keys.
830    pub fn view(&self, ctx: &Ctx<G>) -> KeyBindingsView<G::Action> {
831        let columns = (0..SLOTS).map(|j| match self.side {
832            Side::Keyboard => ctx.text_with("key_bindings.key_column", &[("n", &(j + 1))]),
833            Side::Controller => ctx.text_with("key_bindings.button_column", &[("n", &(j + 1))]),
834        });
835        let mut groups: Vec<GroupView<G::Action>> = Vec::new();
836        let mut last = None;
837        for (i, &(_, _, group)) in self.rows.iter().enumerate() {
838            if last != Some(group) {
839                last = Some(group);
840                groups.push(GroupView {
841                    heading: self.group_heading(ctx, group),
842                    required: group == RowGroup::Required,
843                    rows: Vec::new(),
844                });
845            }
846            if let Some(group) = groups.last_mut() {
847                group.rows.push(self.row_view(ctx, i));
848            }
849        }
850        let km = ctx.help_keys().of(&self.opened_with);
851        KeyBindingsView {
852            title: ctx.text(TITLE).to_owned(),
853            switch: (!self.is_controller_only()).then(|| SwitchView {
854                keyboard: format!(
855                    "{}{SEPARATOR}{}",
856                    ctx.text(KEYBOARD_SIDE),
857                    layout_picker::label(ctx, self.layout)
858                ),
859                controller: ctx.text(CONTROLLER_SIDE).to_owned(),
860                shown: self.side,
861                focused: self.is_on_switch(),
862            }),
863            columns: columns.collect(),
864            groups,
865            restore: RestoreView {
866                label: ctx.text(RESTORE_DEFAULTS).to_owned(),
867                focused: self.row == self.restore_row(),
868            },
869            not_mapped: ctx.help_keys().not_mapped().to_owned(),
870            message: self.message.clone(),
871            question: self
872                .restoring
873                .as_ref()
874                .map(|ask| Self::restore_view(ctx, ask, self.layout, km)),
875            choices: self.choice.map(|focused| ChoicesView {
876                lines: CHOICES.map(|key| ctx.text(key).to_owned()).to_vec(),
877                focused,
878                slot: self.slot,
879            }),
880            help: self.help(ctx),
881        }
882    }
883
884    /// The heading over the rows of `group`.
885    fn group_heading(&self, ctx: &Ctx<G>, group: RowGroup) -> String {
886        let key = match (group, self.side) {
887            (RowGroup::Shared(heading), _) => heading,
888            (RowGroup::Required, Side::Keyboard) => REQUIRED_HEADING,
889            (RowGroup::Required, Side::Controller) => REQUIRED_BUTTON_HEADING,
890            (RowGroup::Optional, _) => OPTIONAL_HEADING,
891        };
892        ctx.text(key).to_owned()
893    }
894
895    /// Row `i` of `rows`, as the screen shows it.
896    fn row_view(&self, ctx: &Ctx<G>, i: usize) -> RowView<G::Action> {
897        let (action, label, _) = self.rows[i];
898        let label = ctx.text(label);
899        let focused = self.row == i;
900        let fixed = match self.side {
901            Side::Keyboard => Keymap::fixed_chords_for(action)
902                .iter()
903                .map(ToString::to_string)
904                .collect(),
905            Side::Controller => Vec::new(),
906        };
907        let slots = self.slot_names(ctx, action).into_iter().enumerate();
908        let slots = slots.map(|(j, name)| match name {
909            _ if focused && self.slot == j && self.capturing => {
910                SlotView::Capturing(ctx.text(self.side.prompt()).to_owned())
911            }
912            Some(name) => SlotView::Bound(name),
913            None => SlotView::Empty,
914        });
915        let unmapped = self.is_unmapped(action);
916        RowView {
917            action,
918            label: label.to_owned(),
919            fixed,
920            slots: slots.collect(),
921            focus: focused.then_some(self.slot),
922            unmapped,
923            blocks_leaving: unmapped && action.is_required(),
924            lost_key: self.moved.is_some_and(|(lost, _)| lost == action),
925        }
926    }
927}
928
929impl<G: GameData> Screen<G> for KeyBindingsScreen<G> {
930    fn name(&self) -> &'static str {
931        Self::NAME
932    }
933
934    /// Its rows and slots, and its questions; the keys and buttons it captures are not actions.
935    fn actions(&self) -> Option<Vec<G::Action>> {
936        Some(crate::input::ui_actions(crate::input::NAVIGATION))
937    }
938
939    fn update(&mut self, ctx: &mut Ctx<G>, input: &FrameInput<G::Action>) -> Transition<G> {
940        self.tick(input.dt());
941        if !self.releasing.is_empty() {
942            // What the capture's buttons do when let go isn't for here.
943            let up = input.released_buttons();
944            self.releasing.retain(|b| !up.contains(b));
945            self.await_release &= <G::Action as ActionSet>::ALL
946                .iter()
947                .any(|&a| is_cursor(a) && input.is_held(a));
948            return Transition::None;
949        }
950        if self.capturing {
951            // This frame's actions and repeats are ignored: the key or
952            // button is for the slot.
953            if !input.pressed_chords().iter().any(|&c| self.capture(ctx, c)) {
954                self.capture_buttons(ctx, input);
955            }
956            if !self.capturing {
957                self.await_release = <G::Action as ActionSet>::ALL
958                    .iter()
959                    .any(|&a| is_cursor(a) && input.is_held(a));
960            }
961            return Transition::None;
962        }
963        if self.restoring.is_some() {
964            for &action in &input.actions {
965                if self.answer_restore(ctx, action) {
966                    break;
967                }
968            }
969            return Transition::None;
970        }
971        if self.choice.is_some() {
972            for &action in &input.actions {
973                if self.answer_choice(ctx, action) {
974                    break;
975                }
976            }
977            return Transition::None;
978        }
979        if input.pressed_chords().iter().any(|&c| is_clear_slot(c)) {
980            self.clear_slot(ctx);
981        }
982        self.await_release &= <G::Action as ActionSet>::ALL
983            .iter()
984            .any(|&a| is_cursor(a) && input.is_held(a));
985        for &action in &input.actions {
986            match action.ui() {
987                _ if self.await_release && is_cursor(action) => {}
988                Some(UiAction::Up) => self.move_row(ctx, false),
989                Some(UiAction::Down) => self.move_row(ctx, true),
990                Some(UiAction::Left) => self.move_across(ctx, false),
991                Some(UiAction::Right) => self.move_across(ctx, true),
992                Some(UiAction::Confirm) => {
993                    self.confirm(ctx);
994                    if self.capturing || self.restoring.is_some() || self.choice.is_some() {
995                        // The key that confirmed isn't the key to bind (or
996                        // the answer), and nothing after it this frame
997                        // counts.
998                        break;
999                    }
1000                }
1001                Some(UiAction::Cancel) if self.leave(ctx) => return Transition::Pop,
1002                _ => {}
1003            }
1004        }
1005        Transition::None
1006    }
1007
1008    fn draw(&self, ctx: &Ctx<G>, buf: &mut GlyphBuffer) {
1009        (self.paint)(ctx, &self.view(ctx), buf);
1010    }
1011
1012    fn as_any(&self) -> Option<&dyn core::any::Any> {
1013        Some(self)
1014    }
1015}
1016
1017#[cfg(test)]
1018mod tests;