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::actions::{DebugAction, DebugSideEffect};
14use super::cell::inspect_cell;
15use super::config::{
16 default_debug_keybindings, default_debug_keybindings_with_toggle, DebugConfig, DebugStyle,
17 StatusItem,
18};
19use super::state::DebugState;
20use super::table::{DebugOverlay, DebugTableBuilder, DebugTableOverlay};
21use super::widgets::{
22 dim_buffer, paint_snapshot, BannerItem, CellPreviewWidget, DebugBanner, DebugTableWidget,
23};
24use super::{DebugFreeze, SimpleDebugContext};
25use crate::keybindings::{format_key_for_display, BindingContext};
26
27pub struct DebugLayer<A, C: BindingContext> {
57 freeze: DebugFreeze<A>,
59 config: DebugConfig<C>,
61}
62
63impl<A, C: BindingContext> std::fmt::Debug for DebugLayer<A, C> {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 f.debug_struct("DebugLayer")
66 .field("enabled", &self.freeze.enabled)
67 .field("has_snapshot", &self.freeze.snapshot.is_some())
68 .field("queued_actions", &self.freeze.queued_actions.len())
69 .finish()
70 }
71}
72
73impl<A, C: BindingContext> DebugLayer<A, C> {
74 pub fn new(config: DebugConfig<C>) -> Self {
76 Self {
77 freeze: DebugFreeze::new(),
78 config,
79 }
80 }
81
82 pub fn is_enabled(&self) -> bool {
84 self.freeze.enabled
85 }
86
87 pub fn freeze(&self) -> &DebugFreeze<A> {
89 &self.freeze
90 }
91
92 pub fn freeze_mut(&mut self) -> &mut DebugFreeze<A> {
94 &mut self.freeze
95 }
96
97 pub fn config(&self) -> &DebugConfig<C> {
99 &self.config
100 }
101
102 pub fn config_mut(&mut self) -> &mut DebugConfig<C> {
104 &mut self.config
105 }
106
107 pub fn render<F>(&mut self, frame: &mut Frame, render_fn: F)
127 where
128 F: FnOnce(&mut Frame, Rect),
129 {
130 let screen = frame.area();
131
132 if !self.freeze.enabled {
133 render_fn(frame, screen);
135 return;
136 }
137
138 let (app_area, banner_area) = self.split_for_banner(screen);
140
141 if self.freeze.pending_capture || self.freeze.snapshot.is_none() {
142 render_fn(frame, app_area);
144 let buffer_clone = frame.buffer_mut().clone();
146 self.freeze.capture(&buffer_clone);
147 } else if let Some(ref snapshot) = self.freeze.snapshot {
148 paint_snapshot(frame, snapshot);
150 }
151
152 self.render_debug_overlay(frame, app_area, banner_area);
154 }
155
156 pub fn split_area(&self, area: Rect) -> (Rect, Rect) {
178 if !self.freeze.enabled {
179 return (area, Rect::ZERO);
180 }
181 self.split_for_banner(area)
182 }
183
184 pub fn render_overlay(&self, frame: &mut Frame, app_area: Rect) {
188 if !self.freeze.enabled {
189 return;
190 }
191
192 dim_buffer(frame.buffer_mut(), self.config.style.dim_factor);
194
195 if let Some(ref overlay) = self.freeze.overlay {
197 self.render_modal(frame, app_area, overlay.table());
198 }
199 }
200
201 pub fn render_banner(&self, frame: &mut Frame, banner_area: Rect) {
205 if !self.freeze.enabled || banner_area.height == 0 {
206 return;
207 }
208
209 let style = &self.config.style;
210 let mut banner = DebugBanner::new()
211 .title("DEBUG")
212 .title_style(style.title_style)
213 .label_style(style.label_style)
214 .background(style.banner_bg);
215
216 let keys = &style.key_styles;
218 self.add_banner_item(&mut banner, DebugAction::CMD_TOGGLE, "resume", keys.toggle);
219 self.add_banner_item(
220 &mut banner,
221 DebugAction::CMD_TOGGLE_STATE,
222 "state",
223 keys.state,
224 );
225 self.add_banner_item(&mut banner, DebugAction::CMD_COPY_FRAME, "copy", keys.copy);
226
227 if self.freeze.mouse_capture_enabled {
228 banner = banner.item(BannerItem::new("click", "inspect", keys.mouse));
229 } else {
230 self.add_banner_item(
231 &mut banner,
232 DebugAction::CMD_TOGGLE_MOUSE,
233 "mouse",
234 keys.mouse,
235 );
236 }
237
238 if let Some(ref msg) = self.freeze.message {
240 banner = banner.item(BannerItem::new("", msg, style.value_style));
241 }
242
243 frame.render_widget(banner, banner_area);
248 }
249
250 pub fn render_banner_with_status(
255 &self,
256 frame: &mut Frame,
257 banner_area: Rect,
258 status_items: &[(&str, &str)],
259 ) {
260 if !self.freeze.enabled || banner_area.height == 0 {
261 return;
262 }
263
264 let style = &self.config.style;
265 let mut banner = DebugBanner::new()
266 .title("DEBUG")
267 .title_style(style.title_style)
268 .label_style(style.label_style)
269 .background(style.banner_bg);
270
271 let keys = &style.key_styles;
273 self.add_banner_item(&mut banner, DebugAction::CMD_TOGGLE, "resume", keys.toggle);
274 self.add_banner_item(
275 &mut banner,
276 DebugAction::CMD_TOGGLE_STATE,
277 "state",
278 keys.state,
279 );
280 self.add_banner_item(&mut banner, DebugAction::CMD_COPY_FRAME, "copy", keys.copy);
281
282 if self.freeze.mouse_capture_enabled {
283 banner = banner.item(BannerItem::new("click", "inspect", keys.mouse));
284 } else {
285 self.add_banner_item(
286 &mut banner,
287 DebugAction::CMD_TOGGLE_MOUSE,
288 "mouse",
289 keys.mouse,
290 );
291 }
292
293 if let Some(ref msg) = self.freeze.message {
295 banner = banner.item(BannerItem::new("", msg, style.value_style));
296 }
297
298 for (label, value) in status_items {
300 banner = banner.item(BannerItem::new(label, value, style.value_style));
301 }
302
303 frame.render_widget(banner, banner_area);
304 }
305
306 pub fn handle_action(&mut self, action: DebugAction) -> Option<DebugSideEffect<A>> {
310 match action {
311 DebugAction::Toggle => {
312 if self.freeze.enabled {
313 let queued = self.freeze.take_queued();
314 self.freeze.disable();
315 if queued.is_empty() {
316 None
317 } else {
318 Some(DebugSideEffect::ProcessQueuedActions(queued))
319 }
320 } else {
321 self.freeze.enable();
322 None
323 }
324 }
325 DebugAction::CopyFrame => {
326 let text = self.freeze.snapshot_text.clone();
327 self.freeze.set_message("Copied to clipboard");
328 Some(DebugSideEffect::CopyToClipboard(text))
329 }
330 DebugAction::ToggleState => {
331 if self.freeze.overlay.is_some() {
333 self.freeze.clear_overlay();
334 } else {
335 let table = DebugTableBuilder::new()
338 .section("State")
339 .entry("hint", "Call show_state_overlay() with your state")
340 .finish("Application State");
341 self.freeze.set_overlay(DebugOverlay::State(table));
342 }
343 None
344 }
345 DebugAction::ToggleMouseCapture => {
346 self.freeze.toggle_mouse_capture();
347 if self.freeze.mouse_capture_enabled {
348 Some(DebugSideEffect::EnableMouseCapture)
349 } else {
350 Some(DebugSideEffect::DisableMouseCapture)
351 }
352 }
353 DebugAction::InspectCell { column, row } => {
354 if let Some(ref snapshot) = self.freeze.snapshot {
355 let overlay = self.build_inspect_overlay(column, row, snapshot);
356 self.freeze.set_overlay(DebugOverlay::Inspect(overlay));
357 }
358 self.freeze.mouse_capture_enabled = false;
359 Some(DebugSideEffect::DisableMouseCapture)
360 }
361 DebugAction::CloseOverlay => {
362 self.freeze.clear_overlay();
363 None
364 }
365 DebugAction::RequestCapture => {
366 self.freeze.request_capture();
367 None
368 }
369 }
370 }
371
372 pub fn handle_mouse(&mut self, mouse: MouseEvent) -> Option<DebugSideEffect<A>> {
376 if !self.freeze.enabled {
377 return None;
378 }
379
380 if matches!(
382 mouse.kind,
383 MouseEventKind::ScrollUp | MouseEventKind::ScrollDown
384 ) {
385 return None;
386 }
387
388 if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left))
390 && self.freeze.mouse_capture_enabled
391 {
392 return self.handle_action(DebugAction::InspectCell {
393 column: mouse.column,
394 row: mouse.row,
395 });
396 }
397
398 None
399 }
400
401 pub fn show_state_overlay<S: DebugState>(&mut self, state: &S) {
403 let table = state.build_debug_table("Application State");
404 self.freeze.set_overlay(DebugOverlay::State(table));
405 }
406
407 pub fn show_state_overlay_with_title<S: DebugState>(&mut self, state: &S, title: &str) {
409 let table = state.build_debug_table(title);
410 self.freeze.set_overlay(DebugOverlay::State(table));
411 }
412
413 pub fn queue_action(&mut self, action: A) {
415 self.freeze.queue(action);
416 }
417
418 fn split_for_banner(&self, area: Rect) -> (Rect, Rect) {
421 let banner_height = 1;
422 let app_area = Rect {
423 height: area.height.saturating_sub(banner_height),
424 ..area
425 };
426 let banner_area = Rect {
427 y: area.y.saturating_add(app_area.height),
428 height: banner_height.min(area.height),
429 ..area
430 };
431 (app_area, banner_area)
432 }
433
434 fn render_debug_overlay(&self, frame: &mut Frame, app_area: Rect, banner_area: Rect) {
435 dim_buffer(frame.buffer_mut(), self.config.style.dim_factor);
437
438 if let Some(ref overlay) = self.freeze.overlay {
440 self.render_modal(frame, app_area, overlay.table());
441 }
442
443 self.render_banner(frame, banner_area);
445 }
446
447 fn render_modal(&self, frame: &mut Frame, app_area: Rect, table: &DebugTableOverlay) {
448 let modal_width = (app_area.width * 80 / 100)
450 .clamp(30, 120)
451 .min(app_area.width);
452 let modal_height = (app_area.height * 60 / 100)
453 .clamp(8, 40)
454 .min(app_area.height);
455
456 let modal_x = app_area.x + (app_area.width.saturating_sub(modal_width)) / 2;
458 let modal_y = app_area.y + (app_area.height.saturating_sub(modal_height)) / 2;
459
460 let modal_area = Rect::new(modal_x, modal_y, modal_width, modal_height);
461
462 frame.render_widget(Clear, modal_area);
464
465 let block = Block::default()
466 .borders(Borders::ALL)
467 .title(format!(" {} ", table.title))
468 .style(self.config.style.banner_bg);
469
470 let inner = block.inner(modal_area);
471 frame.render_widget(block, modal_area);
472
473 if let Some(ref preview) = table.cell_preview {
475 if inner.height > 3 {
476 let preview_height = 2u16; let preview_area = Rect {
478 x: inner.x,
479 y: inner.y,
480 width: inner.width,
481 height: 1,
482 };
483 let table_area = Rect {
484 x: inner.x,
485 y: inner.y.saturating_add(preview_height),
486 width: inner.width,
487 height: inner.height.saturating_sub(preview_height),
488 };
489
490 let preview_widget = CellPreviewWidget::new(preview)
492 .label_style(Style::default().fg(DebugStyle::text_secondary()))
493 .value_style(Style::default().fg(DebugStyle::text_primary()));
494 frame.render_widget(preview_widget, preview_area);
495
496 let table_widget = DebugTableWidget::new(table);
498 frame.render_widget(table_widget, table_area);
499 return;
500 }
501 }
502
503 let table_widget = DebugTableWidget::new(table);
505 frame.render_widget(table_widget, inner);
506 }
507
508 fn add_banner_item(
509 &self,
510 banner: &mut DebugBanner<'_>,
511 command: &str,
512 label: &'static str,
513 style: Style,
514 ) {
515 if let Some(key) = self
516 .config
517 .keybindings
518 .get_first_keybinding(command, self.config.debug_context)
519 {
520 let formatted = format_key_for_display(&key);
521 let key_str: &'static str = Box::leak(formatted.into_boxed_str());
523 *banner = std::mem::take(banner).item(BannerItem::new(key_str, label, style));
524 }
525 }
526
527 fn build_inspect_overlay(&self, column: u16, row: u16, snapshot: &Buffer) -> DebugTableOverlay {
528 let mut builder = DebugTableBuilder::new();
529
530 builder.push_section("Position");
531 builder.push_entry("column", column.to_string());
532 builder.push_entry("row", row.to_string());
533
534 if let Some(preview) = inspect_cell(snapshot, column, row) {
535 builder.set_cell_preview(preview);
536 }
537
538 builder.finish(format!("Inspect ({column}, {row})"))
539 }
540}
541
542pub struct DebugLayerBuilder<A, C: BindingContext> {
544 config: DebugConfig<C>,
545 _marker: PhantomData<A>,
546}
547
548impl<A, C: BindingContext> DebugLayerBuilder<A, C> {
549 pub fn new(config: DebugConfig<C>) -> Self {
551 Self {
552 config,
553 _marker: PhantomData,
554 }
555 }
556
557 pub fn with_status_provider<F>(mut self, provider: F) -> Self
559 where
560 F: Fn() -> Vec<StatusItem> + Send + Sync + 'static,
561 {
562 self.config = self.config.with_status_provider(provider);
563 self
564 }
565
566 pub fn build(self) -> DebugLayer<A, C> {
568 DebugLayer::new(self.config)
569 }
570}
571
572impl<A> DebugLayer<A, SimpleDebugContext> {
577 pub fn simple() -> Self {
609 let keybindings = default_debug_keybindings();
610 let config = DebugConfig::new(keybindings, SimpleDebugContext::Debug);
611 Self::new(config)
612 }
613
614 pub fn simple_with_toggle_key(keys: &[&str]) -> Self {
631 let keybindings = default_debug_keybindings_with_toggle(keys);
632 let config = DebugConfig::new(keybindings, SimpleDebugContext::Debug);
633 Self::new(config)
634 }
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640 use crate::keybindings::Keybindings;
641
642 #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
644 enum TestContext {
645 Debug,
646 }
647
648 impl BindingContext for TestContext {
649 fn name(&self) -> &'static str {
650 "debug"
651 }
652 fn from_name(name: &str) -> Option<Self> {
653 (name == "debug").then_some(TestContext::Debug)
654 }
655 fn all() -> &'static [Self] {
656 &[TestContext::Debug]
657 }
658 }
659
660 #[derive(Debug, Clone)]
661 enum TestAction {
662 Foo,
663 Bar,
664 }
665
666 fn make_layer() -> DebugLayer<TestAction, TestContext> {
667 let config = DebugConfig::new(Keybindings::new(), TestContext::Debug);
668 DebugLayer::new(config)
669 }
670
671 #[test]
672 fn test_debug_layer_creation() {
673 let layer = make_layer();
674 assert!(!layer.is_enabled());
675 assert!(layer.freeze().snapshot.is_none());
676 }
677
678 #[test]
679 fn test_toggle() {
680 let mut layer = make_layer();
681
682 let effect = layer.handle_action(DebugAction::Toggle);
684 assert!(effect.is_none());
685 assert!(layer.is_enabled());
686
687 let effect = layer.handle_action(DebugAction::Toggle);
689 assert!(effect.is_none()); assert!(!layer.is_enabled());
691 }
692
693 #[test]
694 fn test_queued_actions_returned_on_disable() {
695 let mut layer = make_layer();
696
697 layer.handle_action(DebugAction::Toggle); layer.queue_action(TestAction::Foo);
699 layer.queue_action(TestAction::Bar);
700
701 let effect = layer.handle_action(DebugAction::Toggle); match effect {
704 Some(DebugSideEffect::ProcessQueuedActions(actions)) => {
705 assert_eq!(actions.len(), 2);
706 }
707 _ => panic!("Expected ProcessQueuedActions"),
708 }
709 }
710
711 #[test]
712 fn test_split_area() {
713 let layer = make_layer();
714
715 let area = Rect::new(0, 0, 80, 24);
717 let (app, banner) = layer.split_area(area);
718 assert_eq!(app, area);
719 assert_eq!(banner, Rect::ZERO);
720 }
721
722 #[test]
723 fn test_split_area_enabled() {
724 let mut layer = make_layer();
725 layer.handle_action(DebugAction::Toggle);
726
727 let area = Rect::new(0, 0, 80, 24);
728 let (app, banner) = layer.split_area(area);
729
730 assert_eq!(app.height, 23);
731 assert_eq!(banner.height, 1);
732 assert_eq!(banner.y, 23);
733 }
734
735 #[test]
736 fn test_mouse_capture_toggle() {
737 let mut layer = make_layer();
738 layer.handle_action(DebugAction::Toggle); let effect = layer.handle_action(DebugAction::ToggleMouseCapture);
741 assert!(matches!(effect, Some(DebugSideEffect::EnableMouseCapture)));
742 assert!(layer.freeze().mouse_capture_enabled);
743
744 let effect = layer.handle_action(DebugAction::ToggleMouseCapture);
745 assert!(matches!(effect, Some(DebugSideEffect::DisableMouseCapture)));
746 assert!(!layer.freeze().mouse_capture_enabled);
747 }
748
749 #[test]
751 fn test_simple_creation() {
752 let layer: DebugLayer<TestAction, SimpleDebugContext> = DebugLayer::simple();
753 assert!(!layer.is_enabled());
754 assert!(layer.freeze().snapshot.is_none());
755
756 assert_eq!(layer.config().debug_context, SimpleDebugContext::Debug);
758 }
759
760 #[test]
761 fn test_simple_toggle_works() {
762 let mut layer: DebugLayer<TestAction, SimpleDebugContext> = DebugLayer::simple();
763
764 layer.handle_action(DebugAction::Toggle);
766 assert!(layer.is_enabled());
767
768 layer.handle_action(DebugAction::Toggle);
770 assert!(!layer.is_enabled());
771 }
772
773 #[test]
774 fn test_simple_with_toggle_key() {
775 let layer: DebugLayer<TestAction, SimpleDebugContext> =
776 DebugLayer::simple_with_toggle_key(&["F11"]);
777
778 assert!(!layer.is_enabled());
779
780 let keybindings = &layer.config().keybindings;
782 let toggle_keys = keybindings.get_context_bindings(SimpleDebugContext::Debug);
783 assert!(toggle_keys.is_some());
784
785 if let Some(bindings) = toggle_keys {
786 let keys = bindings.get("debug.toggle");
787 assert!(keys.is_some());
788 assert!(keys.unwrap().contains(&"F11".to_string()));
789 }
790 }
791
792 #[test]
793 fn test_simple_has_default_keybindings() {
794 let layer: DebugLayer<TestAction, SimpleDebugContext> = DebugLayer::simple();
795 let keybindings = &layer.config().keybindings;
796
797 let bindings = keybindings
799 .get_context_bindings(SimpleDebugContext::Debug)
800 .unwrap();
801
802 assert!(bindings.contains_key("debug.toggle"));
803 assert!(bindings.contains_key("debug.state"));
804 assert!(bindings.contains_key("debug.copy"));
805 assert!(bindings.contains_key("debug.mouse"));
806
807 let toggle_keys = bindings.get("debug.toggle").unwrap();
809 assert!(toggle_keys.contains(&"F12".to_string()));
810 assert!(toggle_keys.contains(&"Esc".to_string()));
811 }
812}