1use super::RowKind;
10use super::view::{ActionsView, ProfilesView, RowView};
11use crate::ctx::{Ctx, GameData};
12use crate::frame::color::EngineColor;
13use crate::frame::glyph_buffer::{BoxStyle, Cell, GlyphBuffer, Rect};
14use crate::prelude::*;
15use crate::screens::print_centred;
16use crate::widgets::ask;
17use crate::widgets::name_entry::glyph::{TYPING_HEIGHT, grid_height, paint_grid, paint_typing};
18
19const PANEL_Y: i32 = 5;
22const PANEL_W: i32 = 40;
24const LABEL_DX: i32 = 4;
26const MARK_DX: i32 = 24;
28const NAME_BOX_W: i32 = 46;
30
31fn panel(rows: usize, buf: &GlyphBuffer) -> Rect {
34 let rows = i32::try_from(rows).unwrap_or(0);
35 let width = i32::from(buf.width());
36 let w = PANEL_W.min(width);
37 Rect::new((width - w) / 2, PANEL_Y, w, rows + 5)
38}
39
40fn middle(buf: &GlyphBuffer, w: i32, h: i32) -> Rect {
43 let w = w.min(i32::from(buf.width()));
44 Rect::new(
45 (i32::from(buf.width()) - w) / 2,
46 ((i32::from(buf.height()) - h) / 2).max(0),
47 w,
48 h,
49 )
50}
51
52pub fn paint<G: GameData>(ctx: &Ctx<G>, view: &ProfilesView, buf: &mut GlyphBuffer) {
54 let c = |e| ctx.palette.engine(e);
55 let (black, bg) = (c(EngineColor::Background), c(EngineColor::Panel));
56 let (text, dim) = (c(EngineColor::Text), c(EngineColor::TextDim));
57 let panel = panel(view.rows.len(), buf);
58 buf.fill_rect(buf.bounds(), Cell::new(' ', text, black));
59 buf.fill_rect(panel, Cell::new(' ', text, bg));
60 buf.draw_box(panel, BoxStyle::Single, c(EngineColor::Border), bg);
61 let title = format!(" {} ", view.title);
62 buf.print(panel.x + 2, panel.y, &title, c(EngineColor::Highlight), bg);
63
64 let mut y = panel.y + 2;
65 for row in &view.rows {
66 if row.kind == RowKind::Add {
67 y += 1;
69 }
70 paint_row(ctx, buf, row, &view.current_mark, panel.x, y);
71 y += 1;
72 }
73
74 if let Some(message) = &view.message {
75 let y = panel.y + panel.h + 1;
77 print_centred(buf, y, message, c(EngineColor::Highlight), black);
78 }
79 if let Some(actions) = &view.actions {
80 paint_actions(ctx, buf, actions);
81 }
82 if let Some(grid) = &view.grid {
83 let rect = middle(buf, NAME_BOX_W, grid_height(grid));
84 paint_grid(&ctx.palette, buf, rect, grid);
85 }
86 if let Some(typing) = &view.typing {
87 let rect = middle(buf, NAME_BOX_W, TYPING_HEIGHT);
88 paint_typing(&ctx.palette, buf, rect, typing);
89 }
90 if let Some(question) = &view.leaving {
91 ask::glyph::paint(&ctx.palette, question, buf);
92 }
93 if let Some(question) = &view.deleting {
94 ask::glyph::paint(&ctx.palette, question, buf);
95 }
96 let bottom = i32::from(buf.height()) - 1;
97 print_centred(buf, bottom, &view.help, dim, black);
98}
99
100fn paint_row<G: GameData>(
104 ctx: &Ctx<G>,
105 buf: &mut GlyphBuffer,
106 row: &RowView,
107 mark: &str,
108 x: i32,
109 y: i32,
110) {
111 let c = |e| ctx.palette.engine(e);
112 let bg = c(EngineColor::Panel);
113 let fg = if row.enabled {
114 c(EngineColor::Text)
115 } else {
116 c(EngineColor::TextDim)
117 };
118 let label_x = x + LABEL_DX;
119 if row.focused {
120 let bar = c(EngineColor::Focus);
121 buf.print(label_x - 1, y, &format!(" {} ", row.label), bg, bar);
122 } else {
123 buf.print(label_x, y, &row.label, fg, bg);
124 }
125 if row.current {
126 buf.print(x + MARK_DX, y, mark, c(EngineColor::Highlight), bg);
127 }
128}
129
130fn paint_actions<G: GameData>(ctx: &Ctx<G>, buf: &mut GlyphBuffer, actions: &ActionsView) {
135 let c = |e| ctx.palette.engine(e);
136 let (bg, text) = (c(EngineColor::Panel), c(EngineColor::Text));
137 let items = &actions.menu.items;
138 let labels: Vec<String> = items.iter().map(|i| ctx.label(&i.label)).collect();
139 let widest = labels.iter().map(|l| buf.text_cells(l)).max();
140 let w = widest.unwrap_or(0).max(buf.text_cells(&actions.title) + 2) + 6;
141 let h = i32::try_from(items.len()).unwrap_or(0) + 4;
142 let rect = middle(buf, w, h);
143 buf.fill_rect(rect, Cell::new(' ', text, bg));
144 buf.draw_box(rect, BoxStyle::Double, c(EngineColor::Focus), bg);
145 let title = format!(" {} ", actions.title);
146 buf.print(rect.x + 2, rect.y, &title, c(EngineColor::Highlight), bg);
147 for ((item, label), y) in items.iter().zip(&labels).zip(rect.y + 2..) {
148 if item.focused {
149 let bar = c(EngineColor::Focus);
150 buf.print(rect.x + 2, y, &format!(" {label} "), bg, bar);
151 } else {
152 buf.print(rect.x + 3, y, label, text, bg);
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::frame::console::ConsoleSpec;
161 use crate::profiles::ProfileId;
162 use crate::screens::profiles::DeleteChoice;
163 use crate::toy::{ToyCtx, ctx};
164 use crate::widgets::{AnswerView, AskView, Menu, MenuItem};
165
166 const PC: ConsoleSpec = ConsoleSpec::text(100, 32);
167 const PANEL_X: i32 = 30;
169
170 fn painted(c: &ToyCtx, view: &ProfilesView) -> GlyphBuffer {
172 let black = c.palette.engine(EngineColor::Background);
173 let mut buf = GlyphBuffer::with_spec(PC, Cell::new(' ', black, black));
174 paint(c, view, &mut buf);
175 buf
176 }
177
178 fn row(buf: &GlyphBuffer, y: i32) -> String {
180 let glyphs = (0..i32::from(buf.width())).map(|x| buf.get(x, y).map_or(' ', |c| c.glyph));
181 glyphs.collect::<String>().trim().to_owned()
182 }
183
184 fn profile(label: &str, current: bool, focused: bool) -> RowView {
185 RowView {
186 kind: RowKind::Profile(ProfileId::FIRST),
187 label: label.to_owned(),
188 current,
189 focused,
190 enabled: true,
191 }
192 }
193
194 fn view(rows: Vec<RowView>) -> ProfilesView {
195 ProfilesView {
196 title: "Profile".to_owned(),
197 rows,
198 current_mark: "in use".to_owned(),
199 actions: None,
200 typing: None,
201 grid: None,
202 leaving: None,
203 deleting: None,
204 message: None,
205 help: "help".to_owned(),
206 }
207 }
208
209 #[test]
210 fn the_profiles_then_a_blank_row_then_the_add_row_the_current_one_marked() {
211 let c = ctx();
212 let add = RowView {
213 kind: RowKind::Add,
214 label: "Add a profile".to_owned(),
215 current: false,
216 focused: false,
217 enabled: false,
218 };
219 let v = view(vec![
220 profile("Player 1", true, false),
221 profile("Mei", false, true),
222 add,
223 ]);
224 let buf = painted(&c, &v);
225 assert!(row(&buf, PANEL_Y).starts_with("┌─ Profile ─"));
226 let first = row(&buf, PANEL_Y + 2);
227 assert_eq!(first, format!("│ {:<20}in use{:>10}", "Player 1", "│"));
228 assert!(row(&buf, PANEL_Y + 3).starts_with("│ Mei "));
229 assert_eq!(row(&buf, PANEL_Y + 4), row(&buf, PANEL_Y + 1));
230 assert!(row(&buf, PANEL_Y + 5).starts_with("│ Add a profile"));
231 let label_x = PANEL_X + LABEL_DX;
233 let dim = c.palette.engine(EngineColor::TextDim);
234 assert_eq!(buf.get(label_x, PANEL_Y + 5).map(|c| c.fg), Some(dim));
235 let bar = c.palette.engine(EngineColor::Focus);
236 assert_eq!(buf.get(label_x, PANEL_Y + 3).map(|c| c.bg), Some(bar));
237 assert_eq!(row(&buf, PANEL_Y + 6), row(&buf, PANEL_Y + 1));
238 assert!(row(&buf, PANEL_Y + 7).starts_with('└'));
239 assert_eq!(row(&buf, 31), "help");
240 }
241
242 #[test]
243 fn the_message_is_painted_one_blank_row_under_the_panel() {
244 let c = ctx();
245 let mut v = view(vec![profile("Player 1", true, true)]);
246 v.message = Some("There can be at most 8 profiles.".to_owned());
247 let buf = painted(&c, &v);
248 let bottom = PANEL_Y + 5;
249 assert!(row(&buf, bottom).starts_with('└'));
250 assert_eq!(row(&buf, bottom + 1), "");
251 assert_eq!(row(&buf, bottom + 2), "There can be at most 8 profiles.");
252 }
253
254 #[test]
255 fn the_delete_question_is_the_question_widgets_box() {
256 let c = ctx();
257 let mut v = view(vec![profile("Player 1", true, true)]);
258 let answer = |answer, label: &str, focused| AnswerView {
259 answer,
260 label: label.to_owned(),
261 focused,
262 };
263 v.deleting = Some(AskView {
264 text: "Delete Mei?".to_owned(),
265 answers: vec![
266 answer(DeleteChoice::Yes, "Yes", false),
267 answer(DeleteChoice::No, "No", true),
268 ],
269 keys: None,
270 });
271 let buf = painted(&c, &v);
272 let top = (i32::from(PC.rows) - ask::glyph::HEIGHT) / 2;
273 assert!(row(&buf, top + 2).contains("║ Delete Mei? ║"));
274 assert!(row(&buf, top + 3).contains("║ Yes No ║"));
275 }
276
277 #[test]
278 fn the_actions_are_a_box_titled_with_the_profile_the_focused_item_lit() {
279 let c = ctx();
280 let mut v = view(vec![profile("Player 1", true, true)]);
281 let menu = Menu::new(vec![
282 MenuItem::keyed("profile.action.rename"),
283 MenuItem::keyed("profile.action.delete"),
284 ]);
285 v.actions = Some(ActionsView {
286 title: "Mei".to_owned(),
287 menu: menu.view(),
288 });
289 let buf = painted(&c, &v);
290 let top = (i32::from(PC.rows) - 6) / 2;
291 assert!(row(&buf, top).contains("╔═ Mei ═"), "{}", row(&buf, top));
292 assert!(row(&buf, top + 2).contains("║ Rename ║"));
293 assert!(row(&buf, top + 3).contains("║ Delete ║"));
294 let bar = c.palette.engine(EngineColor::Focus);
295 let lit: String = (0..i32::from(PC.cols))
296 .filter_map(|x| buf.get(x, top + 2))
297 .filter(|cell| cell.bg == bar)
298 .map(|cell| cell.glyph)
299 .collect();
300 assert_eq!(lit, " Rename ");
301 if let Some(actions) = &mut v.actions {
303 actions.title = "Abcdefghijkl".to_owned();
304 }
305 let buf = painted(&c, &v);
306 assert!(
307 row(&buf, top).contains("╔═ Abcdefghijkl ═══╗"),
308 "{}",
309 row(&buf, top)
310 );
311 }
312}