1use crate::models::{
4 configuration::{ConfigurationState, DistributionType},
5 config::FairnessMode,
6 interactive_mode::{ApplicationMode, ConfigurationField, InteractiveMode},
7};
8use anyhow::Result;
9use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
10use ratatui::{
11 buffer::Buffer,
12 layout::{Alignment, Constraint, Direction, Layout, Rect},
13 style::{Color, Modifier, Style},
14 text::{Line, Span},
15 widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Widget},
16};
17
18const TOTAL_ALGORITHMS: usize = 7;
20const MAX_ARRAY_SIZE: u32 = 10000;
21const MAX_BUDGET: u32 = 1000000;
22const MAX_FLOAT_PARAM: f32 = 100.0;
23const MAX_INPUT_LENGTH: usize = 10;
24
25#[derive(Debug, Clone)]
27pub struct InteractiveConfigMenu {
28 pub interactive_mode: InteractiveMode,
30 pub config_state: ConfigurationState,
32 array_size_index: usize,
34 distribution_index: usize,
36 fairness_mode_index: usize,
38 current_parameter_value: Option<String>,
40}
41
42impl InteractiveConfigMenu {
43 pub fn new() -> Self {
45 Self {
46 interactive_mode: InteractiveMode::new(),
47 config_state: ConfigurationState::new(),
48 array_size_index: 3, distribution_index: 0, fairness_mode_index: 2, current_parameter_value: None,
52 }
53 }
54
55 pub fn get_interactive_mode(&self) -> &InteractiveMode {
57 &self.interactive_mode
58 }
59
60 pub fn get_interactive_mode_mut(&mut self) -> &mut InteractiveMode {
62 &mut self.interactive_mode
63 }
64
65 pub fn get_run_config(&self) -> Option<crate::models::config::RunConfiguration> {
67 if self.config_state.is_valid() && self.interactive_mode.current_mode == ApplicationMode::Racing {
69 Some(crate::models::config::RunConfiguration {
70 array_size: self.config_state.array_size as usize,
71 distribution: self.config_state.to_legacy_distribution(),
72 seed: 12345, fairness_mode: self.config_state.fairness_mode.clone(),
74 target_fps: 30,
75 })
76 } else {
77 None
78 }
79 }
80
81 pub fn should_start_new_race(&self) -> bool {
83 self.config_state.is_valid() &&
84 self.interactive_mode.current_mode == ApplicationMode::Racing &&
85 self.interactive_mode.config_focus.is_none()
86 }
87
88 pub fn is_racing(&self) -> bool {
90 self.interactive_mode.current_mode == ApplicationMode::Racing
91 }
92
93 pub fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<bool> {
95 match key_event {
96 KeyEvent {
98 code: KeyCode::Char('k'),
99 modifiers: KeyModifiers::NONE,
100 ..
101 } => {
102 if self.interactive_mode.current_mode != ApplicationMode::Racing {
103 self.interactive_mode.set_config_focus(ConfigurationField::ArraySize)?;
104 self.update_array_size_index_from_config();
105 Ok(true)
106 } else {
107 Ok(false)
108 }
109 }
110 KeyEvent {
111 code: KeyCode::Char('b'),
112 modifiers: KeyModifiers::NONE,
113 ..
114 } => {
115 if self.interactive_mode.current_mode != ApplicationMode::Racing {
116 self.interactive_mode.set_config_focus(ConfigurationField::Distribution)?;
117 self.update_distribution_index_from_config();
118 Ok(true)
119 } else {
120 Ok(false)
121 }
122 }
123 KeyEvent {
124 code: KeyCode::Char('f'),
125 modifiers: KeyModifiers::NONE,
126 ..
127 } => {
128 if self.interactive_mode.current_mode != ApplicationMode::Racing {
129 self.interactive_mode.set_config_focus(ConfigurationField::FairnessMode)?;
130 self.update_fairness_mode_index_from_config();
131 Ok(true)
132 } else {
133 Ok(false)
134 }
135 }
136 KeyEvent {
138 code: KeyCode::Char('?'),
139 modifiers: KeyModifiers::NONE,
140 ..
141 } => {
142 self.interactive_mode.toggle_help();
143 Ok(true)
144 }
145 KeyEvent {
147 code: KeyCode::Char('v'),
148 modifiers: KeyModifiers::NONE,
149 ..
150 } => {
151 self.interactive_mode.cycle_array_view_algorithm(TOTAL_ALGORITHMS);
152 Ok(true)
153 }
154 KeyEvent {
156 code: KeyCode::Char(' '),
157 modifiers: KeyModifiers::NONE,
158 ..
159 } => {
160 match self.interactive_mode.current_mode {
161 ApplicationMode::Configuration => {
162 self.interactive_mode.transition_to_racing()?;
163 Ok(true)
164 }
165 ApplicationMode::Racing => {
166 self.interactive_mode.transition_to_paused()?;
167 Ok(true)
168 }
169 ApplicationMode::Paused => {
170 self.interactive_mode.transition_to_racing()?;
171 Ok(true)
172 }
173 ApplicationMode::Complete => {
174 self.interactive_mode.transition_to_configuration()?;
175 Ok(true)
176 }
177 }
178 }
179 KeyEvent {
181 code: KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right,
182 modifiers: KeyModifiers::NONE,
183 ..
184 } => {
185 if let Some(field) = self.interactive_mode.config_focus {
186 self.handle_navigation_key(field, key_event.code)?;
187 Ok(true)
188 } else {
189 Ok(false)
190 }
191 }
192 KeyEvent {
194 code: KeyCode::Enter,
195 modifiers: KeyModifiers::NONE,
196 ..
197 } => {
198 if self.interactive_mode.config_focus.is_some() {
199 self.handle_confirmation()?;
200 Ok(true)
201 } else {
202 Ok(false)
203 }
204 }
205 KeyEvent {
207 code: KeyCode::Esc,
208 modifiers: KeyModifiers::NONE,
209 ..
210 } => {
211 if self.interactive_mode.config_focus.is_some() {
212 self.interactive_mode.clear_config_focus();
213 Ok(true)
214 } else {
215 Ok(false)
216 }
217 }
218 KeyEvent {
220 code: KeyCode::Char(c),
221 modifiers: KeyModifiers::NONE,
222 ..
223 } if c.is_ascii_digit() || c == '.' => {
224 if let Some(field) = self.interactive_mode.config_focus {
225 if matches!(field, ConfigurationField::BudgetParam | ConfigurationField::AlphaParam | ConfigurationField::BetaParam | ConfigurationField::LearningRateParam) {
226 self.handle_numeric_input(c)?;
227 Ok(true)
228 } else {
229 Ok(false)
230 }
231 } else {
232 Ok(false)
233 }
234 }
235 KeyEvent {
237 code: KeyCode::Backspace,
238 modifiers: KeyModifiers::NONE,
239 ..
240 } => {
241 if let Some(field) = self.interactive_mode.config_focus {
242 if matches!(field, ConfigurationField::BudgetParam | ConfigurationField::AlphaParam | ConfigurationField::BetaParam | ConfigurationField::LearningRateParam) {
243 self.handle_backspace_input()?;
244 Ok(true)
245 } else {
246 Ok(false)
247 }
248 } else {
249 Ok(false)
250 }
251 }
252 _ => Ok(false), }
254 }
255
256 fn handle_navigation_key(&mut self, field: ConfigurationField, key_code: KeyCode) -> Result<()> {
258 match field {
259 ConfigurationField::ArraySize => {
260 let sizes = ConfigurationState::get_available_array_sizes();
261 match key_code {
262 KeyCode::Up => {
263 if self.array_size_index > 0 {
264 self.array_size_index -= 1;
265 } else {
266 self.array_size_index = sizes.len() - 1; }
268 }
269 KeyCode::Down => {
270 if self.array_size_index < sizes.len() - 1 {
271 self.array_size_index += 1;
272 } else {
273 self.array_size_index = 0; }
275 }
276 _ => {}
277 }
278 }
279 ConfigurationField::Distribution => {
280 let distributions = ConfigurationState::get_available_distributions();
281 match key_code {
282 KeyCode::Up => {
283 if self.distribution_index > 0 {
284 self.distribution_index -= 1;
285 } else {
286 self.distribution_index = distributions.len() - 1; }
288 }
289 KeyCode::Down => {
290 if self.distribution_index < distributions.len() - 1 {
291 self.distribution_index += 1;
292 } else {
293 self.distribution_index = 0; }
295 }
296 _ => {}
297 }
298 }
299 ConfigurationField::FairnessMode => {
300 let fairness_modes = ConfigurationState::get_available_fairness_modes();
301 match key_code {
302 KeyCode::Up => {
303 if self.fairness_mode_index > 0 {
304 self.fairness_mode_index -= 1;
305 } else {
306 self.fairness_mode_index = fairness_modes.len() - 1; }
308 }
309 KeyCode::Down => {
310 if self.fairness_mode_index < fairness_modes.len() - 1 {
311 self.fairness_mode_index += 1;
312 } else {
313 self.fairness_mode_index = 0; }
315 }
316 _ => {}
317 }
318 }
319 _ => {
320 }
322 }
323 Ok(())
324 }
325
326 fn handle_confirmation(&mut self) -> Result<()> {
328 if let Some(field) = self.interactive_mode.config_focus {
329 match field {
330 ConfigurationField::ArraySize => {
331 let sizes = ConfigurationState::get_available_array_sizes();
332 if let Some(size) = sizes.get(self.array_size_index) {
333 self.interactive_mode.set_array_size_interactive(*size)?;
334 self.config_state.set_array_size(*size)?; self.interactive_mode.clear_config_focus();
336 }
337 }
338 ConfigurationField::Distribution => {
339 let distributions = ConfigurationState::get_available_distributions();
340 if let Some(distribution) = distributions.get(self.distribution_index) {
341 self.interactive_mode.set_distribution_interactive(*distribution);
342 self.config_state.distribution = *distribution; self.interactive_mode.clear_config_focus();
344 }
345 }
346 ConfigurationField::FairnessMode => {
347 let fairness_modes = ConfigurationState::get_available_fairness_modes();
348 if let Some(fairness_mode) = fairness_modes.get(self.fairness_mode_index) {
349 self.interactive_mode.set_fairness_mode_interactive(fairness_mode.clone());
350 self.config_state.set_fairness_mode(fairness_mode.clone()); self.interactive_mode.clear_config_focus();
352 }
353 }
354 ConfigurationField::BudgetParam => {
355 if let Some(ref value_str) = self.current_parameter_value
356 && let Ok(budget) = value_str.parse::<u32>() {
357 self.interactive_mode.set_budget_parameter(budget)?;
358 self.config_state.budget = Some(budget);
359 self.current_parameter_value = None;
360 self.interactive_mode.clear_config_focus();
361 }
362 }
363 ConfigurationField::AlphaParam => {
364 if let Some(ref value_str) = self.current_parameter_value
365 && let Ok(alpha) = value_str.parse::<f32>()
366 && alpha > 0.0 {
367 self.config_state.alpha = Some(alpha);
368 self.current_parameter_value = None;
369 self.interactive_mode.clear_config_focus();
370 }
371 }
372 ConfigurationField::BetaParam => {
373 if let Some(ref value_str) = self.current_parameter_value
374 && let Ok(beta) = value_str.parse::<f32>()
375 && beta > 0.0 {
376 self.config_state.beta = Some(beta);
377 self.current_parameter_value = None;
378 self.interactive_mode.clear_config_focus();
379 }
380 }
381 ConfigurationField::LearningRateParam => {
382 if let Some(ref value_str) = self.current_parameter_value
383 && let Ok(learning_rate) = value_str.parse::<f32>()
384 && learning_rate > 0.0 && learning_rate <= 1.0 {
385 self.config_state.learning_rate = Some(learning_rate);
386 self.current_parameter_value = None;
387 self.interactive_mode.clear_config_focus();
388 }
389 }
390 }
391 }
392 Ok(())
393 }
394
395 fn update_array_size_index_from_config(&mut self) {
397 let sizes = ConfigurationState::get_available_array_sizes();
398 let current_size = self.interactive_mode.get_current_config().array_size;
399
400 if let Some(index) = sizes.iter().position(|&size| size == current_size) {
401 self.array_size_index = index;
402 }
403 }
404
405 fn update_distribution_index_from_config(&mut self) {
407 let distributions = ConfigurationState::get_available_distributions();
408 let current_distribution = self.interactive_mode.get_current_config().distribution;
409
410 if let Some(index) = distributions.iter().position(|&dist| dist == current_distribution) {
411 self.distribution_index = index;
412 }
413 }
414
415 fn update_fairness_mode_index_from_config(&mut self) {
417 let fairness_modes = ConfigurationState::get_available_fairness_modes();
418 let current_mode = &self.interactive_mode.get_current_config().fairness_mode;
419
420 if let Some(index) = fairness_modes.iter().position(|mode| {
422 std::mem::discriminant(mode) == std::mem::discriminant(current_mode)
423 }) {
424 self.fairness_mode_index = index;
425 }
426 }
427
428 pub fn render(&self, area: Rect, buf: &mut Buffer) {
430 self.render_main_config_screen(area, buf);
432
433 if let Some(field) = self.interactive_mode.config_focus {
435 match field {
436 ConfigurationField::ArraySize => {
437 self.render_array_size_menu(area, buf);
438 }
439 ConfigurationField::Distribution => {
440 self.render_distribution_menu(area, buf);
441 }
442 ConfigurationField::FairnessMode => {
443 self.render_fairness_mode_menu(area, buf);
444 }
445 _ => {
446 }
448 }
449 }
450
451 if self.interactive_mode.should_show_help_overlay() {
453 self.render_help_overlay(area, buf);
454 }
455 }
456
457 fn render_main_config_screen(&self, area: Rect, buf: &mut Buffer) {
459 let config = self.interactive_mode.get_current_config();
460
461 let chunks = Layout::default()
463 .direction(Direction::Vertical)
464 .constraints([
465 Constraint::Length(3), Constraint::Min(0), Constraint::Length(3), ])
469 .split(area);
470
471 let title = Paragraph::new(vec![
473 Line::from(vec![
474 Span::styled(
475 "Sorting Race v0.2 - Interactive Configuration",
476 Style::default()
477 .fg(Color::Cyan)
478 .add_modifier(Modifier::BOLD),
479 ),
480 ]),
481 ])
482 .block(Block::default().borders(Borders::ALL))
483 .alignment(Alignment::Center);
484 title.render(chunks[0], buf);
485
486 let config_lines = vec![
488 Line::from(""),
489 Line::from(vec![
490 Span::styled("Current Configuration:", Style::default().add_modifier(Modifier::BOLD)),
491 ]),
492 Line::from(""),
493 Line::from(vec![
494 Span::styled("Array Size: ", Style::default().fg(Color::Yellow)),
495 Span::styled(
496 format!("{} elements", config.array_size),
497 Style::default().fg(Color::White),
498 ),
499 Span::styled(" [Press 'k' to change]", Style::default().fg(Color::Gray)),
500 ]),
501 Line::from(vec![
502 Span::styled("Distribution: ", Style::default().fg(Color::Yellow)),
503 Span::styled(
504 format!("{:?}", config.distribution),
505 Style::default().fg(Color::White),
506 ),
507 Span::styled(" [Press 'b' to change]", Style::default().fg(Color::Gray)),
508 ]),
509 Line::from(vec![
510 Span::styled("Fairness Mode: ", Style::default().fg(Color::Yellow)),
511 Span::styled(
512 self.format_fairness_mode(&config.fairness_mode),
513 Style::default().fg(Color::White),
514 ),
515 Span::styled(" [Press 'f' to change]", Style::default().fg(Color::Gray)),
516 ]),
517 Line::from(""),
518 Line::from(vec![
519 Span::styled(
520 match self.interactive_mode.current_mode {
521 ApplicationMode::Configuration => "Ready to start race",
522 ApplicationMode::Racing => "Race in progress...",
523 ApplicationMode::Paused => "Race paused",
524 ApplicationMode::Complete => "Race complete - configure for next race",
525 },
526 Style::default().fg(match self.interactive_mode.current_mode {
527 ApplicationMode::Configuration => Color::Green,
528 ApplicationMode::Racing => Color::Blue,
529 ApplicationMode::Paused => Color::Yellow,
530 ApplicationMode::Complete => Color::Magenta,
531 }),
532 ),
533 ]),
534 ];
535
536 let config_content = Paragraph::new(config_lines)
537 .block(Block::default().borders(Borders::ALL))
538 .alignment(Alignment::Left);
539 config_content.render(chunks[1], buf);
540
541 let instruction_text = match self.interactive_mode.current_mode {
543 ApplicationMode::Configuration => "Press SPACE to start race | k/b/f to configure | v to switch array view | ? for help | q to quit",
544 ApplicationMode::Racing => "Press SPACE to pause | v to switch array view | ? for help | q to quit",
545 ApplicationMode::Paused => "Press SPACE to resume | k/b/f to configure | v to switch array view | ? for help | q to quit",
546 ApplicationMode::Complete => "Press SPACE or k/b/f to configure next race | v to switch array view | ? for help | q to quit",
547 };
548
549 let instructions = Paragraph::new(instruction_text)
550 .block(Block::default().borders(Borders::ALL))
551 .alignment(Alignment::Center)
552 .style(Style::default().fg(Color::Cyan));
553 instructions.render(chunks[2], buf);
554
555 if let Some(error) = self.interactive_mode.get_error_message() {
557 self.render_error_overlay(area, buf, error);
558 }
559 }
560
561 fn format_fairness_mode(&self, mode: &FairnessMode) -> String {
563 match mode {
564 FairnessMode::ComparisonBudget { k } => format!("Comparison (budget: {})", k),
565 FairnessMode::Weighted { alpha, beta } => format!("Weighted (α:{:.1}, β:{:.1})", alpha, beta),
566 FairnessMode::WallTime { slice_ms } => format!("Wall-time ({}ms)", slice_ms),
567 FairnessMode::Adaptive { learning_rate } => format!("Adaptive (rate:{:.1})", learning_rate),
568 FairnessMode::EqualSteps => "Equal Steps".to_string(),
569 }
570 }
571
572 fn render_array_size_menu(&self, area: Rect, buf: &mut Buffer) {
574 let sizes = ConfigurationState::get_available_array_sizes();
575
576 let popup_area = self.centered_rect(40, 60, area);
578
579 Clear.render(popup_area, buf);
581
582 let items: Vec<ListItem> = sizes
584 .iter()
585 .enumerate()
586 .map(|(i, &size)| {
587 let style = if i == self.array_size_index {
588 Style::default().bg(Color::Blue).fg(Color::White)
589 } else {
590 Style::default()
591 };
592
593 ListItem::new(format!("{} elements", size)).style(style)
594 })
595 .collect();
596
597 let list = List::new(items)
598 .block(Block::default()
599 .borders(Borders::ALL)
600 .title("Select Array Size"))
601 .highlight_style(Style::default().bg(Color::Blue));
602
603 list.render(popup_area, buf);
604
605 let instruction_area = Rect {
607 x: popup_area.x,
608 y: popup_area.y + popup_area.height,
609 width: popup_area.width,
610 height: 1,
611 };
612
613 if instruction_area.y < area.height {
614 let instructions = Paragraph::new("↑↓ Navigate | Enter to select | Esc to cancel")
615 .style(Style::default().fg(Color::Gray));
616 instructions.render(instruction_area, buf);
617 }
618 }
619
620 fn render_distribution_menu(&self, area: Rect, buf: &mut Buffer) {
622 let distributions = ConfigurationState::get_available_distributions();
623
624 let popup_area = self.centered_rect(40, 50, area);
626
627 Clear.render(popup_area, buf);
629
630 let items: Vec<ListItem> = distributions
632 .iter()
633 .enumerate()
634 .map(|(i, &dist)| {
635 let style = if i == self.distribution_index {
636 Style::default().bg(Color::Blue).fg(Color::White)
637 } else {
638 Style::default()
639 };
640
641 let description = match dist {
642 DistributionType::Shuffled => "Random order",
643 DistributionType::Reversed => "Reverse sorted",
644 DistributionType::NearlySorted => "Mostly sorted",
645 DistributionType::FewUnique => "Few unique values",
646 };
647
648 ListItem::new(format!("{:?} - {}", dist, description)).style(style)
649 })
650 .collect();
651
652 let list = List::new(items)
653 .block(Block::default()
654 .borders(Borders::ALL)
655 .title("Select Distribution"))
656 .highlight_style(Style::default().bg(Color::Blue));
657
658 list.render(popup_area, buf);
659
660 let instruction_area = Rect {
662 x: popup_area.x,
663 y: popup_area.y + popup_area.height,
664 width: popup_area.width,
665 height: 1,
666 };
667
668 if instruction_area.y < area.height {
669 let instructions = Paragraph::new("↑↓ Navigate | Enter to select | Esc to cancel")
670 .style(Style::default().fg(Color::Gray));
671 instructions.render(instruction_area, buf);
672 }
673 }
674
675 fn render_fairness_mode_menu(&self, area: Rect, buf: &mut Buffer) {
677 let fairness_modes = ConfigurationState::get_available_fairness_modes();
678
679 let popup_area = self.centered_rect(60, 70, area);
681
682 Clear.render(popup_area, buf);
684
685 let items: Vec<ListItem> = fairness_modes
687 .iter()
688 .enumerate()
689 .map(|(i, mode)| {
690 let style = if i == self.fairness_mode_index {
691 Style::default().bg(Color::Blue).fg(Color::White)
692 } else {
693 Style::default()
694 };
695
696 let description = match mode {
697 FairnessMode::ComparisonBudget { .. } => "Fixed comparison budget per step",
698 FairnessMode::Weighted { .. } => "Weighted by comparisons and moves",
699 FairnessMode::WallTime { .. } => "Equal time slices for each algorithm",
700 FairnessMode::Adaptive { .. } => "Adaptive allocation based on performance",
701 _ => "Equal steps",
702 };
703
704 ListItem::new(vec![
705 Line::from(self.format_fairness_mode(mode)),
706 Line::from(Span::styled(description, Style::default().fg(Color::Gray))),
707 ]).style(style)
708 })
709 .collect();
710
711 let list = List::new(items)
712 .block(Block::default()
713 .borders(Borders::ALL)
714 .title("Select Fairness Mode"))
715 .highlight_style(Style::default().bg(Color::Blue));
716
717 list.render(popup_area, buf);
718
719 let instruction_area = Rect {
721 x: popup_area.x,
722 y: popup_area.y + popup_area.height,
723 width: popup_area.width,
724 height: 1,
725 };
726
727 if instruction_area.y < area.height {
728 let instructions = Paragraph::new("↑↓ Navigate | Enter to select | Esc to cancel")
729 .style(Style::default().fg(Color::Gray));
730 instructions.render(instruction_area, buf);
731 }
732 }
733
734 fn render_help_overlay(&self, area: Rect, buf: &mut Buffer) {
736 let popup_area = self.centered_rect(80, 80, area);
737
738 Clear.render(popup_area, buf);
740
741 let help_content = self.interactive_mode.get_help_overlay_content();
742 let help_widget = Paragraph::new(help_content)
743 .block(Block::default()
744 .borders(Borders::ALL)
745 .title("Help - Keyboard Shortcuts"))
746 .alignment(Alignment::Left);
747
748 help_widget.render(popup_area, buf);
749 }
750
751 fn render_error_overlay(&self, area: Rect, buf: &mut Buffer, error: &str) {
753 let popup_area = self.centered_rect(60, 20, area);
754
755 Clear.render(popup_area, buf);
757
758 let error_widget = Paragraph::new(error)
759 .block(Block::default()
760 .borders(Borders::ALL)
761 .title("Error")
762 .border_style(Style::default().fg(Color::Red)))
763 .style(Style::default().fg(Color::Red))
764 .alignment(Alignment::Center);
765
766 error_widget.render(popup_area, buf);
767 }
768
769 fn centered_rect(&self, percent_x: u16, percent_y: u16, r: Rect) -> Rect {
771 let popup_layout = Layout::default()
772 .direction(Direction::Vertical)
773 .constraints([
774 Constraint::Percentage((100 - percent_y) / 2),
775 Constraint::Percentage(percent_y),
776 Constraint::Percentage((100 - percent_y) / 2),
777 ])
778 .split(r);
779
780 Layout::default()
781 .direction(Direction::Horizontal)
782 .constraints([
783 Constraint::Percentage((100 - percent_x) / 2),
784 Constraint::Percentage(percent_x),
785 Constraint::Percentage((100 - percent_x) / 2),
786 ])
787 .split(popup_layout[1])[1]
788 }
789
790 fn handle_numeric_input(&mut self, digit: char) -> Result<()> {
792 if self.current_parameter_value.is_none() {
793 self.current_parameter_value = Some(String::new());
794 }
795 let current_value = self.current_parameter_value.as_mut().unwrap();
796
797 if digit == '.' && current_value.contains('.') {
799 return Ok(());
800 }
801
802 let test_value = format!("{}{}", current_value, digit);
804
805 let is_valid = match self.interactive_mode.config_focus {
807 Some(ConfigurationField::ArraySize) => {
808 test_value.parse::<u32>()
809 .map(|v| v <= MAX_ARRAY_SIZE)
810 .unwrap_or(true) }
812 Some(ConfigurationField::BudgetParam) => {
813 test_value.parse::<u32>()
814 .map(|v| v <= MAX_BUDGET)
815 .unwrap_or(true)
816 }
817 Some(ConfigurationField::AlphaParam | ConfigurationField::BetaParam | ConfigurationField::LearningRateParam) => {
818 test_value.parse::<f32>()
819 .map(|v| v <= MAX_FLOAT_PARAM)
820 .unwrap_or(true)
821 }
822 _ => true,
823 };
824
825 if is_valid && current_value.len() < MAX_INPUT_LENGTH {
826 current_value.push(digit);
827 }
828 Ok(())
829 }
830
831 fn handle_backspace_input(&mut self) -> Result<()> {
833 if let Some(ref mut current_value) = self.current_parameter_value {
834 current_value.pop();
835 if current_value.is_empty() {
836 self.current_parameter_value = None;
837 }
838 }
839 Ok(())
840 }
841}
842
843impl Default for InteractiveConfigMenu {
844 fn default() -> Self {
845 Self::new()
846 }
847}
848
849impl Widget for InteractiveConfigMenu {
850 fn render(self, area: Rect, buf: &mut Buffer) {
851 InteractiveConfigMenu::render(&self, area, buf);
852 }
853}
854
855impl Widget for &InteractiveConfigMenu {
856 fn render(self, area: Rect, buf: &mut Buffer) {
857 InteractiveConfigMenu::render(self, area, buf);
858 }
859}
860
861#[cfg(test)]
862mod tests {
863 use super::*;
864 use crossterm::event::KeyEventKind;
865
866 #[test]
867 fn test_interactive_config_menu_creation() {
868 let menu = InteractiveConfigMenu::new();
869 assert_eq!(menu.interactive_mode.current_mode, ApplicationMode::Configuration);
870 assert_eq!(menu.array_size_index, 3); assert_eq!(menu.distribution_index, 0); assert_eq!(menu.fairness_mode_index, 2); }
874
875 #[test]
876 fn test_configuration_key_handling() {
877 let mut menu = InteractiveConfigMenu::new();
878
879 let k_key = KeyEvent {
881 code: KeyCode::Char('k'),
882 modifiers: KeyModifiers::NONE,
883 kind: KeyEventKind::Press,
884 state: crossterm::event::KeyEventState::empty(),
885 };
886
887 let handled = menu.handle_key_event(k_key).unwrap();
888 assert!(handled);
889 assert_eq!(menu.interactive_mode.config_focus, Some(ConfigurationField::ArraySize));
890 }
891
892 #[test]
893 fn test_navigation_in_array_size_menu() {
894 let mut menu = InteractiveConfigMenu::new();
895 menu.interactive_mode.set_config_focus(ConfigurationField::ArraySize).unwrap();
896 menu.array_size_index = 2; let up_key = KeyEvent {
900 code: KeyCode::Up,
901 modifiers: KeyModifiers::NONE,
902 kind: KeyEventKind::Press,
903 state: crossterm::event::KeyEventState::empty(),
904 };
905
906 let handled = menu.handle_key_event(up_key).unwrap();
907 assert!(handled);
908 assert_eq!(menu.array_size_index, 1); }
910
911 #[test]
912 fn test_navigation_wrapping() {
913 let mut menu = InteractiveConfigMenu::new();
914 menu.interactive_mode.set_config_focus(ConfigurationField::ArraySize).unwrap();
915 menu.array_size_index = 0; let up_key = KeyEvent {
919 code: KeyCode::Up,
920 modifiers: KeyModifiers::NONE,
921 kind: KeyEventKind::Press,
922 state: crossterm::event::KeyEventState::empty(),
923 };
924
925 let handled = menu.handle_key_event(up_key).unwrap();
926 assert!(handled);
927 let sizes = ConfigurationState::get_available_array_sizes();
928 assert_eq!(menu.array_size_index, sizes.len() - 1); }
930
931 #[test]
932 fn test_confirmation_handling() {
933 let mut menu = InteractiveConfigMenu::new();
934 menu.interactive_mode.set_config_focus(ConfigurationField::ArraySize).unwrap();
935 menu.array_size_index = 0; let enter_key = KeyEvent {
938 code: KeyCode::Enter,
939 modifiers: KeyModifiers::NONE,
940 kind: KeyEventKind::Press,
941 state: crossterm::event::KeyEventState::empty(),
942 };
943
944 let handled = menu.handle_key_event(enter_key).unwrap();
945 assert!(handled);
946 assert_eq!(menu.interactive_mode.get_current_config().array_size, 10);
947 assert_eq!(menu.interactive_mode.config_focus, None); }
949
950 #[test]
951 fn test_help_toggle() {
952 let mut menu = InteractiveConfigMenu::new();
953
954 let help_key = KeyEvent {
955 code: KeyCode::Char('?'),
956 modifiers: KeyModifiers::NONE,
957 kind: KeyEventKind::Press,
958 state: crossterm::event::KeyEventState::empty(),
959 };
960
961 assert!(!menu.interactive_mode.should_show_help_overlay());
962
963 let handled = menu.handle_key_event(help_key).unwrap();
964 assert!(handled);
965 assert!(menu.interactive_mode.should_show_help_overlay());
966 }
967
968 #[test]
969 fn test_race_control_transitions() {
970 let mut menu = InteractiveConfigMenu::new();
971
972 let space_key = KeyEvent {
973 code: KeyCode::Char(' '),
974 modifiers: KeyModifiers::NONE,
975 kind: KeyEventKind::Press,
976 state: crossterm::event::KeyEventState::empty(),
977 };
978
979 assert_eq!(menu.interactive_mode.current_mode, ApplicationMode::Configuration);
981 let handled = menu.handle_key_event(space_key).unwrap();
982 assert!(handled);
983 assert_eq!(menu.interactive_mode.current_mode, ApplicationMode::Racing);
984
985 let handled = menu.handle_key_event(space_key).unwrap();
987 assert!(handled);
988 assert_eq!(menu.interactive_mode.current_mode, ApplicationMode::Paused);
989 }
990}