1pub 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
30pub const TITLE: &str = "layout_picker.title";
32pub const HELP_FIRST: &str = "layout_picker.help_first";
35pub const HELP_CHANGE: &str = "layout_picker.help_change";
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum LegendKeys<A> {
42 Cursor,
44 All(A),
46 First(A),
48}
49
50pub type Legend<A> = [(LegendKeys<A>, &'static str)];
53
54pub 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
64fn layouts<G: GameData>(ctx: &Ctx<G>) -> Vec<Layout> {
66 ctx.game.keymap_def().layout_ids().collect()
67}
68
69pub struct LayoutPickerScreen<G: GameData> {
75 menu: Menu<Layout>,
76 cancellable: bool,
79 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 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 pub const NAME: &'static str = "layout_picker";
107
108 pub fn new(ctx: &Ctx<G>, legend: &'static Legend<G::Action>) -> Self {
111 Self {
112 menu: Self::menu(ctx).without_cancel(),
114 cancellable: false,
115 legend,
116 }
117 }
118
119 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 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 pub fn focused(&self) -> Option<Layout> {
142 self.menu.value(self.menu.focus()).copied()
143 }
144
145 pub fn help(ctx: &Ctx<G>) -> String {
149 Self::help_with(ctx, false)
150 }
151
152 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 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 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
210fn 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
221fn 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 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 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 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;