1use crate::draw_utils::print_with_color_and_background_at;
2use crate::model::choice::Choice;
3use crate::model::common::{Bounds, ScreenBuffer};
4
5#[derive(Debug, Clone, PartialEq)]
7pub enum SelectionStyle {
8 ColorHighlight,
10 InvertColors,
12 BorderHighlight,
14 PointerIndicator,
16 UnderlineHighlight,
18 BoldHighlight,
20 Combined(Vec<SelectionStyle>),
22}
23
24#[derive(Debug, Clone, PartialEq)]
26pub enum FocusStyle {
27 None,
29 Blinking,
31 Intensity,
33 DottedBorder,
35 AnimatedCursor,
37}
38
39#[derive(Debug, Clone, PartialEq)]
41pub enum FeedbackStyle {
42 ColorFlash,
44 ColorTransition,
46 RippleEffect,
48 SlideAnimation,
50 None,
52}
53
54#[derive(Debug, Clone)]
56pub struct SelectionStyleConfig {
57 pub selection_style: SelectionStyle,
58 pub focus_style: FocusStyle,
59 pub feedback_style: FeedbackStyle,
60 pub animation_duration_ms: u32,
61 pub intensity_factor: f32,
62 pub custom_indicators: Option<SelectionIndicators>,
63}
64
65#[derive(Debug, Clone)]
67pub struct SelectionIndicators {
68 pub pointer_symbol: String,
69 pub selection_prefix: String,
70 pub selection_suffix: String,
71 pub focus_indicator: String,
72 pub border_chars: BorderChars,
73}
74
75#[derive(Debug, Clone)]
77pub struct BorderChars {
78 pub top_left: char,
79 pub top_right: char,
80 pub bottom_left: char,
81 pub bottom_right: char,
82 pub horizontal: char,
83 pub vertical: char,
84}
85
86impl Default for SelectionStyleConfig {
87 fn default() -> Self {
88 Self {
89 selection_style: SelectionStyle::ColorHighlight,
90 focus_style: FocusStyle::None,
91 feedback_style: FeedbackStyle::None,
92 animation_duration_ms: 200,
93 intensity_factor: 1.5,
94 custom_indicators: None,
95 }
96 }
97}
98
99impl Default for SelectionIndicators {
100 fn default() -> Self {
101 Self {
102 pointer_symbol: "▶".to_string(),
103 selection_prefix: "".to_string(),
104 selection_suffix: "".to_string(),
105 focus_indicator: "◀".to_string(),
106 border_chars: BorderChars::default(),
107 }
108 }
109}
110
111impl Default for BorderChars {
112 fn default() -> Self {
113 Self {
114 top_left: '┌',
115 top_right: '┐',
116 bottom_left: '└',
117 bottom_right: '┘',
118 horizontal: '─',
119 vertical: '│',
120 }
121 }
122}
123
124pub struct SelectionStyleRenderer {
126 pub id: String,
127 pub config: SelectionStyleConfig,
128}
129
130impl SelectionStyleRenderer {
131 pub fn new(id: String, config: SelectionStyleConfig) -> Self {
133 Self { id, config }
134 }
135
136 pub fn with_defaults(id: String) -> Self {
138 Self {
139 id,
140 config: SelectionStyleConfig::default(),
141 }
142 }
143
144 pub fn render_choice(
146 &self,
147 choice: &Choice,
148 bounds: &Bounds,
149 y_position: usize,
150 x_position: usize,
151 base_fg_color: &Option<String>,
152 base_bg_color: &Option<String>,
153 selected_fg_color: &Option<String>,
154 selected_bg_color: &Option<String>,
155 is_focused: bool,
156 buffer: &mut ScreenBuffer,
157 ) {
158 let (final_fg_color, final_bg_color, display_text) = self.calculate_style_colors_and_text(
159 choice,
160 base_fg_color,
161 base_bg_color,
162 selected_fg_color,
163 selected_bg_color,
164 is_focused,
165 );
166
167 print_with_color_and_background_at(
169 y_position,
170 x_position,
171 &final_fg_color,
172 &final_bg_color,
173 &display_text,
174 buffer,
175 );
176
177 if choice.selected {
179 self.apply_selection_style(
180 choice,
181 bounds,
182 y_position,
183 x_position,
184 &display_text,
185 buffer,
186 );
187 }
188
189 if is_focused {
191 self.apply_focus_style(bounds, y_position, x_position, &display_text, buffer);
192 }
193 }
194
195 pub fn calculate_style_colors_and_text(
197 &self,
198 choice: &Choice,
199 base_fg_color: &Option<String>,
200 base_bg_color: &Option<String>,
201 selected_fg_color: &Option<String>,
202 selected_bg_color: &Option<String>,
203 is_focused: bool,
204 ) -> (Option<String>, Option<String>, String) {
205 let mut fg_color = if choice.selected {
206 selected_fg_color.clone()
207 } else {
208 base_fg_color.clone()
209 };
210
211 let mut bg_color = if choice.selected {
212 selected_bg_color.clone()
213 } else {
214 base_bg_color.clone()
215 };
216
217 let mut display_text = choice.content.as_ref().unwrap_or(&String::new()).clone();
218
219 if choice.selected {
221 match &self.config.selection_style {
222 SelectionStyle::InvertColors => {
223 std::mem::swap(&mut fg_color, &mut bg_color);
225 }
226 SelectionStyle::BoldHighlight => {
227 fg_color = self.make_color_bright(&fg_color);
229 }
230 SelectionStyle::PointerIndicator => {
231 let default_indicators = SelectionIndicators::default();
233 let indicators = self
234 .config
235 .custom_indicators
236 .as_ref()
237 .unwrap_or(&default_indicators);
238 display_text = format!("{} {}", indicators.pointer_symbol, display_text);
239 }
240 SelectionStyle::Combined(styles) => {
241 for style in styles {
243 match style {
244 SelectionStyle::InvertColors => {
245 std::mem::swap(&mut fg_color, &mut bg_color);
246 }
247 SelectionStyle::BoldHighlight => {
248 fg_color = self.make_color_bright(&fg_color);
249 }
250 SelectionStyle::PointerIndicator => {
251 let default_indicators = SelectionIndicators::default();
252 let indicators = self
253 .config
254 .custom_indicators
255 .as_ref()
256 .unwrap_or(&default_indicators);
257 display_text =
258 format!("{} {}", indicators.pointer_symbol, display_text);
259 }
260 _ => {} }
262 }
263 }
264 _ => {} }
266 }
267
268 if is_focused && matches!(self.config.focus_style, FocusStyle::Intensity) {
270 fg_color = self.apply_intensity(&fg_color);
271 }
272
273 if choice.waiting {
275 display_text = format!("{}...", display_text);
276 }
277
278 (fg_color, bg_color, display_text)
279 }
280
281 fn apply_selection_style(
283 &self,
284 _choice: &Choice,
285 bounds: &Bounds,
286 y_position: usize,
287 x_position: usize,
288 display_text: &str,
289 buffer: &mut ScreenBuffer,
290 ) {
291 match &self.config.selection_style {
292 SelectionStyle::BorderHighlight => {
293 self.draw_selection_border(
294 bounds,
295 y_position,
296 x_position,
297 display_text.len(),
298 buffer,
299 );
300 }
301 SelectionStyle::UnderlineHighlight => {
302 self.draw_selection_underline(
303 bounds,
304 y_position,
305 x_position,
306 display_text.len(),
307 buffer,
308 );
309 }
310 SelectionStyle::Combined(styles) => {
311 if styles.contains(&SelectionStyle::BorderHighlight) {
312 self.draw_selection_border(
313 bounds,
314 y_position,
315 x_position,
316 display_text.len(),
317 buffer,
318 );
319 }
320 if styles.contains(&SelectionStyle::UnderlineHighlight) {
321 self.draw_selection_underline(
322 bounds,
323 y_position,
324 x_position,
325 display_text.len(),
326 buffer,
327 );
328 }
329 }
330 _ => {}
331 }
332 }
333
334 fn apply_focus_style(
336 &self,
337 bounds: &Bounds,
338 y_position: usize,
339 x_position: usize,
340 display_text: &str,
341 buffer: &mut ScreenBuffer,
342 ) {
343 match &self.config.focus_style {
344 FocusStyle::DottedBorder => {
345 self.draw_dotted_focus_border(
346 bounds,
347 y_position,
348 x_position,
349 display_text.len(),
350 buffer,
351 );
352 }
353 FocusStyle::AnimatedCursor => {
354 self.draw_animated_cursor(bounds, y_position, x_position, buffer);
355 }
356 FocusStyle::Blinking => {
357 self.draw_focus_indicator(bounds, y_position, x_position, buffer);
359 }
360 _ => {}
361 }
362 }
363
364 fn draw_selection_border(
366 &self,
367 bounds: &Bounds,
368 y_position: usize,
369 x_position: usize,
370 text_length: usize,
371 buffer: &mut ScreenBuffer,
372 ) {
373 let default_indicators = SelectionIndicators::default();
374 let indicators = self
375 .config
376 .custom_indicators
377 .as_ref()
378 .unwrap_or(&default_indicators);
379 let border = &indicators.border_chars;
380
381 if y_position > bounds.top() && y_position < bounds.bottom() && x_position > bounds.left() {
383 if x_position > 0 {
385 print_with_color_and_background_at(
386 y_position,
387 x_position - 1,
388 &Some("bright_yellow".to_string()),
389 &Some("black".to_string()),
390 &border.vertical.to_string(),
391 buffer,
392 );
393 }
394
395 if x_position + text_length < bounds.right() {
396 print_with_color_and_background_at(
397 y_position,
398 x_position + text_length,
399 &Some("bright_yellow".to_string()),
400 &Some("black".to_string()),
401 &border.vertical.to_string(),
402 buffer,
403 );
404 }
405 }
406 }
407
408 fn draw_selection_underline(
410 &self,
411 bounds: &Bounds,
412 y_position: usize,
413 x_position: usize,
414 text_length: usize,
415 buffer: &mut ScreenBuffer,
416 ) {
417 if y_position < bounds.bottom() {
418 let underline = "─".repeat(text_length);
419 print_with_color_and_background_at(
420 y_position + 1,
421 x_position,
422 &Some("bright_yellow".to_string()),
423 &Some("black".to_string()),
424 &underline,
425 buffer,
426 );
427 }
428 }
429
430 fn draw_dotted_focus_border(
432 &self,
433 bounds: &Bounds,
434 y_position: usize,
435 x_position: usize,
436 text_length: usize,
437 buffer: &mut ScreenBuffer,
438 ) {
439 if y_position >= bounds.top()
440 && y_position <= bounds.bottom()
441 && x_position >= bounds.left()
442 {
443 if x_position > 0 {
445 print_with_color_and_background_at(
446 y_position,
447 x_position - 1,
448 &Some("bright_cyan".to_string()),
449 &Some("black".to_string()),
450 ":",
451 buffer,
452 );
453 }
454
455 if x_position + text_length < bounds.right() {
456 print_with_color_and_background_at(
457 y_position,
458 x_position + text_length,
459 &Some("bright_cyan".to_string()),
460 &Some("black".to_string()),
461 ":",
462 buffer,
463 );
464 }
465 }
466 }
467
468 fn draw_animated_cursor(
470 &self,
471 bounds: &Bounds,
472 y_position: usize,
473 x_position: usize,
474 buffer: &mut ScreenBuffer,
475 ) {
476 if y_position >= bounds.top() && y_position <= bounds.bottom() && x_position > bounds.left()
477 {
478 let default_indicators = SelectionIndicators::default();
479 let indicators = self
480 .config
481 .custom_indicators
482 .as_ref()
483 .unwrap_or(&default_indicators);
484 print_with_color_and_background_at(
485 y_position,
486 x_position - 1,
487 &Some("bright_magenta".to_string()),
488 &Some("black".to_string()),
489 &indicators.focus_indicator,
490 buffer,
491 );
492 }
493 }
494
495 fn draw_focus_indicator(
497 &self,
498 bounds: &Bounds,
499 y_position: usize,
500 x_position: usize,
501 buffer: &mut ScreenBuffer,
502 ) {
503 if y_position >= bounds.top() && y_position <= bounds.bottom() && x_position > bounds.left()
504 {
505 print_with_color_and_background_at(
506 y_position,
507 x_position - 1,
508 &Some("bright_white".to_string()),
509 &Some("black".to_string()),
510 "●",
511 buffer,
512 );
513 }
514 }
515
516 fn make_color_bright(&self, color: &Option<String>) -> Option<String> {
518 match color {
519 Some(color_str) => match color_str.as_str() {
520 "red" => Some("bright_red".to_string()),
521 "green" => Some("bright_green".to_string()),
522 "blue" => Some("bright_blue".to_string()),
523 "yellow" => Some("bright_yellow".to_string()),
524 "magenta" => Some("bright_magenta".to_string()),
525 "cyan" => Some("bright_cyan".to_string()),
526 "white" => Some("bright_white".to_string()),
527 "black" => Some("bright_black".to_string()),
528 _ => Some(color_str.clone()), },
530 None => None, }
532 }
533
534 fn apply_intensity(&self, color: &Option<String>) -> Option<String> {
536 self.make_color_bright(color)
538 }
539
540 pub fn get_legacy_style_config() -> SelectionStyleConfig {
542 SelectionStyleConfig {
543 selection_style: SelectionStyle::ColorHighlight,
544 focus_style: FocusStyle::None,
545 feedback_style: FeedbackStyle::None,
546 animation_duration_ms: 0,
547 intensity_factor: 1.0,
548 custom_indicators: None,
549 }
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556 use crate::model::common::ScreenBuffer;
557
558 #[test]
559 fn test_selection_style_renderer_creation() {
560 let renderer = SelectionStyleRenderer::with_defaults("test".to_string());
561 assert_eq!(renderer.id, "test");
562 assert_eq!(
563 renderer.config.selection_style,
564 SelectionStyle::ColorHighlight
565 );
566 }
567
568 #[test]
569 fn test_custom_selection_style_config() {
570 let config = SelectionStyleConfig {
571 selection_style: SelectionStyle::PointerIndicator,
572 focus_style: FocusStyle::Intensity,
573 feedback_style: FeedbackStyle::ColorFlash,
574 animation_duration_ms: 300,
575 intensity_factor: 2.0,
576 custom_indicators: Some(SelectionIndicators::default()),
577 };
578
579 let renderer = SelectionStyleRenderer::new("custom".to_string(), config);
580 assert!(matches!(
581 renderer.config.selection_style,
582 SelectionStyle::PointerIndicator
583 ));
584 assert!(matches!(renderer.config.focus_style, FocusStyle::Intensity));
585 }
586
587 #[test]
588 fn test_color_bright_conversion() {
589 let renderer = SelectionStyleRenderer::with_defaults("test".to_string());
590 assert_eq!(
591 renderer.make_color_bright(&Some("red".to_string())),
592 Some("bright_red".to_string())
593 );
594 assert_eq!(
595 renderer.make_color_bright(&Some("green".to_string())),
596 Some("bright_green".to_string())
597 );
598 assert_eq!(
599 renderer.make_color_bright(&Some("bright_blue".to_string())),
600 Some("bright_blue".to_string())
601 ); assert_eq!(renderer.make_color_bright(&None), None); }
604
605 #[test]
606 fn test_selection_indicators_default() {
607 let indicators = SelectionIndicators::default();
608 assert_eq!(indicators.pointer_symbol, "▶");
609 assert_eq!(indicators.focus_indicator, "◀");
610 assert_eq!(indicators.border_chars.top_left, '┌');
611 }
612
613 #[test]
614 fn test_combined_selection_styles() {
615 let combined = SelectionStyle::Combined(vec![
616 SelectionStyle::PointerIndicator,
617 SelectionStyle::BoldHighlight,
618 SelectionStyle::BorderHighlight,
619 ]);
620
621 if let SelectionStyle::Combined(styles) = combined {
622 assert_eq!(styles.len(), 3);
623 assert!(styles.contains(&SelectionStyle::PointerIndicator));
624 assert!(styles.contains(&SelectionStyle::BoldHighlight));
625 assert!(styles.contains(&SelectionStyle::BorderHighlight));
626 } else {
627 panic!("Expected Combined selection style");
628 }
629 }
630
631 #[test]
632 fn test_calculate_style_colors_and_text_basic() {
633 let renderer = SelectionStyleRenderer::with_defaults("test".to_string());
634 let mut choice = Choice {
635 id: "test_choice".to_string(),
636 content: Some("Test Choice".to_string()),
637 selected: true,
638 hovered: false,
639 waiting: false,
640 script: None,
641 execution_mode: crate::model::common::ExecutionMode::Immediate,
642 redirect_output: None,
643 append_output: None,
644 };
645
646 let (fg, bg, text) = renderer.calculate_style_colors_and_text(
647 &choice,
648 &Some("white".to_string()),
649 &Some("black".to_string()),
650 &Some("bright_white".to_string()),
651 &Some("blue".to_string()),
652 false,
653 );
654
655 assert_eq!(fg, Some("bright_white".to_string()));
656 assert_eq!(bg, Some("blue".to_string()));
657 assert_eq!(text, "Test Choice");
658 }
659
660 #[test]
661 fn test_calculate_style_colors_and_text_with_pointer() {
662 let config = SelectionStyleConfig {
663 selection_style: SelectionStyle::PointerIndicator,
664 focus_style: FocusStyle::None,
665 feedback_style: FeedbackStyle::None,
666 animation_duration_ms: 0,
667 intensity_factor: 1.0,
668 custom_indicators: Some(SelectionIndicators::default()),
669 };
670
671 let renderer = SelectionStyleRenderer::new("test".to_string(), config);
672 let choice = Choice {
673 id: "menu_item".to_string(),
674 content: Some("Menu Item".to_string()),
675 selected: true,
676 hovered: false,
677 waiting: false,
678 script: None,
679 execution_mode: crate::model::common::ExecutionMode::Immediate,
680 redirect_output: None,
681 append_output: None,
682 };
683
684 let (fg, bg, text) = renderer.calculate_style_colors_and_text(
685 &choice,
686 &Some("white".to_string()),
687 &Some("black".to_string()),
688 &Some("bright_white".to_string()),
689 &Some("blue".to_string()),
690 false,
691 );
692
693 assert_eq!(fg, Some("bright_white".to_string()));
694 assert_eq!(bg, Some("blue".to_string()));
695 assert_eq!(text, "▶ Menu Item");
696 }
697
698 #[test]
699 fn test_inverted_colors() {
700 let config = SelectionStyleConfig {
701 selection_style: SelectionStyle::InvertColors,
702 ..SelectionStyleConfig::default()
703 };
704
705 let renderer = SelectionStyleRenderer::new("test".to_string(), config);
706 let choice = Choice {
707 id: "inverted".to_string(),
708 content: Some("Inverted".to_string()),
709 selected: true,
710 hovered: false,
711 waiting: false,
712 script: None,
713 execution_mode: crate::model::common::ExecutionMode::Immediate,
714 redirect_output: None,
715 append_output: None,
716 };
717
718 let (fg, bg, text) = renderer.calculate_style_colors_and_text(
719 &choice,
720 &Some("white".to_string()),
721 &Some("black".to_string()),
722 &Some("bright_white".to_string()),
723 &Some("blue".to_string()),
724 false,
725 );
726
727 assert_eq!(fg, Some("blue".to_string()));
729 assert_eq!(bg, Some("bright_white".to_string()));
730 assert_eq!(text, "Inverted");
731 }
732
733 #[test]
734 fn test_waiting_state_indicator() {
735 let renderer = SelectionStyleRenderer::with_defaults("test".to_string());
736 let choice = Choice {
737 id: "loading".to_string(),
738 content: Some("Loading".to_string()),
739 selected: false,
740 hovered: false,
741 waiting: true,
742 script: None,
743 execution_mode: crate::model::common::ExecutionMode::Immediate,
744 redirect_output: None,
745 append_output: None,
746 };
747
748 let (fg, bg, text) = renderer.calculate_style_colors_and_text(
749 &choice,
750 &Some("white".to_string()),
751 &Some("black".to_string()),
752 &Some("bright_white".to_string()),
753 &Some("blue".to_string()),
754 false,
755 );
756
757 assert_eq!(text, "Loading...");
758 }
759
760 #[test]
761 fn test_focus_intensity() {
762 let config = SelectionStyleConfig {
763 selection_style: SelectionStyle::ColorHighlight,
764 focus_style: FocusStyle::Intensity,
765 ..SelectionStyleConfig::default()
766 };
767
768 let renderer = SelectionStyleRenderer::new("test".to_string(), config);
769 let choice = Choice {
770 id: "focused".to_string(),
771 content: Some("Focused".to_string()),
772 selected: true,
773 hovered: false,
774 waiting: false,
775 script: None,
776 execution_mode: crate::model::common::ExecutionMode::Immediate,
777 redirect_output: None,
778 append_output: None,
779 };
780
781 let (fg, bg, text) = renderer.calculate_style_colors_and_text(
782 &choice,
783 &Some("white".to_string()),
784 &Some("black".to_string()),
785 &Some("red".to_string()),
786 &Some("blue".to_string()),
787 true, );
789
790 assert_eq!(fg, Some("bright_red".to_string()));
792 assert_eq!(bg, Some("blue".to_string()));
793 }
794}