1use super::action_logger::ActionLog;
8use super::cell::CellPreview;
9
10#[derive(Debug, Clone)]
12pub enum DebugTableRow {
13 Section(String),
15 Entry { key: String, value: String },
17}
18
19#[derive(Debug, Clone)]
21pub struct DebugTableOverlay {
22 pub title: String,
24 pub rows: Vec<DebugTableRow>,
26 pub cell_preview: Option<CellPreview>,
28}
29
30impl DebugTableOverlay {
31 pub fn new(title: impl Into<String>, rows: Vec<DebugTableRow>) -> Self {
33 Self {
34 title: title.into(),
35 rows,
36 cell_preview: None,
37 }
38 }
39
40 pub fn with_cell_preview(
42 title: impl Into<String>,
43 rows: Vec<DebugTableRow>,
44 preview: CellPreview,
45 ) -> Self {
46 Self {
47 title: title.into(),
48 rows,
49 cell_preview: Some(preview),
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
56pub enum DebugOverlay {
57 Inspect(DebugTableOverlay),
59 State(DebugTableOverlay),
61 ActionLog(ActionLogOverlay),
63 ActionDetail(ActionDetailOverlay),
65}
66
67#[derive(Debug, Clone)]
69pub struct ActionDetailOverlay {
70 pub sequence: u64,
72 pub name: String,
74 pub params: String,
76 pub elapsed: String,
78}
79
80impl DebugOverlay {
81 pub fn table(&self) -> Option<&DebugTableOverlay> {
83 match self {
84 DebugOverlay::Inspect(table) | DebugOverlay::State(table) => Some(table),
85 DebugOverlay::ActionLog(_) | DebugOverlay::ActionDetail(_) => None,
86 }
87 }
88
89 pub fn action_log(&self) -> Option<&ActionLogOverlay> {
91 match self {
92 DebugOverlay::ActionLog(log) => Some(log),
93 _ => None,
94 }
95 }
96
97 pub fn action_log_mut(&mut self) -> Option<&mut ActionLogOverlay> {
99 match self {
100 DebugOverlay::ActionLog(log) => Some(log),
101 _ => None,
102 }
103 }
104
105 pub fn kind(&self) -> &'static str {
107 match self {
108 DebugOverlay::Inspect(_) => "inspect",
109 DebugOverlay::State(_) => "state",
110 DebugOverlay::ActionLog(_) => "action_log",
111 DebugOverlay::ActionDetail(_) => "action_detail",
112 }
113 }
114}
115
116#[derive(Debug, Clone)]
122pub struct ActionLogDisplayEntry {
123 pub sequence: u64,
125 pub name: String,
127 pub params: String,
129 pub params_detail: String,
131 pub elapsed: String,
133}
134
135#[derive(Debug, Clone)]
137pub struct ActionLogOverlay {
138 pub title: String,
140 pub entries: Vec<ActionLogDisplayEntry>,
142 pub selected: usize,
144 pub scroll_offset: usize,
146 pub search_query: String,
148 pub search_matches: Vec<usize>,
150 pub search_match_index: usize,
152 pub search_input_active: bool,
154}
155
156impl ActionLogOverlay {
157 pub fn from_log(log: &ActionLog, title: impl Into<String>) -> Self {
159 let entries: Vec<_> = log
160 .entries_rev()
161 .map(|e| ActionLogDisplayEntry {
162 sequence: e.sequence,
163 name: e.name.to_string(),
164 params: e.params.clone(),
165 params_detail: e.params_pretty.clone(),
166 elapsed: e.elapsed.clone(),
167 })
168 .collect();
169
170 Self {
171 title: title.into(),
172 entries,
173 selected: 0,
174 scroll_offset: 0,
175 search_query: String::new(),
176 search_matches: Vec::new(),
177 search_match_index: 0,
178 search_input_active: false,
179 }
180 }
181
182 pub fn scroll_up(&mut self) {
184 if self.navigate_filtered(|current, _| current.saturating_sub(1)) {
185 return;
186 }
187 if self.selected > 0 {
188 self.selected -= 1;
189 self.sync_search_index_from_selection();
190 }
191 }
192
193 pub fn scroll_down(&mut self) {
195 if self.navigate_filtered(|current, max| current.saturating_add(1).min(max)) {
196 return;
197 }
198 if self.selected + 1 < self.entries.len() {
199 self.selected += 1;
200 self.sync_search_index_from_selection();
201 }
202 }
203
204 pub fn scroll_to_top(&mut self) {
206 if self.navigate_filtered(|_, _| 0) {
207 return;
208 }
209 self.selected = 0;
210 self.sync_search_index_from_selection();
211 }
212
213 pub fn scroll_to_bottom(&mut self) {
215 if self.navigate_filtered(|_, max| max) {
216 return;
217 }
218 if !self.entries.is_empty() {
219 self.selected = self.entries.len() - 1;
220 self.sync_search_index_from_selection();
221 }
222 }
223
224 pub fn page_up(&mut self, page_size: usize) {
226 if self.navigate_filtered(|current, _| current.saturating_sub(page_size)) {
227 return;
228 }
229 self.selected = self.selected.saturating_sub(page_size);
230 self.sync_search_index_from_selection();
231 }
232
233 pub fn page_down(&mut self, page_size: usize) {
235 if self.navigate_filtered(|current, max| current.saturating_add(page_size).min(max)) {
236 return;
237 }
238 self.selected = (self.selected + page_size).min(self.entries.len().saturating_sub(1));
239 self.sync_search_index_from_selection();
240 }
241
242 pub fn scroll_offset_for(&self, visible_rows: usize) -> usize {
244 if visible_rows == 0 {
245 return 0;
246 }
247 if self.selected >= visible_rows {
248 self.selected - visible_rows + 1
249 } else {
250 0
251 }
252 }
253
254 pub fn get_selected(&self) -> Option<&ActionLogDisplayEntry> {
256 self.entries.get(self.selected)
257 }
258
259 pub fn selected_detail(&self) -> Option<ActionDetailOverlay> {
261 self.get_selected().map(|entry| ActionDetailOverlay {
262 sequence: entry.sequence,
263 name: entry.name.clone(),
264 params: entry.params_detail.clone(),
265 elapsed: entry.elapsed.clone(),
266 })
267 }
268
269 pub fn set_search_query(&mut self, query: impl Into<String>) {
271 self.search_query = query.into();
272 self.rebuild_search_matches();
273 }
274
275 pub fn push_search_char(&mut self, ch: char) {
277 self.search_query.push(ch);
278 self.rebuild_search_matches();
279 }
280
281 pub fn pop_search_char(&mut self) -> bool {
283 let popped = self.search_query.pop().is_some();
284 if popped {
285 self.rebuild_search_matches();
286 }
287 popped
288 }
289
290 pub fn clear_search_query(&mut self) {
292 self.search_query.clear();
293 self.search_matches.clear();
294 self.search_match_index = 0;
295 }
296
297 pub fn search_next(&mut self) -> bool {
299 if self.search_matches.is_empty() {
300 return false;
301 }
302 self.search_match_index = (self.search_match_index + 1) % self.search_matches.len();
303 self.selected = self.search_matches[self.search_match_index];
304 true
305 }
306
307 pub fn search_prev(&mut self) -> bool {
309 if self.search_matches.is_empty() {
310 return false;
311 }
312 self.search_match_index = if self.search_match_index == 0 {
313 self.search_matches.len() - 1
314 } else {
315 self.search_match_index - 1
316 };
317 self.selected = self.search_matches[self.search_match_index];
318 true
319 }
320
321 pub fn has_search_query(&self) -> bool {
323 !self.search_query.is_empty()
324 }
325
326 fn navigate_filtered<F>(&mut self, advance: F) -> bool
329 where
330 F: FnOnce(usize, usize) -> usize,
331 {
332 if !self.has_search_query() {
333 return false;
334 }
335 if self.search_matches.is_empty() {
336 return true;
338 }
339
340 let max_match_index = self.search_matches.len() - 1;
341 self.search_match_index =
342 advance(self.search_match_index, max_match_index).min(max_match_index);
343 self.selected = self.search_matches[self.search_match_index];
344 true
345 }
346
347 pub fn search_match_count(&self) -> usize {
349 self.search_matches.len()
350 }
351
352 pub fn search_match_position(&self) -> Option<(usize, usize)> {
354 if self.search_matches.is_empty() {
355 None
356 } else {
357 Some((self.search_match_index + 1, self.search_matches.len()))
358 }
359 }
360
361 pub fn is_search_match(&self, row_index: usize) -> bool {
363 self.search_matches.binary_search(&row_index).is_ok()
364 }
365
366 fn rebuild_search_matches(&mut self) {
367 self.search_matches.clear();
368 self.search_match_index = 0;
369
370 let query = self.search_query.trim().to_ascii_lowercase();
371 if query.is_empty() {
372 return;
373 }
374
375 for (idx, entry) in self.entries.iter().enumerate() {
376 let name = entry.name.to_ascii_lowercase();
377 let params = entry.params.to_ascii_lowercase();
378 let params_detail = entry.params_detail.to_ascii_lowercase();
379 if name.contains(&query) || params.contains(&query) || params_detail.contains(&query) {
380 self.search_matches.push(idx);
382 }
383 }
384
385 if self.search_matches.is_empty() {
386 return;
387 }
388
389 if let Some(position) = self
390 .search_matches
391 .iter()
392 .position(|&idx| idx == self.selected)
393 {
394 self.search_match_index = position;
395 } else {
396 self.search_match_index = 0;
397 self.selected = self.search_matches[0];
398 }
399 }
400
401 fn sync_search_index_from_selection(&mut self) {
402 if self.search_matches.is_empty() {
403 return;
404 }
405 if let Some(position) = self
406 .search_matches
407 .iter()
408 .position(|&idx| idx == self.selected)
409 {
410 self.search_match_index = position;
411 } else {
412 self.search_match_index = self.search_match_index.min(self.search_matches.len() - 1);
413 self.selected = self.search_matches[self.search_match_index];
414 }
415 }
416}
417
418#[derive(Debug, Default)]
437pub struct DebugTableBuilder {
438 rows: Vec<DebugTableRow>,
439 cell_preview: Option<CellPreview>,
440}
441
442impl DebugTableBuilder {
443 pub fn new() -> Self {
445 Self::default()
446 }
447
448 pub fn section(mut self, title: impl Into<String>) -> Self {
450 self.rows.push(DebugTableRow::Section(title.into()));
451 self
452 }
453
454 pub fn entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
456 self.rows.push(DebugTableRow::Entry {
457 key: key.into(),
458 value: value.into(),
459 });
460 self
461 }
462
463 pub fn push_section(&mut self, title: impl Into<String>) {
465 self.rows.push(DebugTableRow::Section(title.into()));
466 }
467
468 pub fn push_entry(&mut self, key: impl Into<String>, value: impl Into<String>) {
470 self.rows.push(DebugTableRow::Entry {
471 key: key.into(),
472 value: value.into(),
473 });
474 }
475
476 pub fn cell_preview(mut self, preview: CellPreview) -> Self {
478 self.cell_preview = Some(preview);
479 self
480 }
481
482 pub fn set_cell_preview(&mut self, preview: CellPreview) {
484 self.cell_preview = Some(preview);
485 }
486
487 pub fn finish(self, title: impl Into<String>) -> DebugTableOverlay {
489 DebugTableOverlay {
490 title: title.into(),
491 rows: self.rows,
492 cell_preview: self.cell_preview,
493 }
494 }
495
496 pub fn finish_inspect(self, title: impl Into<String>) -> DebugOverlay {
498 DebugOverlay::Inspect(self.finish(title))
499 }
500
501 pub fn finish_state(self, title: impl Into<String>) -> DebugOverlay {
503 DebugOverlay::State(self.finish(title))
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510
511 #[test]
512 fn test_builder_basic() {
513 let table = DebugTableBuilder::new()
514 .section("Test")
515 .entry("key1", "value1")
516 .entry("key2", "value2")
517 .finish("Test Table");
518
519 assert_eq!(table.title, "Test Table");
520 assert_eq!(table.rows.len(), 3);
521 assert!(table.cell_preview.is_none());
522 }
523
524 #[test]
525 fn test_builder_multiple_sections() {
526 let table = DebugTableBuilder::new()
527 .section("Section 1")
528 .entry("a", "1")
529 .section("Section 2")
530 .entry("b", "2")
531 .finish("Multi-Section");
532
533 assert_eq!(table.rows.len(), 4);
534
535 match &table.rows[0] {
536 DebugTableRow::Section(s) => assert_eq!(s, "Section 1"),
537 _ => panic!("Expected section"),
538 }
539 match &table.rows[2] {
540 DebugTableRow::Section(s) => assert_eq!(s, "Section 2"),
541 _ => panic!("Expected section"),
542 }
543 }
544
545 #[test]
546 fn test_overlay_kinds() {
547 let table = DebugTableBuilder::new().finish("Test");
548
549 let inspect = DebugOverlay::Inspect(table.clone());
550 assert_eq!(inspect.kind(), "inspect");
551 assert!(inspect.table().is_some());
552 assert!(inspect.action_log().is_none());
553
554 let state = DebugOverlay::State(table);
555 assert_eq!(state.kind(), "state");
556
557 let action_log = ActionLogOverlay {
558 title: "Test".to_string(),
559 entries: vec![],
560 selected: 0,
561 scroll_offset: 0,
562 search_query: String::new(),
563 search_matches: vec![],
564 search_match_index: 0,
565 search_input_active: false,
566 };
567 let log_overlay = DebugOverlay::ActionLog(action_log);
568 assert_eq!(log_overlay.kind(), "action_log");
569 assert!(log_overlay.table().is_none());
570 assert!(log_overlay.action_log().is_some());
571 }
572
573 #[test]
574 fn test_action_log_overlay_scrolling() {
575 let mut overlay = ActionLogOverlay {
576 title: "Test".to_string(),
577 entries: vec![
578 ActionLogDisplayEntry {
579 sequence: 0,
580 name: "A".to_string(),
581 params: "".to_string(),
582 params_detail: "".to_string(),
583 elapsed: "0ms".to_string(),
584 },
585 ActionLogDisplayEntry {
586 sequence: 1,
587 name: "B".to_string(),
588 params: "x: 1".to_string(),
589 params_detail: "x: 1".to_string(),
590 elapsed: "1ms".to_string(),
591 },
592 ActionLogDisplayEntry {
593 sequence: 2,
594 name: "C".to_string(),
595 params: "y: 2".to_string(),
596 params_detail: "y: 2".to_string(),
597 elapsed: "2ms".to_string(),
598 },
599 ],
600 selected: 0,
601 scroll_offset: 0,
602 search_query: String::new(),
603 search_matches: vec![],
604 search_match_index: 0,
605 search_input_active: false,
606 };
607
608 assert_eq!(overlay.selected, 0);
609
610 overlay.scroll_down();
611 assert_eq!(overlay.selected, 1);
612
613 overlay.scroll_down();
614 assert_eq!(overlay.selected, 2);
615
616 overlay.scroll_down(); assert_eq!(overlay.selected, 2);
618
619 overlay.scroll_up();
620 assert_eq!(overlay.selected, 1);
621
622 overlay.scroll_to_top();
623 assert_eq!(overlay.selected, 0);
624
625 overlay.scroll_to_bottom();
626 assert_eq!(overlay.selected, 2);
627 }
628
629 #[test]
630 fn test_action_log_overlay_search_query_and_navigation() {
631 let mut overlay = ActionLogOverlay {
632 title: "Test".to_string(),
633 entries: vec![
634 ActionLogDisplayEntry {
635 sequence: 10,
636 name: "SearchStart".to_string(),
637 params: "query: \"foo\"".to_string(),
638 params_detail: "query: \"foo\"".to_string(),
639 elapsed: "0ms".to_string(),
640 },
641 ActionLogDisplayEntry {
642 sequence: 11,
643 name: "SearchSubmit".to_string(),
644 params: "query: \"foo\"".to_string(),
645 params_detail: "query: \"foo\"".to_string(),
646 elapsed: "1ms".to_string(),
647 },
648 ActionLogDisplayEntry {
649 sequence: 12,
650 name: "Connect".to_string(),
651 params: "host: \"localhost\"".to_string(),
652 params_detail: "host: \"localhost\"".to_string(),
653 elapsed: "2ms".to_string(),
654 },
655 ],
656 selected: 0,
657 scroll_offset: 0,
658 search_query: String::new(),
659 search_matches: vec![],
660 search_match_index: 0,
661 search_input_active: false,
662 };
663
664 overlay.set_search_query("search");
665 assert!(overlay.has_search_query());
666 assert_eq!(overlay.search_match_count(), 2);
667 assert_eq!(overlay.selected, 0);
668 assert_eq!(overlay.search_match_position(), Some((1, 2)));
669
670 assert!(overlay.search_next());
671 assert_eq!(overlay.selected, 1);
672 assert_eq!(overlay.search_match_position(), Some((2, 2)));
673
674 assert!(overlay.search_next());
675 assert_eq!(overlay.selected, 0);
676 assert_eq!(overlay.search_match_position(), Some((1, 2)));
677
678 assert!(overlay.search_prev());
679 assert_eq!(overlay.selected, 1);
680 assert_eq!(overlay.search_match_position(), Some((2, 2)));
681 assert_eq!(overlay.search_matches, vec![0, 1]);
682 }
683
684 #[test]
685 fn test_action_log_overlay_search_edge_cases() {
686 let mut overlay = ActionLogOverlay {
687 title: "Test".to_string(),
688 entries: vec![ActionLogDisplayEntry {
689 sequence: 0,
690 name: "Connect".to_string(),
691 params: "host: \"example\"".to_string(),
692 params_detail: "host: \"example\"".to_string(),
693 elapsed: "0ms".to_string(),
694 }],
695 selected: 0,
696 scroll_offset: 0,
697 search_query: String::new(),
698 search_matches: vec![],
699 search_match_index: 0,
700 search_input_active: false,
701 };
702
703 overlay.set_search_query("missing");
704 assert_eq!(overlay.search_match_count(), 0);
705 assert!(!overlay.search_next());
706 assert!(!overlay.search_prev());
707 assert_eq!(overlay.search_match_position(), None);
708
709 overlay.set_search_query("connect");
710 assert_eq!(overlay.search_match_count(), 1);
711 assert_eq!(overlay.search_match_position(), Some((1, 1)));
712 assert!(overlay.search_next());
713 assert_eq!(overlay.search_match_position(), Some((1, 1)));
714
715 assert!(overlay.pop_search_char());
716 assert!(overlay.has_search_query());
717 overlay.clear_search_query();
718 assert!(!overlay.has_search_query());
719 assert_eq!(overlay.search_match_count(), 0);
720 }
721
722 #[test]
723 fn test_action_log_overlay_scroll_respects_filter() {
724 let mut overlay = ActionLogOverlay {
725 title: "Test".to_string(),
726 entries: vec![
727 ActionLogDisplayEntry {
728 sequence: 0,
729 name: "SearchStart".to_string(),
730 params: "".to_string(),
731 params_detail: "".to_string(),
732 elapsed: "0ms".to_string(),
733 },
734 ActionLogDisplayEntry {
735 sequence: 1,
736 name: "Connect".to_string(),
737 params: "".to_string(),
738 params_detail: "".to_string(),
739 elapsed: "1ms".to_string(),
740 },
741 ActionLogDisplayEntry {
742 sequence: 2,
743 name: "SearchSubmit".to_string(),
744 params: "".to_string(),
745 params_detail: "".to_string(),
746 elapsed: "2ms".to_string(),
747 },
748 ],
749 selected: 0,
750 scroll_offset: 0,
751 search_query: String::new(),
752 search_matches: vec![],
753 search_match_index: 0,
754 search_input_active: false,
755 };
756
757 overlay.set_search_query("search");
758 assert_eq!(overlay.search_matches, vec![0, 2]);
759 assert_eq!(overlay.selected, 0);
760
761 overlay.scroll_down();
762 assert_eq!(overlay.selected, 2);
763 overlay.scroll_up();
764 assert_eq!(overlay.selected, 0);
765 }
766}