1use super::app::CalculatorApp;
9use super::keypad::{Keypad, KeypadWidget, Rect, TextBuffer};
10
11pub fn render_to_buffer(app: &CalculatorApp, width: u16, height: u16) -> TextBuffer {
13 let area = Rect::new(0, 0, width, height);
14 let mut buf = TextBuffer::empty(area);
15 let ui = CalculatorUI::new(app);
16 ui.render(area, &mut buf);
17 buf
18}
19
20pub struct CalculatorUI<'a> {
22 app: &'a CalculatorApp,
23 keypad: Keypad,
24}
25
26impl<'a> CalculatorUI<'a> {
27 #[must_use]
29 pub fn new(app: &'a CalculatorApp) -> Self {
30 Self {
31 app,
32 keypad: Keypad::new(),
33 }
34 }
35
36 pub fn render(&self, area: Rect, buf: &mut TextBuffer) {
38 buf.write_str(area.x + 1, area.y, DEMO_TITLE);
40
41 let margin = 1u16;
43 let keypad_w = 22u16;
44 let help_w = 22u16;
45 let main_w = area.width.saturating_sub(margin * 2 + keypad_w + help_w);
46
47 let content_x = area.x + margin;
48 let content_y = area.y + margin;
49
50 let input_h = 3u16;
52 let result_h = 3u16;
53 let jidoka_h = 5u16;
54 let history_h = area
55 .height
56 .saturating_sub(margin * 2 + input_h + result_h + jidoka_h);
57
58 self.render_input(Rect::new(content_x, content_y, main_w, input_h), buf);
59 self.render_result(
60 Rect::new(content_x, content_y + input_h, main_w, result_h),
61 buf,
62 );
63 self.render_history(
64 Rect::new(content_x, content_y + input_h + result_h, main_w, history_h),
65 buf,
66 );
67 self.render_jidoka_status(
68 Rect::new(
69 content_x,
70 content_y + input_h + result_h + history_h,
71 main_w,
72 jidoka_h,
73 ),
74 buf,
75 );
76
77 let keypad_x = content_x + main_w;
79 self.render_keypad(
80 Rect::new(
81 keypad_x,
82 content_y,
83 keypad_w,
84 area.height.saturating_sub(margin * 2),
85 ),
86 buf,
87 );
88
89 let help_x = keypad_x + keypad_w;
91 self.render_help_sidebar(
92 Rect::new(
93 help_x,
94 content_y,
95 help_w,
96 area.height.saturating_sub(margin * 2),
97 ),
98 buf,
99 );
100 }
101
102 fn render_keypad(&self, area: Rect, buf: &mut TextBuffer) {
104 let widget = KeypadWidget::new(&self.keypad);
105 widget.render(area, buf);
106 }
107
108 fn render_help_sidebar(&self, area: Rect, buf: &mut TextBuffer) {
110 buf.write_str(area.x + 1, area.y, " Help ");
111
112 let mut y = area.y + 1;
113 for (key, desc) in HELP_SHORTCUTS {
114 let line = format!("{:>7} {}", key, desc);
115 buf.write_str(area.x + 1, y, &line);
116 y += 1;
117 }
118
119 buf.write_str(area.x + 1, y + 1, HELP_OPERATORS);
121
122 buf.write_str(area.x + 1, y + 3, PROBAR_BADGE);
124 }
125
126 fn render_input(&self, area: Rect, buf: &mut TextBuffer) {
128 buf.write_str(area.x + 1, area.y, " Expression ");
129 let input_text = self.app.input();
130 buf.write_str(area.x + 1, area.y + 1, input_text);
131 }
132
133 fn render_result(&self, area: Rect, buf: &mut TextBuffer) {
135 buf.write_str(area.x + 1, area.y, " Result ");
136 let result_text = self.app.result_display();
137 buf.write_str(area.x + 1, area.y + 1, &result_text);
138 }
139
140 fn render_history(&self, area: Rect, buf: &mut TextBuffer) {
142 buf.write_str(area.x + 1, area.y, " History (newest first) ");
143 let history = self.app.history();
144
145 let mut y = area.y + 1;
146 for entry in history.iter_rev().take(10) {
147 let line = format!("{} = {}", entry.expression, entry.result);
148 buf.write_str(area.x + 1, y, &line);
149 y += 1;
150 }
151 }
152
153 fn render_jidoka_status(&self, area: Rect, buf: &mut TextBuffer) {
155 buf.write_str(area.x + 1, area.y, " Anomaly Status ");
156 let status_lines = self.app.jidoka_status();
157
158 let mut y = area.y + 1;
159 for line in &status_lines {
160 buf.write_str(area.x + 1, y, line);
161 y += 1;
162 }
163 }
164}
165
166pub const DEMO_TITLE: &str = " Showcase Calculator - 100% Test Coverage Demo ";
168
169pub const HELP_SHORTCUTS: &[(&str, &str)] = &[
171 ("Enter", "Evaluate"),
172 ("Esc", "Clear"),
173 ("Up", "Recall"),
174 ("Left/Right", "Move cursor"),
175 ("Ctrl+C", "Quit"),
176 ("Ctrl+L", "Clear all"),
177 ("?", "Toggle help"),
178];
179
180pub const HELP_OPERATORS: &str = "Ops: + - * / % ^ ( )";
182
183pub const PROBAR_BADGE: &str = "Probar - paiml.com/probar";
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
193 fn test_calculator_ui_new() {
194 let app = CalculatorApp::new();
195 let ui = CalculatorUI::new(&app);
196 let _ = format!("{:p}", ui.app);
198 }
199
200 #[test]
201 fn test_calculator_ui_render() {
202 let app = CalculatorApp::new();
203 let buf = render_to_buffer(&app, 80, 24);
204 let _ = buf.to_string_content();
206 }
207
208 #[test]
209 fn test_render_with_input() {
210 let mut app = CalculatorApp::new();
211 app.set_input("2 + 3");
212 let buf = render_to_buffer(&app, 80, 24);
213
214 let content = buf.to_string_content();
215 assert!(content.contains("2 + 3"));
216 }
217
218 #[test]
219 fn test_render_with_result() {
220 let mut app = CalculatorApp::new();
221 app.set_input("2 + 3");
222 app.evaluate();
223 let buf = render_to_buffer(&app, 80, 24);
224
225 let content = buf.to_string_content();
226 assert!(content.contains('5'));
227 }
228
229 #[test]
230 fn test_render_with_error() {
231 let mut app = CalculatorApp::new();
232 app.set_input("1 / 0");
233 app.evaluate();
234 let buf = render_to_buffer(&app, 80, 24);
235
236 let content = buf.to_string_content();
237 assert!(content.contains("Error"));
238 }
239
240 #[test]
241 fn test_render_with_history() {
242 let mut app = CalculatorApp::new();
243 app.set_input("1 + 1");
244 app.evaluate();
245 app.set_input("2 + 2");
246 app.evaluate();
247 let buf = render_to_buffer(&app, 80, 24);
248
249 let content = buf.to_string_content();
250 assert!(content.contains("1 + 1"));
252 }
253
254 #[test]
255 fn test_render_jidoka_success() {
256 let mut app = CalculatorApp::new();
257 app.set_input("5 + 5");
258 app.evaluate();
259 let buf = render_to_buffer(&app, 80, 24);
260
261 let content = buf.to_string_content();
262 assert!(
264 content.contains('\u{2713}') || content.contains("Anomaly"),
265 "Should contain Anomaly status section"
266 );
267 }
268
269 #[test]
270 fn test_render_cursor_position() {
271 let mut app = CalculatorApp::new();
272 app.set_input("12345");
273 app.set_cursor(2); let buf = render_to_buffer(&app, 80, 24);
275
276 let _ = buf.to_string_content();
278 }
279
280 #[test]
281 fn test_render_cursor_at_end() {
282 let mut app = CalculatorApp::new();
283 app.set_input("abc");
284 let buf = render_to_buffer(&app, 80, 24);
286 let _ = buf.to_string_content();
287 }
288
289 #[test]
290 fn test_render_empty_input() {
291 let app = CalculatorApp::new();
292 let buf = render_to_buffer(&app, 80, 24);
293
294 let content = buf.to_string_content();
295 assert!(content.contains("Expression"));
296 }
297
298 #[test]
299 fn test_render_small_terminal() {
300 let app = CalculatorApp::new();
301 let buf = render_to_buffer(&app, 20, 10);
302 let _ = buf.to_string_content();
303 }
304
305 #[test]
308 fn test_demo_title_constant() {
309 assert!(DEMO_TITLE.contains("Showcase Calculator"));
310 assert!(DEMO_TITLE.contains("100% Test Coverage"));
311 assert!(DEMO_TITLE.contains("Demo"));
312 }
313
314 #[test]
315 fn test_help_shortcuts_contains_essential_keys() {
316 let keys: Vec<&str> = HELP_SHORTCUTS.iter().map(|(k, _)| *k).collect();
317 assert!(keys.contains(&"Enter"));
318 assert!(keys.contains(&"Esc"));
319 assert!(keys.contains(&"Ctrl+C"));
320 }
321
322 #[test]
323 fn test_help_shortcuts_has_descriptions() {
324 for (key, desc) in HELP_SHORTCUTS {
325 assert!(!key.is_empty(), "Key should not be empty");
326 assert!(!desc.is_empty(), "Description should not be empty");
327 }
328 }
329
330 #[test]
331 fn test_help_shortcuts_count() {
332 assert!(
333 HELP_SHORTCUTS.len() >= 5,
334 "Should have at least 5 shortcuts"
335 );
336 }
337
338 #[test]
339 fn test_help_operators_contains_all_ops() {
340 assert!(HELP_OPERATORS.contains('+'));
341 assert!(HELP_OPERATORS.contains('-'));
342 assert!(HELP_OPERATORS.contains('*'));
343 assert!(HELP_OPERATORS.contains('/'));
344 assert!(HELP_OPERATORS.contains('%'));
345 assert!(HELP_OPERATORS.contains('^'));
346 assert!(HELP_OPERATORS.contains('('));
347 assert!(HELP_OPERATORS.contains(')'));
348 }
349
350 #[test]
351 fn test_probar_badge_contains_branding() {
352 assert!(PROBAR_BADGE.contains("Probar"));
353 assert!(PROBAR_BADGE.contains("paiml"));
354 }
355
356 #[test]
357 fn test_render_shows_demo_title() {
358 let app = CalculatorApp::new();
359 let buf = render_to_buffer(&app, 80, 24);
360
361 let content = buf.to_string_content();
362 assert!(content.contains("Showcase") || content.contains("Calculator"));
363 }
364
365 #[test]
366 fn test_render_shows_help_panel() {
367 let app = CalculatorApp::new();
368 let buf = render_to_buffer(&app, 100, 30);
369
370 let content = buf.to_string_content();
371 assert!(content.contains("Enter") || content.contains("Help") || content.contains("Esc"));
372 }
373
374 #[test]
375 fn test_render_shows_probar_badge() {
376 let app = CalculatorApp::new();
377 let buf = render_to_buffer(&app, 100, 30);
378
379 let content = buf.to_string_content();
380 assert!(content.contains("Probar") || content.contains("paiml"));
381 }
382
383 #[test]
384 fn test_render_help_sidebar_directly() {
385 let app = CalculatorApp::new();
386 let ui = CalculatorUI::new(&app);
387 let area = Rect::new(0, 0, 22, 20);
388 let mut buf = TextBuffer::empty(Rect::new(0, 0, 80, 24));
389
390 ui.render_help_sidebar(area, &mut buf);
391
392 let content = buf.to_string_content();
393 assert!(content.contains("Help"));
394 assert!(content.contains("Enter"));
395 assert!(content.contains("Esc"));
396 }
397
398 #[test]
401 fn test_widget_render_direct() {
402 let app = CalculatorApp::new();
403 let ui = CalculatorUI::new(&app);
404 let area = Rect::new(0, 0, 80, 24);
405 let mut buf = TextBuffer::empty(area);
406
407 ui.render(area, &mut buf);
408
409 let content = buf.to_string_content();
411 assert!(content.contains("Calculator"));
412 }
413
414 #[test]
415 fn test_render_sections_individually() {
416 let app = CalculatorApp::new();
417 let ui = CalculatorUI::new(&app);
418
419 let mut buf = TextBuffer::empty(Rect::new(0, 0, 80, 24));
420
421 let input_area = Rect::new(0, 0, 40, 3);
422 ui.render_input(input_area, &mut buf);
423
424 let result_area = Rect::new(0, 3, 40, 3);
425 ui.render_result(result_area, &mut buf);
426
427 let history_area = Rect::new(0, 6, 40, 10);
428 ui.render_history(history_area, &mut buf);
429
430 let jidoka_area = Rect::new(0, 16, 40, 5);
431 ui.render_jidoka_status(jidoka_area, &mut buf);
432 }
433
434 #[test]
435 fn test_render_history_many_entries() {
436 let mut app = CalculatorApp::new();
437 for i in 1..=20 {
438 app.set_input(&format!("{i} + {i}"));
439 app.evaluate();
440 }
441 let buf = render_to_buffer(&app, 80, 24);
442
443 let content = buf.to_string_content();
444 assert!(content.contains("20 + 20")); }
446
447 #[test]
450 fn test_calculator_ui_has_keypad() {
451 let app = CalculatorApp::new();
452 let ui = CalculatorUI::new(&app);
453 assert_eq!(ui.keypad.button_count(), 20);
455 }
456
457 #[test]
458 fn test_render_keypad_directly() {
459 let app = CalculatorApp::new();
460 let ui = CalculatorUI::new(&app);
461 let area = Rect::new(0, 0, 22, 12);
462 let mut buf = TextBuffer::empty(Rect::new(0, 0, 80, 24));
463
464 ui.render_keypad(area, &mut buf);
465
466 let content = buf.to_string_content();
467 assert!(content.contains("Keypad"));
468 assert!(content.contains("[7]"));
469 }
470
471 #[test]
472 fn test_render_shows_keypad_in_full_layout() {
473 let app = CalculatorApp::new();
474 let buf = render_to_buffer(&app, 120, 30);
475
476 let content = buf.to_string_content();
477 assert!(content.contains("Keypad"));
479 assert!(content.contains("[7]") || content.contains("[+]") || content.contains("[=]"));
481 }
482
483 #[test]
484 fn test_keypad_shows_operators() {
485 let app = CalculatorApp::new();
486 let buf = render_to_buffer(&app, 120, 30);
487
488 let content = buf.to_string_content();
489 assert!(content.contains("[/]") || content.contains("[*]") || content.contains("[-]"));
491 }
492
493 #[test]
494 fn test_full_layout_three_columns() {
495 let app = CalculatorApp::new();
496 let ui = CalculatorUI::new(&app);
497 let area = Rect::new(0, 0, 120, 30);
498 let mut buf = TextBuffer::empty(area);
499
500 ui.render(area, &mut buf);
501
502 let content = buf.to_string_content();
503 assert!(content.contains("Expression")); assert!(content.contains("Keypad")); assert!(content.contains("Help")); }
508}