Skip to main content

shuyi_ui/screens/
layout_picker.rs

1//! The first-launch "Pick your layout" screen: one panel per [`Layout`]
2//! the keymap file lists, with a small keyboard diagram and a legend, both
3//! read from that layout's bindings. A game's options open it again to
4//! switch layout ([`LayoutPickerScreen::change`]). What the legend lists
5//! is the game's ([`Legend`]).
6//!
7//! **The look is a skin.** This module decides what the screen shows and
8//! does, and says it as plain data: a [`LayoutPickerView`]
9//! ([`LayoutPickerScreen::view`]), with each layout's legend and what each
10//! key of its keyboard does. [`glyph::paint`] draws that view as glyphs and
11//! decides where the keys sit. Tests of what happened read the view; only
12//! [`glyph`]'s tests read cells.
13
14pub mod glyph;
15pub mod view;
16
17use crate::prelude::*;
18use core::fmt;
19pub use view::{KeyCapView, KeyRole, LayoutPickerView, LayoutView, LegendRow};
20
21use shuyi_assets::keymap::{ActionSet, Chord, Key, Layout, UiAction};
22
23use crate::ctx::{Ctx, GameData};
24use crate::frame::glyph_buffer::GlyphBuffer;
25use crate::input::help_keys::{all_key_names, cursor_keys_name, key_name};
26use crate::input::keymap::Keymap;
27use crate::screen::{FrameInput, Screen, Transition};
28use crate::widgets::{Menu, MenuEvent, MenuItem};
29
30/// The key of the title in the language files ([`Ctx::text`]).
31pub const TITLE: &str = "layout_picker.title";
32/// The text key of the help line the first time, before any layout is
33/// chosen, with `{up}`, `{down}` and `{confirm}`: every key of each.
34pub const HELP_FIRST: &str = "layout_picker.help_first";
35/// The text key of the help line when the picker is opened to switch
36/// layout (it names the Cancel key too).
37pub const HELP_CHANGE: &str = "layout_picker.help_change";
38
39/// Which keys a line of the legend names.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum LegendKeys<A> {
42    /// The cursor keys: the line that is flagged as movement.
43    Cursor,
44    /// Every key of the action.
45    All(A),
46    /// The action's first key.
47    First(A),
48}
49
50/// The game's legend, top to bottom: which keys each line names, and the
51/// text key of what they do.
52pub type Legend<A> = [(LegendKeys<A>, &'static str)];
53
54/// The name shown for a layout, in `ctx`'s language: the text key the
55/// keymap file gives it (empty for a layout the keymap doesn't have, which
56/// is never in use).
57pub fn label<G: GameData>(ctx: &Ctx<G>, layout: Layout) -> &str {
58    ctx.game
59        .keymap_def()
60        .layout_text(layout)
61        .map_or("", |key| ctx.text(key))
62}
63
64/// The layouts the picker shows, in the keymap file's order.
65fn layouts<G: GameData>(ctx: &Ctx<G>) -> Vec<Layout> {
66    ctx.game.keymap_def().layout_ids().collect()
67}
68
69/// Shows every layout and saves the one picked. Asked for by a key press
70/// while no layout is chosen, it can't be cancelled with a key (the
71/// keyboard needs a layout), but any controller button closes it without
72/// choosing: that player is on the controller. Pops itself once a layout
73/// is picked.
74pub struct LayoutPickerScreen<G: GameData> {
75    menu: Menu<Layout>,
76    /// Whether Cancel backs out without changing anything (opened from
77    /// the options).
78    cancellable: bool,
79    /// What the legend lists.
80    legend: &'static Legend<G::Action>,
81}
82
83impl<G: GameData> fmt::Debug for LayoutPickerScreen<G> {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        // check-text: not player text, the type name for Debug
86        f.debug_struct("LayoutPickerScreen")
87            .field("menu", &self.menu)
88            .field("cancellable", &self.cancellable)
89            .field("legend", &self.legend)
90            .finish()
91    }
92}
93
94impl<G: GameData> Clone for LayoutPickerScreen<G> {
95    fn clone(&self) -> Self {
96        Self {
97            menu: self.menu.clone(),
98            cancellable: self.cancellable,
99            legend: self.legend,
100        }
101    }
102}
103
104impl<G: GameData> LayoutPickerScreen<G> {
105    /// Its [`Screen::name`].
106    pub const NAME: &'static str = "layout_picker";
107
108    /// The picker with the first layout focused, its legend listing
109    /// `legend`.
110    pub fn new(ctx: &Ctx<G>, legend: &'static Legend<G::Action>) -> Self {
111        Self {
112            // A layout must be picked.
113            menu: Self::menu(ctx).without_cancel(),
114            cancellable: false,
115            legend,
116        }
117    }
118
119    /// The picker as the options open it, to switch layout: the layout in
120    /// use focused, and Cancel backs out without changing anything.
121    pub fn change(ctx: &Ctx<G>, legend: &'static Legend<G::Action>) -> Self {
122        let current = layouts(ctx).iter().position(|&l| Some(l) == ctx.layout());
123        Self {
124            menu: Self::menu(ctx).focused(current.unwrap_or(0)),
125            cancellable: true,
126            legend,
127        }
128    }
129
130    /// One line per layout, standing for it. The menu only keeps the
131    /// focus: it is never drawn, so its lines have no text.
132    fn menu(ctx: &Ctx<G>) -> Menu<Layout> {
133        let items = layouts(ctx)
134            .into_iter()
135            .map(|layout| MenuItem::new(String::new()).with_value(layout));
136        Menu::new(items.collect())
137    }
138
139    /// The focused layout (`None` only for a keymap with no layouts, which
140    /// the loader refuses).
141    pub fn focused(&self) -> Option<Layout> {
142        self.menu.value(self.menu.focus()).copied()
143    }
144
145    /// The bottom help line: every Cursor up and Cursor down key, then every
146    /// Confirm key, from the active keymap (before any layout is chosen,
147    /// the layout picker's own keys).
148    pub fn help(ctx: &Ctx<G>) -> String {
149        Self::help_with(ctx, false)
150    }
151
152    /// [`help`](Self::help); when the picker can be backed out of
153    /// (`cancellable`, from Options), the line that also names the Cancel
154    /// key.
155    fn help_with(ctx: &Ctx<G>, cancellable: bool) -> String {
156        if cancellable {
157            return ctx.text_with(HELP_CHANGE, &[]);
158        }
159        let km = ctx.help_keys();
160        let names = |ui| G::Action::for_ui(ui).map_or_else(String::new, |a| all_key_names(km, a));
161        let up = names(UiAction::Up);
162        let down = names(UiAction::Down);
163        let confirm = names(UiAction::Confirm);
164        ctx.text_with(
165            HELP_FIRST,
166            &[("up", &up), ("down", &down), ("confirm", &confirm)],
167        )
168    }
169
170    /// The legend for `km`, in `ctx`'s language: the keys and what they
171    /// do; an action with no key shows `! not mapped`. Always its keyboard
172    /// keys: the screen is about the keyboard's layouts.
173    pub fn legend(&self, ctx: &Ctx<G>, km: &Keymap<G::Action>) -> Vec<LegendRow> {
174        let km = ctx.help_keys().of(km).on_keyboard();
175        let row = |&(keys, what): &(LegendKeys<G::Action>, &str)| LegendRow {
176            keys: match keys {
177                LegendKeys::Cursor => cursor_keys_name(km),
178                LegendKeys::All(action) => all_key_names(km, action),
179                LegendKeys::First(action) => key_name(km, action),
180            },
181            what: ctx.text(what).to_owned(),
182            movement: keys == LegendKeys::Cursor,
183        };
184        self.legend.iter().map(row).collect()
185    }
186
187    /// The screen as it is now, as plain data for a skin to paint
188    /// ([`glyph::paint`]): each layout with its legend and what every key
189    /// of its keyboard does, the one in focus, and the help line.
190    pub fn view(&self, ctx: &Ctx<G>) -> LayoutPickerView {
191        let focused = self.focused();
192        let layouts = layouts(ctx).into_iter().map(|layout| {
193            let km = ctx.keymap_for(layout);
194            LayoutView {
195                layout,
196                name: label(ctx, layout).to_owned(),
197                focused: Some(layout) == focused,
198                legend: self.legend(ctx, &km),
199                keys: key_caps(&km),
200            }
201        });
202        LayoutPickerView {
203            title: ctx.text(TITLE).to_owned(),
204            layouts: layouts.collect(),
205            help: Self::help_with(ctx, self.cancellable),
206        }
207    }
208}
209
210/// What `key` does in `km`: moves the cursor (a key that repeats), does
211/// something else, or nothing.
212// check-keys: keyboard picture
213fn key_role<A: ActionSet>(km: &Keymap<A>, key: Key) -> KeyRole {
214    match km.action(Chord::plain(key)) {
215        Some(action) if action.is_repeatable() => KeyRole::Movement,
216        Some(_) => KeyRole::Bound,
217        None => KeyRole::Unbound,
218    }
219}
220
221/// Every key of the keyboard with its role in `km`; a skin draws the ones
222/// its picture has.
223// check-keys: keyboard picture
224fn key_caps<A: ActionSet>(km: &Keymap<A>) -> Vec<KeyCapView> {
225    Key::ALL
226        .iter()
227        .map(|&key| KeyCapView {
228            key,
229            role: key_role(km, key),
230        })
231        .collect()
232}
233
234impl<G: GameData> Screen<G> for LayoutPickerScreen<G> {
235    fn name(&self) -> &'static str {
236        Self::NAME
237    }
238
239    /// Its menu's.
240    fn actions(&self) -> Option<Vec<G::Action>> {
241        Some(crate::input::ui_actions(Menu::<()>::ACTIONS))
242    }
243
244    fn update(&mut self, ctx: &mut Ctx<G>, input: &FrameInput<G::Action>) -> Transition<G> {
245        // Bound or not; the button does nothing else.
246        if !self.cancellable && input.pad_pressed() {
247            return Transition::Pop;
248        }
249        for &action in &input.actions {
250            match self.menu.handle_with_sound(action, &mut ctx.audio) {
251                Some(MenuEvent::Chosen(layout)) => {
252                    // If saving fails the layout is still used this
253                    // session; the player is just asked again next launch.
254                    let _ = ctx.choose_layout(layout);
255                    return Transition::Pop;
256                }
257                Some(MenuEvent::Cancelled) => return Transition::Pop,
258                None => {}
259            }
260        }
261        Transition::None
262    }
263
264    fn draw(&self, ctx: &Ctx<G>, buf: &mut GlyphBuffer) {
265        glyph::paint(ctx, &self.view(ctx), buf);
266    }
267
268    fn as_any(&self) -> Option<&dyn core::any::Any> {
269        Some(self)
270    }
271}
272
273#[cfg(test)]
274mod tests;