1use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
6use ratatui::buffer::Buffer;
7use ratatui::layout::Rect;
8use ratatui::style::Style;
9use ratatui::widgets::{Block, Borders, Clear};
10use ratatui::Frame;
11use std::marker::PhantomData;
12
13use super::action_logger::ActionLog;
14use super::actions::{DebugAction, DebugSideEffect};
15use super::cell::inspect_cell;
16use super::config::{
17 default_debug_keybindings, default_debug_keybindings_with_toggle, DebugConfig, DebugStyle,
18 StatusItem,
19};
20use super::state::DebugState;
21use super::table::{ActionLogOverlay, DebugOverlay, DebugTableBuilder, DebugTableOverlay};
22use super::widgets::{
23 dim_buffer, paint_snapshot, ActionLogWidget, BannerItem, CellPreviewWidget, DebugBanner,
24 DebugTableWidget,
25};
26use super::{DebugFreeze, SimpleDebugContext};
27use crate::keybindings::{format_key_for_display, BindingContext};
28
29pub struct DebugLayer<A, C: BindingContext> {
59 freeze: DebugFreeze<A>,
61 config: DebugConfig<C>,
63}
64
65impl<A, C: BindingContext> std::fmt::Debug for DebugLayer<A, C> {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct("DebugLayer")
68 .field("enabled", &self.freeze.enabled)
69 .field("has_snapshot", &self.freeze.snapshot.is_some())
70 .field("queued_actions", &self.freeze.queued_actions.len())
71 .finish()
72 }
73}
74
75impl<A, C: BindingContext> DebugLayer<A, C> {
76 pub fn new(config: DebugConfig<C>) -> Self {
78 Self {
79 freeze: DebugFreeze::new(),
80 config,
81 }
82 }
83
84 pub fn is_enabled(&self) -> bool {
86 self.freeze.enabled
87 }
88
89 pub fn freeze(&self) -> &DebugFreeze<A> {
91 &self.freeze
92 }
93
94 pub fn freeze_mut(&mut self) -> &mut DebugFreeze<A> {
96 &mut self.freeze
97 }
98
99 pub fn config(&self) -> &DebugConfig<C> {
101 &self.config
102 }
103
104 pub fn config_mut(&mut self) -> &mut DebugConfig<C> {
106 &mut self.config
107 }
108
109 pub fn render<F>(&mut self, frame: &mut Frame, render_fn: F)
129 where
130 F: FnOnce(&mut Frame, Rect),
131 {
132 let screen = frame.area();
133
134 if !self.freeze.enabled {
135 render_fn(frame, screen);
137 return;
138 }
139
140 let (app_area, banner_area) = self.split_for_banner(screen);
142
143 if self.freeze.pending_capture || self.freeze.snapshot.is_none() {
144 render_fn(frame, app_area);
146 let buffer_clone = frame.buffer_mut().clone();
148 self.freeze.capture(&buffer_clone);
149 } else if let Some(ref snapshot) = self.freeze.snapshot {
150 paint_snapshot(frame, snapshot);
152 }
153
154 self.render_debug_overlay(frame, app_area, banner_area);
156 }
157
158 pub fn split_area(&self, area: Rect) -> (Rect, Rect) {
180 if !self.freeze.enabled {
181 return (area, Rect::ZERO);
182 }
183 self.split_for_banner(area)
184 }
185
186 pub fn render_overlay(&self, frame: &mut Frame, app_area: Rect) {
190 if !self.freeze.enabled {
191 return;
192 }
193
194 dim_buffer(frame.buffer_mut(), self.config.style.dim_factor);
196
197 if let Some(ref overlay) = self.freeze.overlay {
199 match overlay {
200 DebugOverlay::Inspect(table) | DebugOverlay::State(table) => {
201 self.render_table_modal(frame, app_area, table);
202 }
203 DebugOverlay::ActionLog(log) => {
204 self.render_action_log_modal(frame, app_area, log);
205 }
206 }
207 }
208 }
209
210 pub fn render_banner(&self, frame: &mut Frame, banner_area: Rect) {
214 if !self.freeze.enabled || banner_area.height == 0 {
215 return;
216 }
217
218 let style = &self.config.style;
219 let mut banner = DebugBanner::new()
220 .title("DEBUG")
221 .title_style(style.title_style)
222 .label_style(style.label_style)
223 .background(style.banner_bg);
224
225 let keys = &style.key_styles;
227 self.add_banner_item(&mut banner, DebugAction::CMD_TOGGLE, "resume", keys.toggle);
228 self.add_banner_item(
229 &mut banner,
230 DebugAction::CMD_TOGGLE_ACTION_LOG,
231 "actions",
232 keys.actions,
233 );
234 self.add_banner_item(
235 &mut banner,
236 DebugAction::CMD_TOGGLE_STATE,
237 "state",
238 keys.state,
239 );
240 self.add_banner_item(&mut banner, DebugAction::CMD_COPY_FRAME, "copy", keys.copy);
241
242 if self.freeze.mouse_capture_enabled {
243 banner = banner.item(BannerItem::new("click", "inspect", keys.mouse));
244 } else {
245 self.add_banner_item(
246 &mut banner,
247 DebugAction::CMD_TOGGLE_MOUSE,
248 "mouse",
249 keys.mouse,
250 );
251 }
252
253 if let Some(ref msg) = self.freeze.message {
255 banner = banner.item(BannerItem::new("", msg, style.value_style));
256 }
257
258 frame.render_widget(banner, banner_area);
263 }
264
265 pub fn render_banner_with_status(
270 &self,
271 frame: &mut Frame,
272 banner_area: Rect,
273 status_items: &[(&str, &str)],
274 ) {
275 if !self.freeze.enabled || banner_area.height == 0 {
276 return;
277 }
278
279 let style = &self.config.style;
280 let mut banner = DebugBanner::new()
281 .title("DEBUG")
282 .title_style(style.title_style)
283 .label_style(style.label_style)
284 .background(style.banner_bg);
285
286 let keys = &style.key_styles;
288 self.add_banner_item(&mut banner, DebugAction::CMD_TOGGLE, "resume", keys.toggle);
289 self.add_banner_item(
290 &mut banner,
291 DebugAction::CMD_TOGGLE_ACTION_LOG,
292 "actions",
293 keys.actions,
294 );
295 self.add_banner_item(
296 &mut banner,
297 DebugAction::CMD_TOGGLE_STATE,
298 "state",
299 keys.state,
300 );
301 self.add_banner_item(&mut banner, DebugAction::CMD_COPY_FRAME, "copy", keys.copy);
302
303 if self.freeze.mouse_capture_enabled {
304 banner = banner.item(BannerItem::new("click", "inspect", keys.mouse));
305 } else {
306 self.add_banner_item(
307 &mut banner,
308 DebugAction::CMD_TOGGLE_MOUSE,
309 "mouse",
310 keys.mouse,
311 );
312 }
313
314 if let Some(ref msg) = self.freeze.message {
316 banner = banner.item(BannerItem::new("", msg, style.value_style));
317 }
318
319 for (label, value) in status_items {
321 banner = banner.item(BannerItem::new(label, value, style.value_style));
322 }
323
324 frame.render_widget(banner, banner_area);
325 }
326
327 pub fn handle_action(&mut self, action: DebugAction) -> Option<DebugSideEffect<A>> {
331 match action {
332 DebugAction::Toggle => {
333 if self.freeze.enabled {
334 let queued = self.freeze.take_queued();
335 self.freeze.disable();
336 if queued.is_empty() {
337 None
338 } else {
339 Some(DebugSideEffect::ProcessQueuedActions(queued))
340 }
341 } else {
342 self.freeze.enable();
343 None
344 }
345 }
346 DebugAction::CopyFrame => {
347 let text = self.freeze.snapshot_text.clone();
348 self.freeze.set_message("Copied to clipboard");
349 Some(DebugSideEffect::CopyToClipboard(text))
350 }
351 DebugAction::ToggleState => {
352 if matches!(self.freeze.overlay, Some(DebugOverlay::State(_))) {
354 self.freeze.clear_overlay();
355 } else {
356 let table = DebugTableBuilder::new()
359 .section("State")
360 .entry("hint", "Call show_state_overlay() with your state")
361 .finish("Application State");
362 self.freeze.set_overlay(DebugOverlay::State(table));
363 }
364 None
365 }
366 DebugAction::ToggleActionLog => {
367 if matches!(self.freeze.overlay, Some(DebugOverlay::ActionLog(_))) {
369 self.freeze.clear_overlay();
370 } else {
371 let overlay = ActionLogOverlay {
374 title: "Action Log".to_string(),
375 entries: vec![],
376 selected: 0,
377 scroll_offset: 0,
378 };
379 self.freeze
380 .set_message("Call show_action_log() with your ActionLog");
381 self.freeze.set_overlay(DebugOverlay::ActionLog(overlay));
382 }
383 None
384 }
385 DebugAction::ActionLogScrollUp => {
386 if let Some(DebugOverlay::ActionLog(ref mut log)) = self.freeze.overlay {
387 log.scroll_up();
388 }
389 None
390 }
391 DebugAction::ActionLogScrollDown => {
392 if let Some(DebugOverlay::ActionLog(ref mut log)) = self.freeze.overlay {
393 log.scroll_down();
394 }
395 None
396 }
397 DebugAction::ActionLogScrollTop => {
398 if let Some(DebugOverlay::ActionLog(ref mut log)) = self.freeze.overlay {
399 log.scroll_to_top();
400 }
401 None
402 }
403 DebugAction::ActionLogScrollBottom => {
404 if let Some(DebugOverlay::ActionLog(ref mut log)) = self.freeze.overlay {
405 log.scroll_to_bottom();
406 }
407 None
408 }
409 DebugAction::ToggleMouseCapture => {
410 self.freeze.toggle_mouse_capture();
411 if self.freeze.mouse_capture_enabled {
412 Some(DebugSideEffect::EnableMouseCapture)
413 } else {
414 Some(DebugSideEffect::DisableMouseCapture)
415 }
416 }
417 DebugAction::InspectCell { column, row } => {
418 if let Some(ref snapshot) = self.freeze.snapshot {
419 let overlay = self.build_inspect_overlay(column, row, snapshot);
420 self.freeze.set_overlay(DebugOverlay::Inspect(overlay));
421 }
422 self.freeze.mouse_capture_enabled = false;
423 Some(DebugSideEffect::DisableMouseCapture)
424 }
425 DebugAction::CloseOverlay => {
426 self.freeze.clear_overlay();
427 None
428 }
429 DebugAction::RequestCapture => {
430 self.freeze.request_capture();
431 None
432 }
433 }
434 }
435
436 pub fn handle_mouse(&mut self, mouse: MouseEvent) -> Option<DebugSideEffect<A>> {
440 if !self.freeze.enabled {
441 return None;
442 }
443
444 if matches!(
446 mouse.kind,
447 MouseEventKind::ScrollUp | MouseEventKind::ScrollDown
448 ) {
449 return None;
450 }
451
452 if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left))
454 && self.freeze.mouse_capture_enabled
455 {
456 return self.handle_action(DebugAction::InspectCell {
457 column: mouse.column,
458 row: mouse.row,
459 });
460 }
461
462 None
463 }
464
465 pub fn show_state_overlay<S: DebugState>(&mut self, state: &S) {
467 let table = state.build_debug_table("Application State");
468 self.freeze.set_overlay(DebugOverlay::State(table));
469 }
470
471 pub fn show_state_overlay_with_title<S: DebugState>(&mut self, state: &S, title: &str) {
473 let table = state.build_debug_table(title);
474 self.freeze.set_overlay(DebugOverlay::State(table));
475 }
476
477 pub fn show_action_log(&mut self, log: &ActionLog) {
490 let overlay = ActionLogOverlay::from_log(log, "Action Log");
491 self.freeze.set_overlay(DebugOverlay::ActionLog(overlay));
492 }
493
494 pub fn show_action_log_with_title(&mut self, log: &ActionLog, title: &str) {
496 let overlay = ActionLogOverlay::from_log(log, title);
497 self.freeze.set_overlay(DebugOverlay::ActionLog(overlay));
498 }
499
500 pub fn queue_action(&mut self, action: A) {
502 self.freeze.queue(action);
503 }
504
505 fn split_for_banner(&self, area: Rect) -> (Rect, Rect) {
508 let banner_height = 1;
509 let app_area = Rect {
510 height: area.height.saturating_sub(banner_height),
511 ..area
512 };
513 let banner_area = Rect {
514 y: area.y.saturating_add(app_area.height),
515 height: banner_height.min(area.height),
516 ..area
517 };
518 (app_area, banner_area)
519 }
520
521 fn render_debug_overlay(&self, frame: &mut Frame, app_area: Rect, banner_area: Rect) {
522 dim_buffer(frame.buffer_mut(), self.config.style.dim_factor);
524
525 if let Some(ref overlay) = self.freeze.overlay {
527 match overlay {
528 DebugOverlay::Inspect(table) | DebugOverlay::State(table) => {
529 self.render_table_modal(frame, app_area, table);
530 }
531 DebugOverlay::ActionLog(log) => {
532 self.render_action_log_modal(frame, app_area, log);
533 }
534 }
535 }
536
537 self.render_banner(frame, banner_area);
539 }
540
541 fn render_table_modal(&self, frame: &mut Frame, app_area: Rect, table: &DebugTableOverlay) {
542 let modal_width = (app_area.width * 80 / 100)
544 .clamp(30, 120)
545 .min(app_area.width);
546 let modal_height = (app_area.height * 60 / 100)
547 .clamp(8, 40)
548 .min(app_area.height);
549
550 let modal_x = app_area.x + (app_area.width.saturating_sub(modal_width)) / 2;
552 let modal_y = app_area.y + (app_area.height.saturating_sub(modal_height)) / 2;
553
554 let modal_area = Rect::new(modal_x, modal_y, modal_width, modal_height);
555
556 frame.render_widget(Clear, modal_area);
558
559 let block = Block::default()
560 .borders(Borders::ALL)
561 .title(format!(" {} ", table.title))
562 .style(self.config.style.banner_bg);
563
564 let inner = block.inner(modal_area);
565 frame.render_widget(block, modal_area);
566
567 if let Some(ref preview) = table.cell_preview {
569 if inner.height > 3 {
570 let preview_height = 2u16; let preview_area = Rect {
572 x: inner.x,
573 y: inner.y,
574 width: inner.width,
575 height: 1,
576 };
577 let table_area = Rect {
578 x: inner.x,
579 y: inner.y.saturating_add(preview_height),
580 width: inner.width,
581 height: inner.height.saturating_sub(preview_height),
582 };
583
584 let preview_widget = CellPreviewWidget::new(preview)
586 .label_style(Style::default().fg(DebugStyle::text_secondary()))
587 .value_style(Style::default().fg(DebugStyle::text_primary()));
588 frame.render_widget(preview_widget, preview_area);
589
590 let table_widget = DebugTableWidget::new(table);
592 frame.render_widget(table_widget, table_area);
593 return;
594 }
595 }
596
597 let table_widget = DebugTableWidget::new(table);
599 frame.render_widget(table_widget, inner);
600 }
601
602 fn render_action_log_modal(&self, frame: &mut Frame, app_area: Rect, log: &ActionLogOverlay) {
603 let modal_width = (app_area.width * 90 / 100)
605 .clamp(40, 140)
606 .min(app_area.width);
607 let modal_height = (app_area.height * 70 / 100)
608 .clamp(10, 50)
609 .min(app_area.height);
610
611 let modal_x = app_area.x + (app_area.width.saturating_sub(modal_width)) / 2;
613 let modal_y = app_area.y + (app_area.height.saturating_sub(modal_height)) / 2;
614
615 let modal_area = Rect::new(modal_x, modal_y, modal_width, modal_height);
616
617 frame.render_widget(Clear, modal_area);
619
620 let entry_count = log.entries.len();
621 let title = if entry_count > 0 {
622 format!(" {} ({} entries) ", log.title, entry_count)
623 } else {
624 format!(" {} (empty) ", log.title)
625 };
626
627 let block = Block::default()
628 .borders(Borders::ALL)
629 .title(title)
630 .style(self.config.style.banner_bg);
631
632 let inner = block.inner(modal_area);
633 frame.render_widget(block, modal_area);
634
635 let widget = ActionLogWidget::new(log);
637 frame.render_widget(widget, inner);
638 }
639
640 fn add_banner_item(
641 &self,
642 banner: &mut DebugBanner<'_>,
643 command: &str,
644 label: &'static str,
645 style: Style,
646 ) {
647 if let Some(key) = self
648 .config
649 .keybindings
650 .get_first_keybinding(command, self.config.debug_context)
651 {
652 let formatted = format_key_for_display(&key);
653 let key_str: &'static str = Box::leak(formatted.into_boxed_str());
655 *banner = std::mem::take(banner).item(BannerItem::new(key_str, label, style));
656 }
657 }
658
659 fn build_inspect_overlay(&self, column: u16, row: u16, snapshot: &Buffer) -> DebugTableOverlay {
660 let mut builder = DebugTableBuilder::new();
661
662 builder.push_section("Position");
663 builder.push_entry("column", column.to_string());
664 builder.push_entry("row", row.to_string());
665
666 if let Some(preview) = inspect_cell(snapshot, column, row) {
667 builder.set_cell_preview(preview);
668 }
669
670 builder.finish(format!("Inspect ({column}, {row})"))
671 }
672}
673
674pub struct DebugLayerBuilder<A, C: BindingContext> {
676 config: DebugConfig<C>,
677 _marker: PhantomData<A>,
678}
679
680impl<A, C: BindingContext> DebugLayerBuilder<A, C> {
681 pub fn new(config: DebugConfig<C>) -> Self {
683 Self {
684 config,
685 _marker: PhantomData,
686 }
687 }
688
689 pub fn with_status_provider<F>(mut self, provider: F) -> Self
691 where
692 F: Fn() -> Vec<StatusItem> + Send + Sync + 'static,
693 {
694 self.config = self.config.with_status_provider(provider);
695 self
696 }
697
698 pub fn build(self) -> DebugLayer<A, C> {
700 DebugLayer::new(self.config)
701 }
702}
703
704impl<A> DebugLayer<A, SimpleDebugContext> {
709 pub fn simple() -> Self {
741 let keybindings = default_debug_keybindings();
742 let config = DebugConfig::new(keybindings, SimpleDebugContext::Debug);
743 Self::new(config)
744 }
745
746 pub fn simple_with_toggle_key(keys: &[&str]) -> Self {
763 let keybindings = default_debug_keybindings_with_toggle(keys);
764 let config = DebugConfig::new(keybindings, SimpleDebugContext::Debug);
765 Self::new(config)
766 }
767}
768
769#[cfg(test)]
770mod tests {
771 use super::*;
772 use crate::keybindings::Keybindings;
773
774 #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
776 enum TestContext {
777 Debug,
778 }
779
780 impl BindingContext for TestContext {
781 fn name(&self) -> &'static str {
782 "debug"
783 }
784 fn from_name(name: &str) -> Option<Self> {
785 (name == "debug").then_some(TestContext::Debug)
786 }
787 fn all() -> &'static [Self] {
788 &[TestContext::Debug]
789 }
790 }
791
792 #[derive(Debug, Clone)]
793 enum TestAction {
794 Foo,
795 Bar,
796 }
797
798 fn make_layer() -> DebugLayer<TestAction, TestContext> {
799 let config = DebugConfig::new(Keybindings::new(), TestContext::Debug);
800 DebugLayer::new(config)
801 }
802
803 #[test]
804 fn test_debug_layer_creation() {
805 let layer = make_layer();
806 assert!(!layer.is_enabled());
807 assert!(layer.freeze().snapshot.is_none());
808 }
809
810 #[test]
811 fn test_toggle() {
812 let mut layer = make_layer();
813
814 let effect = layer.handle_action(DebugAction::Toggle);
816 assert!(effect.is_none());
817 assert!(layer.is_enabled());
818
819 let effect = layer.handle_action(DebugAction::Toggle);
821 assert!(effect.is_none()); assert!(!layer.is_enabled());
823 }
824
825 #[test]
826 fn test_queued_actions_returned_on_disable() {
827 let mut layer = make_layer();
828
829 layer.handle_action(DebugAction::Toggle); layer.queue_action(TestAction::Foo);
831 layer.queue_action(TestAction::Bar);
832
833 let effect = layer.handle_action(DebugAction::Toggle); match effect {
836 Some(DebugSideEffect::ProcessQueuedActions(actions)) => {
837 assert_eq!(actions.len(), 2);
838 }
839 _ => panic!("Expected ProcessQueuedActions"),
840 }
841 }
842
843 #[test]
844 fn test_split_area() {
845 let layer = make_layer();
846
847 let area = Rect::new(0, 0, 80, 24);
849 let (app, banner) = layer.split_area(area);
850 assert_eq!(app, area);
851 assert_eq!(banner, Rect::ZERO);
852 }
853
854 #[test]
855 fn test_split_area_enabled() {
856 let mut layer = make_layer();
857 layer.handle_action(DebugAction::Toggle);
858
859 let area = Rect::new(0, 0, 80, 24);
860 let (app, banner) = layer.split_area(area);
861
862 assert_eq!(app.height, 23);
863 assert_eq!(banner.height, 1);
864 assert_eq!(banner.y, 23);
865 }
866
867 #[test]
868 fn test_mouse_capture_toggle() {
869 let mut layer = make_layer();
870 layer.handle_action(DebugAction::Toggle); let effect = layer.handle_action(DebugAction::ToggleMouseCapture);
873 assert!(matches!(effect, Some(DebugSideEffect::EnableMouseCapture)));
874 assert!(layer.freeze().mouse_capture_enabled);
875
876 let effect = layer.handle_action(DebugAction::ToggleMouseCapture);
877 assert!(matches!(effect, Some(DebugSideEffect::DisableMouseCapture)));
878 assert!(!layer.freeze().mouse_capture_enabled);
879 }
880
881 #[test]
883 fn test_simple_creation() {
884 let layer: DebugLayer<TestAction, SimpleDebugContext> = DebugLayer::simple();
885 assert!(!layer.is_enabled());
886 assert!(layer.freeze().snapshot.is_none());
887
888 assert_eq!(layer.config().debug_context, SimpleDebugContext::Debug);
890 }
891
892 #[test]
893 fn test_simple_toggle_works() {
894 let mut layer: DebugLayer<TestAction, SimpleDebugContext> = DebugLayer::simple();
895
896 layer.handle_action(DebugAction::Toggle);
898 assert!(layer.is_enabled());
899
900 layer.handle_action(DebugAction::Toggle);
902 assert!(!layer.is_enabled());
903 }
904
905 #[test]
906 fn test_simple_with_toggle_key() {
907 let layer: DebugLayer<TestAction, SimpleDebugContext> =
908 DebugLayer::simple_with_toggle_key(&["F11"]);
909
910 assert!(!layer.is_enabled());
911
912 let keybindings = &layer.config().keybindings;
914 let toggle_keys = keybindings.get_context_bindings(SimpleDebugContext::Debug);
915 assert!(toggle_keys.is_some());
916
917 if let Some(bindings) = toggle_keys {
918 let keys = bindings.get("debug.toggle");
919 assert!(keys.is_some());
920 assert!(keys.unwrap().contains(&"F11".to_string()));
921 }
922 }
923
924 #[test]
925 fn test_simple_has_default_keybindings() {
926 let layer: DebugLayer<TestAction, SimpleDebugContext> = DebugLayer::simple();
927 let keybindings = &layer.config().keybindings;
928
929 let bindings = keybindings
931 .get_context_bindings(SimpleDebugContext::Debug)
932 .unwrap();
933
934 assert!(bindings.contains_key("debug.toggle"));
935 assert!(bindings.contains_key("debug.state"));
936 assert!(bindings.contains_key("debug.copy"));
937 assert!(bindings.contains_key("debug.mouse"));
938
939 let toggle_keys = bindings.get("debug.toggle").unwrap();
941 assert!(toggle_keys.contains(&"F12".to_string()));
942 assert!(toggle_keys.contains(&"Esc".to_string()));
943 }
944}