1use shuyi_assets::keymap::Key;
8
9use super::view::{KeyRole, LayoutPickerView, LayoutView};
10use crate::ctx::{Ctx, GameData};
11use crate::frame::color::EngineColor;
12use crate::frame::draw::PanelLook;
13use crate::frame::glyph_buffer::{BoxStyle, Cell, GlyphBuffer, Rect};
14use crate::prelude::*;
15use crate::screens::print_centred;
16
17const TITLE_ROW: i32 = 1;
19const FIRST_PANEL_ROW: i32 = 3;
21const PANEL_W: i32 = 76;
23const PANEL_H: i32 = 13;
24const PANEL_GAP: i32 = 1;
26
27const KEYS_X: i32 = 3;
30const TOP_ROW_Y: i32 = 4;
31const HOME_ROW_Y: i32 = 5;
32const SPACE_ROW_Y: i32 = 7;
33const LEGEND_X: i32 = 50;
34const LEGEND_Y: i32 = 2;
35const LEGEND_KEY_W: usize = 12;
37
38const CAP_W: i32 = 3;
40const TOP_ROW: [Key; 10] = [
44 Key::Q,
45 Key::W,
46 Key::E,
47 Key::R,
48 Key::T,
49 Key::Y,
50 Key::U,
51 Key::I,
52 Key::O,
53 Key::P,
54];
55const HOME_ROW: [Key; 10] = [
57 Key::A,
58 Key::S,
59 Key::D,
60 Key::F,
61 Key::G,
62 Key::H,
63 Key::J,
64 Key::K,
65 Key::L,
66 Key::Semicolon,
67];
68const UP_CAP: (Key, &str) = (Key::Up, "↑");
71const LOWER_ARROW_CAPS: [(Key, &str); 3] = [(Key::Left, "←"), (Key::Down, "↓"), (Key::Right, "→")];
73const SPACE_BAR: Key = Key::Space;
76const ARROWS_X: i32 = KEYS_X + 10 * CAP_W + 4;
78const SPACE_X: i32 = KEYS_X + 1 + 2 * CAP_W;
80const SPACE_W: usize = 19;
81
82fn role_colour(role: KeyRole) -> EngineColor {
84 match role {
85 KeyRole::Movement => EngineColor::Accent,
86 KeyRole::Bound => EngineColor::Highlight,
87 KeyRole::Unbound => EngineColor::TextDim,
88 }
89}
90
91pub fn paint<G: GameData>(ctx: &Ctx<G>, view: &LayoutPickerView, buf: &mut GlyphBuffer) {
93 let c = |e| ctx.palette.engine(e);
94 let black = c(EngineColor::Background);
95 buf.fill_rect(buf.bounds(), Cell::new(' ', c(EngineColor::Text), black));
96 print_centred(
97 buf,
98 TITLE_ROW,
99 &view.title,
100 c(EngineColor::Highlight),
101 black,
102 );
103 let x = (i32::from(buf.width()) - PANEL_W) / 2;
104 for (i, layout) in (0..).zip(&view.layouts) {
105 let y = FIRST_PANEL_ROW + i * (PANEL_H + PANEL_GAP);
106 paint_panel(ctx, buf, layout, x, y);
107 }
108 let bottom = i32::from(buf.height()) - 1;
109 print_centred(buf, bottom, &view.help, c(EngineColor::TextDim), black);
110}
111
112fn paint_panel<G: GameData>(
115 ctx: &Ctx<G>,
116 buf: &mut GlyphBuffer,
117 layout: &LayoutView,
118 x: i32,
119 y: i32,
120) {
121 let c = |e| ctx.palette.engine(e);
122 let bg = c(EngineColor::Panel);
123 let rect = Rect::new(x, y, PANEL_W, PANEL_H);
124 let (style, border, title, title_fg) = if layout.focused {
125 (
126 BoxStyle::Double,
127 c(EngineColor::Focus),
128 format!("► {}", layout.name),
130 c(EngineColor::Highlight),
131 )
132 } else {
133 (
134 BoxStyle::Single,
135 c(EngineColor::Border),
136 layout.name.clone(),
138 c(EngineColor::Text),
139 )
140 };
141 let look = PanelLook::new(style, c(EngineColor::Text), bg)
142 .border(border)
143 .title(title_fg);
144 buf.panel(rect, &look, Some(&title));
145
146 let cap_fg = |key| c(role_colour(layout.role(key)));
147 let dim = c(EngineColor::TextDim);
148 let mut cap = |cx: i32, cy: i32, key: Key, glyph: &str| {
149 buf.print(cx, cy, "[", dim, bg);
150 buf.print(cx + 1, cy, glyph, cap_fg(key), bg);
151 buf.print(cx + 2, cy, "]", dim, bg);
152 };
153 for (col, key) in (0..).zip(TOP_ROW) {
154 cap(x + KEYS_X + col * CAP_W, y + TOP_ROW_Y, key, key.name());
155 }
156 for (col, key) in (0..).zip(HOME_ROW) {
157 cap(
158 x + KEYS_X + 1 + col * CAP_W,
159 y + HOME_ROW_Y,
160 key,
161 key.name(),
162 );
163 }
164 let (up, up_glyph) = UP_CAP;
165 cap(x + ARROWS_X + CAP_W, y + TOP_ROW_Y, up, up_glyph);
166 for (col, (key, glyph)) in (0..).zip(LOWER_ARROW_CAPS) {
167 cap(x + ARROWS_X + col * CAP_W, y + HOME_ROW_Y, key, glyph);
168 }
169 let space = format!("[{:^w$}]", SPACE_BAR.name(), w = SPACE_W - 2);
170 buf.print(x + SPACE_X, y + SPACE_ROW_Y, &space, dim, bg);
171 let name_x = x + SPACE_X + (i32::try_from(SPACE_W).unwrap_or(0) - 5) / 2;
172 buf.print(
173 name_x,
174 y + SPACE_ROW_Y,
175 SPACE_BAR.name(),
176 cap_fg(SPACE_BAR),
177 bg,
178 );
179
180 for (row, line) in (y + LEGEND_Y..).zip(&layout.legend) {
181 let fg = if line.movement {
182 c(EngineColor::Accent)
183 } else {
184 c(EngineColor::Highlight)
185 };
186 buf.print(x + LEGEND_X, row, &line.keys, fg, bg);
187 let what_x = x + LEGEND_X + i32::try_from(LEGEND_KEY_W).unwrap_or(0);
188 buf.print(what_x, row, &line.what, c(EngineColor::Text), bg);
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::frame::console::ConsoleSpec;
196 use crate::screens::layout_picker::LayoutPickerScreen;
197 use crate::screens::layout_picker::view::{KeyCapView, LegendRow};
198 use crate::toy::{self, ToyCtx, ctx};
199
200 const PC: ConsoleSpec = ConsoleSpec::text(100, 32);
202
203 fn painted(c: &ToyCtx, view: &LayoutPickerView) -> GlyphBuffer {
204 let stale = Cell::new(
205 'x',
206 c.palette.engine(EngineColor::Warning),
207 c.palette.engine(EngineColor::Warning),
208 );
209 let mut buf = GlyphBuffer::with_spec(PC, stale);
210 paint(c, view, &mut buf);
211 buf
212 }
213
214 fn text(buf: &GlyphBuffer, y: i32) -> String {
215 (0..i32::from(buf.width()))
216 .map(|x| buf.get(x, y).map_or(' ', |c| c.glyph))
217 .collect()
218 }
219
220 fn col(row: &str, word: &str) -> usize {
222 row[..row.find(word).unwrap()].chars().count()
223 }
224
225 fn view(role_of_q: KeyRole, role_of_f: KeyRole) -> LayoutPickerView {
227 let layout = |name: &str, focused| LayoutView {
228 layout: toy::RIGHT_HANDED,
229 name: name.to_owned(),
230 focused,
231 legend: vec![
232 LegendRow {
233 keys: "arrows".to_owned(),
234 what: "move".to_owned(),
235 movement: true,
236 },
237 LegendRow {
238 keys: "f".to_owned(),
239 what: "select".to_owned(),
240 movement: false,
241 },
242 ],
243 keys: vec![
244 KeyCapView {
245 key: Key::Q,
246 role: role_of_q,
247 },
248 KeyCapView {
249 key: Key::F,
250 role: role_of_f,
251 },
252 ],
253 };
254 LayoutPickerView {
255 title: "Pick".to_owned(),
256 layouts: vec![layout("First", false), layout("Second", true)],
257 help: "help".to_owned(),
258 }
259 }
260
261 const LEFT: i32 = (PC.cols as i32 - PANEL_W) / 2;
262 const SECOND: i32 = FIRST_PANEL_ROW + PANEL_H + PANEL_GAP;
263
264 #[test]
265 fn two_panels_stack_under_the_centred_title_with_help_at_the_bottom() {
266 let c = ctx();
267 let buf = painted(&c, &view(KeyRole::Bound, KeyRole::Bound));
268 assert_eq!(text(&buf, TITLE_ROW).trim(), "Pick");
269 let first = text(&buf, FIRST_PANEL_ROW);
270 let second = text(&buf, SECOND);
271 assert_eq!(first.find('┌'), Some(usize::try_from(LEFT).unwrap()));
272 assert!(first.contains("┌─ First ─"), "{first}");
273 assert!(second.contains("╔═ ► Second ═"), "{second}");
275 assert!(text(&buf, FIRST_PANEL_ROW + PANEL_H - 1).contains('└'));
276 assert!(text(&buf, SECOND + PANEL_H - 1).contains('╚'));
277 assert_eq!(text(&buf, i32::from(PC.rows) - 1).trim(), "help");
278 let found = text(&buf, FIRST_PANEL_ROW + PANEL_H);
280 assert!(found.trim().is_empty(), "{found:?}");
281 }
282
283 #[test]
284 fn the_caps_sit_where_the_keyboard_has_them_and_take_their_role_colour() {
285 let c = ctx();
286 let buf = painted(&c, &view(KeyRole::Movement, KeyRole::Unbound));
287 let at = |x, y| *buf.get(x, y).unwrap();
288 let (qx, qy) = (LEFT + KEYS_X, FIRST_PANEL_ROW + TOP_ROW_Y);
291 let q_row = text(&buf, qy);
292 assert_eq!(col(&q_row, "[q]"), usize::try_from(qx).unwrap());
293 assert_eq!(at(qx + 1, qy).fg, c.palette.engine(EngineColor::Accent));
294 let (fx, fy) = (LEFT + KEYS_X + 1 + 3 * CAP_W, FIRST_PANEL_ROW + HOME_ROW_Y);
295 assert_eq!(at(fx + 1, fy).glyph, 'f');
296 assert_eq!(at(fx + 1, fy).fg, c.palette.engine(EngineColor::TextDim));
297 assert_eq!(at(qx, qy).fg, c.palette.engine(EngineColor::TextDim));
299 let (wx, wy) = (LEFT + KEYS_X + CAP_W, FIRST_PANEL_ROW + TOP_ROW_Y);
301 assert_eq!(at(wx + 1, wy).glyph, 'w');
302 assert_eq!(at(wx + 1, wy).fg, c.palette.engine(EngineColor::TextDim));
303 let arrows = text(&buf, FIRST_PANEL_ROW + HOME_ROW_Y);
305 assert!(arrows.contains("[←][↓][→]"), "{arrows}");
306 assert!(text(&buf, FIRST_PANEL_ROW + TOP_ROW_Y).contains("[↑]"));
307 assert!(text(&buf, FIRST_PANEL_ROW + SPACE_ROW_Y).contains("[ Space ]"));
308 }
309
310 #[test]
311 fn the_legend_is_two_columns_and_the_movement_keys_are_set_apart() {
312 let c = ctx();
313 let buf = painted(&c, &view(KeyRole::Bound, KeyRole::Bound));
314 let (x, y) = (LEFT + LEGEND_X, FIRST_PANEL_ROW + LEGEND_Y);
315 let first = text(&buf, y);
316 assert_eq!(col(&first, "arrows"), usize::try_from(x).unwrap());
317 assert_eq!(
318 col(&first, "move"),
319 usize::try_from(x).unwrap() + LEGEND_KEY_W
320 );
321 let at = |x, y| buf.get(x, y).unwrap().fg;
322 assert_eq!(at(x, y), c.palette.engine(EngineColor::Accent));
323 assert_eq!(at(x, y + 1), c.palette.engine(EngineColor::Highlight));
324 let what_x = x + i32::try_from(LEGEND_KEY_W).unwrap();
325 assert_eq!(at(what_x, y), c.palette.engine(EngineColor::Text));
326 }
327
328 #[test]
329 fn it_paints_the_real_screen_over_the_whole_buffer() {
330 let c = ctx();
331 let screen = LayoutPickerScreen::new(&c, &toy::LEGEND);
332 let stale = Cell::new(
333 'x',
334 c.palette.engine(EngineColor::Warning),
335 c.palette.engine(EngineColor::Warning),
336 );
337 let buf = painted(&c, &screen.view(&c));
338 let left = (0..i32::from(PC.rows))
339 .flat_map(|y| (0..i32::from(PC.cols)).map(move |x| (x, y)))
340 .filter(|&(x, y)| buf.get(x, y) == Some(&stale))
341 .count();
342 assert_eq!(left, 0);
343 }
344
345 #[test]
346 fn roles_have_their_colours() {
347 assert_eq!(role_colour(KeyRole::Movement), EngineColor::Accent);
348 assert_eq!(role_colour(KeyRole::Bound), EngineColor::Highlight);
349 assert_eq!(role_colour(KeyRole::Unbound), EngineColor::TextDim);
350 }
351}