1use super::view::{write_line_to_terminal, View};
6use crate::core::command::CommandId;
7use crate::core::draw::DrawBuffer;
8use crate::core::event::{Event, EventType, KB_ENTER, MB_LEFT_BUTTON};
9use crate::core::geometry::Rect;
10use crate::core::palette::{
11 BUTTON_DEFAULT, BUTTON_DISABLED, BUTTON_NORMAL, BUTTON_SELECTED, BUTTON_SHADOW, BUTTON_SHORTCUT,
12};
13use crate::core::state::{StateFlags, SF_DISABLED, SHADOW_BOTTOM, SHADOW_SOLID, SHADOW_TOP};
14use crate::terminal::Terminal;
15
16pub struct Button {
17 bounds: Rect,
18 title: String,
19 command: CommandId,
20 is_default: bool,
21 is_broadcast: bool,
22 state: StateFlags,
23 options: u16,
24 palette_chain: Option<crate::core::palette_chain::PaletteChainNode>,
25}
26
27impl Button {
28 pub fn new(bounds: Rect, title: &str, command: CommandId, is_default: bool) -> Self {
29 use crate::core::command_set;
30 use crate::core::state::OF_POST_PROCESS;
31
32 let mut state = 0;
35 if !command_set::command_enabled(command) {
36 state |= SF_DISABLED;
37 }
38
39 Self {
40 bounds,
41 title: title.to_string(),
42 command,
43 is_default,
44 is_broadcast: false,
45 state,
46 options: OF_POST_PROCESS, palette_chain: None,
48 }
49 }
50
51 pub fn set_disabled(&mut self, disabled: bool) {
52 self.set_state_flag(SF_DISABLED, disabled);
53 }
54
55 pub fn is_disabled(&self) -> bool {
56 self.get_state_flag(SF_DISABLED)
57 }
58
59 pub fn set_broadcast(&mut self, broadcast: bool) {
62 self.is_broadcast = broadcast;
63 }
64
65 pub fn set_selectable(&mut self, selectable: bool) {
68 use crate::core::state::OF_SELECTABLE;
69 if selectable {
70 self.options |= OF_SELECTABLE;
71 } else {
72 self.options &= !OF_SELECTABLE;
73 }
74 }
75
76 fn get_hotkey(&self) -> Option<char> {
79 let mut chars = self.title.chars();
80 while let Some(ch) = chars.next() {
81 if ch == '~' {
82 if let Some(hotkey) = chars.next() {
84 return Some(hotkey.to_uppercase().next().unwrap_or(hotkey));
85 }
86 }
87 }
88 None
89 }
90}
91
92impl View for Button {
93 fn bounds(&self) -> Rect {
94 self.bounds
95 }
96
97 fn set_bounds(&mut self, bounds: Rect) {
98 self.bounds = bounds;
99 }
100
101 fn draw(&mut self, terminal: &mut Terminal) {
102 let width = self.bounds.width_clamped() as usize;
103 let height = self.bounds.height_clamped() as usize;
104
105 if width < 4 || height < 2 {
109 return;
110 }
111
112 let is_disabled = self.is_disabled();
113 let is_focused = self.is_focused();
114
115 let button_attr = if is_disabled {
123 self.map_color(BUTTON_DISABLED) } else if is_focused {
125 self.map_color(BUTTON_SELECTED) } else if self.is_default {
127 self.map_color(BUTTON_DEFAULT) } else {
129 self.map_color(BUTTON_NORMAL) };
131
132 let mut shadow_attr = self.map_color(BUTTON_SHADOW);
135
136 if shadow_attr.to_u8() == 0xCF { use crate::core::palette::Attr;
140 shadow_attr = Attr::from_u8(0x07);
142 }
143
144 let shadow_attr = shadow_attr.swap();
145
146 let shortcut_attr = if is_disabled {
148 self.map_color(BUTTON_DISABLED) } else {
150 self.map_color(BUTTON_SHORTCUT) };
152
153 for y in 0..(height - 1) {
155 let mut buf = DrawBuffer::new(width);
156
157 buf.move_char(0, ' ', button_attr, width);
159
160 let shadow_char = if y == 0 { SHADOW_TOP } else { SHADOW_SOLID };
162 buf.put_char(width - 1, shadow_char, shadow_attr);
163
164 if y == (height - 1) / 2 {
166 let display_len = self.title.chars().filter(|&c| c != '~').count();
168 let content_width = width - 1; let start = (content_width.saturating_sub(display_len)) / 2;
170 buf.move_str_with_shortcut(start, &self.title, button_attr, shortcut_attr);
171 }
172
173 write_line_to_terminal(terminal, self.bounds.a.x, self.bounds.a.y + y as i16, &buf);
174 }
175
176 let mut bottom_buf = DrawBuffer::new(width - 1);
178 bottom_buf.move_char(0, SHADOW_BOTTOM, shadow_attr, width - 1);
180 write_line_to_terminal(
181 terminal,
182 self.bounds.a.x + 1,
183 self.bounds.a.y + (height - 1) as i16,
184 &bottom_buf,
185 );
186 }
187
188 fn handle_event(&mut self, event: &mut Event) {
189 if event.what == EventType::Broadcast {
200 use crate::core::command::CM_COMMAND_SET_CHANGED;
201 use crate::core::command_set;
202
203 if event.command == CM_COMMAND_SET_CHANGED {
204 let should_be_enabled = command_set::command_enabled(self.command);
206 let is_currently_disabled = self.is_disabled();
207
208 if should_be_enabled && is_currently_disabled {
211 self.set_disabled(false);
213 } else if !should_be_enabled && !is_currently_disabled {
214 self.set_disabled(true);
216 }
217
218 }
221 return; }
223
224 if self.is_disabled() {
228 return;
229 }
230
231 match event.what {
232 EventType::Keyboard => {
233 if let Some(hotkey) = self.get_hotkey() {
236 let key_char = (event.key_code & 0xFF) as u8 as char;
238 let key_char_upper = key_char.to_uppercase().next().unwrap_or(key_char);
239
240 if key_char_upper == hotkey {
241 if self.is_broadcast {
243 *event = Event::broadcast(self.command);
244 } else {
245 *event = Event::command(self.command);
246 }
247 return;
248 }
249 }
250
251 if !self.is_focused() {
253 return;
254 }
255 if event.key_code == KB_ENTER || event.key_code == ' ' as u16 {
256 if self.is_broadcast {
257 *event = Event::broadcast(self.command);
258 } else {
259 *event = Event::command(self.command);
260 }
261 }
262 }
263 EventType::MouseDown => {
264 let mouse_pos = event.mouse.pos;
266 if event.mouse.buttons & MB_LEFT_BUTTON != 0
267 && mouse_pos.x >= self.bounds.a.x
268 && mouse_pos.x < self.bounds.b.x
269 && mouse_pos.y >= self.bounds.a.y
270 && mouse_pos.y < self.bounds.b.y - 1
271 {
273 if self.is_broadcast {
275 *event = Event::broadcast(self.command);
276 } else {
277 *event = Event::command(self.command);
278 }
279 }
280 }
281 _ => {}
282 }
283 }
284
285 fn can_focus(&self) -> bool {
286 !self.is_disabled()
287 }
288
289 fn state(&self) -> StateFlags {
293 self.state
294 }
295
296 fn set_state(&mut self, state: StateFlags) {
297 self.state = state;
298 }
299
300 fn options(&self) -> u16 {
301 self.options
302 }
303
304 fn set_options(&mut self, options: u16) {
305 self.options = options;
306 }
307
308 fn is_default_button(&self) -> bool {
309 self.is_default
310 }
311
312 fn button_command(&self) -> Option<u16> {
313 Some(self.command)
314 }
315
316 fn set_palette_chain(&mut self, node: Option<crate::core::palette_chain::PaletteChainNode>) {
317 self.palette_chain = node;
318 }
319
320 fn get_palette_chain(&self) -> Option<&crate::core::palette_chain::PaletteChainNode> {
321 self.palette_chain.as_ref()
322 }
323
324 fn get_palette(&self) -> Option<crate::core::palette::Palette> {
325 use crate::core::palette::{palettes, Palette};
326 Some(Palette::from_slice(palettes::CP_BUTTON))
327 }
328}
329
330pub struct ButtonBuilder {
347 bounds: Option<Rect>,
348 title: Option<String>,
349 command: Option<CommandId>,
350 is_default: bool,
351}
352
353impl ButtonBuilder {
354 pub fn new() -> Self {
356 Self {
357 bounds: None,
358 title: None,
359 command: None,
360 is_default: false,
361 }
362 }
363
364 #[must_use]
366 pub fn bounds(mut self, bounds: Rect) -> Self {
367 self.bounds = Some(bounds);
368 self
369 }
370
371 #[must_use]
373 pub fn title(mut self, title: impl Into<String>) -> Self {
374 self.title = Some(title.into());
375 self
376 }
377
378 #[must_use]
380 pub fn command(mut self, command: CommandId) -> Self {
381 self.command = Some(command);
382 self
383 }
384
385 #[must_use]
390 pub fn default(mut self, is_default: bool) -> Self {
391 self.is_default = is_default;
392 self
393 }
394
395 pub fn build(self) -> Button {
401 let bounds = self.bounds.expect("Button bounds must be set");
402 let title = self.title.expect("Button title must be set");
403 let command = self.command.expect("Button command must be set");
404
405 Button::new(bounds, &title, command, self.is_default)
406 }
407}
408
409impl Default for ButtonBuilder {
410 fn default() -> Self {
411 Self::new()
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418 use crate::core::command::CM_COMMAND_SET_CHANGED;
419 use crate::core::command_set;
420 use crate::core::geometry::Point;
421
422 #[test]
423 fn test_button_creation_with_disabled_command() {
424 const TEST_CMD: u16 = 500;
426 command_set::disable_command(TEST_CMD);
427
428 let button = Button::new(Rect::new(0, 0, 10, 2), "Test", TEST_CMD, false);
429
430 assert!(
431 button.is_disabled(),
432 "Button should start disabled when command is disabled"
433 );
434 }
435
436 #[test]
437 fn test_button_creation_with_enabled_command() {
438 const TEST_CMD: u16 = 501;
440 command_set::enable_command(TEST_CMD);
441
442 let button = Button::new(Rect::new(0, 0, 10, 2), "Test", TEST_CMD, false);
443
444 assert!(
445 !button.is_disabled(),
446 "Button should start enabled when command is enabled"
447 );
448 }
449
450 #[test]
451 fn test_disabled_button_receives_broadcast_and_becomes_enabled() {
452 const TEST_CMD: u16 = 502;
457
458 command_set::disable_command(TEST_CMD);
460
461 let mut button = Button::new(Rect::new(0, 0, 10, 2), "Test", TEST_CMD, false);
462
463 assert!(button.is_disabled(), "Button should start disabled");
465
466 command_set::enable_command(TEST_CMD);
468
469 let mut event = Event::broadcast(CM_COMMAND_SET_CHANGED);
471 button.handle_event(&mut event);
472
473 assert!(
475 !button.is_disabled(),
476 "Button should be enabled after receiving broadcast"
477 );
478 }
479
480 #[test]
481 fn test_enabled_button_receives_broadcast_and_becomes_disabled() {
482 const TEST_CMD: u16 = 503;
485
486 command_set::enable_command(TEST_CMD);
488
489 let mut button = Button::new(Rect::new(0, 0, 10, 2), "Test", TEST_CMD, false);
490
491 assert!(!button.is_disabled(), "Button should start enabled");
493
494 command_set::disable_command(TEST_CMD);
496
497 let mut event = Event::broadcast(CM_COMMAND_SET_CHANGED);
499 button.handle_event(&mut event);
500
501 assert!(
503 button.is_disabled(),
504 "Button should be disabled after receiving broadcast"
505 );
506 }
507
508 #[test]
509 fn test_disabled_button_ignores_keyboard_events() {
510 const TEST_CMD: u16 = 504;
513 command_set::disable_command(TEST_CMD);
514
515 let mut button = Button::new(Rect::new(0, 0, 10, 2), "Test", TEST_CMD, false);
516
517 button.set_focus(true);
518
519 let mut event = Event::keyboard(crate::core::event::KB_ENTER);
521 button.handle_event(&mut event);
522
523 assert_ne!(
525 event.what,
526 EventType::Command,
527 "Disabled button should not generate command"
528 );
529 }
530
531 #[test]
532 fn test_disabled_button_ignores_mouse_clicks() {
533 const TEST_CMD: u16 = 505;
536 command_set::disable_command(TEST_CMD);
537
538 let mut button = Button::new(Rect::new(0, 0, 10, 2), "Test", TEST_CMD, false);
539
540 let mut event = Event::mouse(
542 EventType::MouseDown,
543 Point::new(5, 1),
544 crate::core::event::MB_LEFT_BUTTON,
545 false,
546 );
547 button.handle_event(&mut event);
548
549 assert_ne!(
551 event.what,
552 EventType::Command,
553 "Disabled button should not generate command"
554 );
555 }
556
557 #[test]
558 fn test_broadcast_does_not_clear_event() {
559 const TEST_CMD: u16 = 506;
563 command_set::disable_command(TEST_CMD);
564
565 let mut button = Button::new(Rect::new(0, 0, 10, 2), "Test", TEST_CMD, false);
566
567 command_set::enable_command(TEST_CMD);
568
569 let mut event = Event::broadcast(CM_COMMAND_SET_CHANGED);
570 button.handle_event(&mut event);
571
572 assert_eq!(
574 event.what,
575 EventType::Broadcast,
576 "Broadcast should not be cleared"
577 );
578 assert_eq!(
579 event.command, CM_COMMAND_SET_CHANGED,
580 "Broadcast command should remain"
581 );
582 }
583
584 #[test]
585 fn test_button_builder() {
586 const TEST_CMD: u16 = 507;
587 command_set::enable_command(TEST_CMD);
588
589 let button = ButtonBuilder::new()
590 .bounds(Rect::new(5, 10, 15, 12))
591 .title("Test")
592 .command(TEST_CMD)
593 .default(true)
594 .build();
595
596 assert_eq!(button.bounds(), Rect::new(5, 10, 15, 12));
597 assert_eq!(button.is_default_button(), true);
598 assert_eq!(button.button_command(), Some(TEST_CMD));
599 }
600
601 #[test]
602 fn test_button_builder_default_is_false() {
603 const TEST_CMD: u16 = 508;
604 command_set::enable_command(TEST_CMD);
605
606 let button = ButtonBuilder::new()
607 .bounds(Rect::new(0, 0, 10, 2))
608 .title("Test")
609 .command(TEST_CMD)
610 .build();
611
612 assert_eq!(button.is_default_button(), false);
613 }
614
615 #[test]
616 #[should_panic(expected = "Button bounds must be set")]
617 fn test_button_builder_panics_without_bounds() {
618 const TEST_CMD: u16 = 509;
619 ButtonBuilder::new().title("Test").command(TEST_CMD).build();
620 }
621
622 #[test]
623 #[should_panic(expected = "Button title must be set")]
624 fn test_button_builder_panics_without_title() {
625 const TEST_CMD: u16 = 510;
626 ButtonBuilder::new()
627 .bounds(Rect::new(0, 0, 10, 2))
628 .command(TEST_CMD)
629 .build();
630 }
631
632 #[test]
633 #[should_panic(expected = "Button command must be set")]
634 fn test_button_builder_panics_without_command() {
635 ButtonBuilder::new()
636 .bounds(Rect::new(0, 0, 10, 2))
637 .title("Test")
638 .build();
639 }
640
641 #[test]
642 fn test_button_with_small_dimensions_doesnt_panic() {
643 const TEST_CMD: u16 = 511;
650
651 let test_cases = vec![
653 Rect::new(0, 0, 0, 0), Rect::new(0, 0, 1, 1), Rect::new(0, 0, 2, 1), Rect::new(0, 0, 3, 1), Rect::new(0, 0, 4, 1), Rect::new(0, 0, 1, 2), Rect::new(0, 0, 2, 2), Rect::new(0, 0, 3, 2), Rect::new(10, 5, 5, 2), Rect::new(5, 10, 2, 5), ];
664
665 for rect in test_cases {
666 let button = Button::new(rect, "Test", TEST_CMD, false);
668 let bounds = button.bounds();
669
670 assert!(bounds.width_clamped() >= 0);
672 assert!(bounds.height_clamped() >= 0);
673 }
674 }
675}