revue/devtools/
events.rs

1//! Event logger for debugging event flow
2
3use super::DevToolsConfig;
4use crate::layout::Rect;
5use crate::render::Buffer;
6use crate::style::Color;
7use std::collections::VecDeque;
8use std::time::{Duration, Instant};
9
10/// Helper context for rendering devtools panels
11struct RenderCtx<'a> {
12    buffer: &'a mut Buffer,
13    x: u16,
14    width: u16,
15    config: &'a DevToolsConfig,
16}
17
18impl<'a> RenderCtx<'a> {
19    fn new(buffer: &'a mut Buffer, x: u16, width: u16, config: &'a DevToolsConfig) -> Self {
20        Self {
21            buffer,
22            x,
23            width,
24            config,
25        }
26    }
27
28    fn draw_text(&mut self, y: u16, text: &str, color: Color) {
29        for (i, ch) in text.chars().enumerate() {
30            if let Some(cell) = self.buffer.get_mut(self.x + i as u16, y) {
31                cell.symbol = ch;
32                cell.fg = Some(color);
33            }
34        }
35    }
36
37    fn draw_separator(&mut self, y: u16) {
38        for px in self.x..self.x + self.width {
39            if let Some(cell) = self.buffer.get_mut(px, y) {
40                cell.symbol = '─';
41                cell.fg = Some(self.config.accent_color);
42            }
43        }
44    }
45}
46
47/// Event type for logging
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub enum EventType {
50    /// Key press event
51    KeyPress,
52    /// Key release event
53    KeyRelease,
54    /// Mouse click
55    MouseClick,
56    /// Mouse move
57    MouseMove,
58    /// Mouse scroll
59    MouseScroll,
60    /// Focus gained
61    FocusIn,
62    /// Focus lost
63    FocusOut,
64    /// Resize event
65    Resize,
66    /// Custom/user event
67    Custom,
68}
69
70impl EventType {
71    /// Get display label
72    pub fn label(&self) -> &'static str {
73        match self {
74            Self::KeyPress => "KeyPress",
75            Self::KeyRelease => "KeyRelease",
76            Self::MouseClick => "Click",
77            Self::MouseMove => "Move",
78            Self::MouseScroll => "Scroll",
79            Self::FocusIn => "FocusIn",
80            Self::FocusOut => "FocusOut",
81            Self::Resize => "Resize",
82            Self::Custom => "Custom",
83        }
84    }
85
86    /// Get icon
87    pub fn icon(&self) -> &'static str {
88        match self {
89            Self::KeyPress => "⌨",
90            Self::KeyRelease => "⌨",
91            Self::MouseClick => "●",
92            Self::MouseMove => "→",
93            Self::MouseScroll => "↕",
94            Self::FocusIn => "◉",
95            Self::FocusOut => "○",
96            Self::Resize => "⊡",
97            Self::Custom => "★",
98        }
99    }
100
101    /// Get color for event type
102    pub fn color(&self) -> Color {
103        match self {
104            Self::KeyPress | Self::KeyRelease => Color::rgb(130, 180, 255),
105            Self::MouseClick => Color::rgb(255, 180, 130),
106            Self::MouseMove => Color::rgb(180, 180, 180),
107            Self::MouseScroll => Color::rgb(180, 255, 180),
108            Self::FocusIn | Self::FocusOut => Color::rgb(255, 220, 130),
109            Self::Resize => Color::rgb(200, 130, 255),
110            Self::Custom => Color::rgb(255, 130, 180),
111        }
112    }
113}
114
115/// A logged event
116#[derive(Debug, Clone)]
117pub struct LoggedEvent {
118    /// Event ID
119    pub id: u64,
120    /// Event type
121    pub event_type: EventType,
122    /// Event details
123    pub details: String,
124    /// Target widget (if any)
125    pub target: Option<String>,
126    /// Timestamp
127    pub timestamp: Instant,
128    /// Was event handled
129    pub handled: bool,
130    /// Was event propagated
131    pub propagated: bool,
132}
133
134impl LoggedEvent {
135    /// Create a new logged event
136    pub fn new(id: u64, event_type: EventType, details: impl Into<String>) -> Self {
137        Self {
138            id,
139            event_type,
140            details: details.into(),
141            target: None,
142            timestamp: Instant::now(),
143            handled: false,
144            propagated: true,
145        }
146    }
147
148    /// Set target
149    pub fn target(mut self, target: impl Into<String>) -> Self {
150        self.target = Some(target.into());
151        self
152    }
153
154    /// Mark as handled
155    pub fn handled(mut self) -> Self {
156        self.handled = true;
157        self
158    }
159
160    /// Mark as not propagated
161    pub fn stopped(mut self) -> Self {
162        self.propagated = false;
163        self
164    }
165
166    /// Get age since event occurred
167    pub fn age(&self) -> Duration {
168        self.timestamp.elapsed()
169    }
170
171    /// Format age for display
172    pub fn age_str(&self) -> String {
173        let age = self.age();
174        if age.as_secs() >= 60 {
175            format!("{}m ago", age.as_secs() / 60)
176        } else if age.as_secs() > 0 {
177            format!("{}s ago", age.as_secs())
178        } else {
179            format!("{}ms ago", age.as_millis())
180        }
181    }
182}
183
184/// Event filter configuration
185#[derive(Debug, Clone, Default)]
186pub struct EventFilter {
187    /// Show key events
188    pub show_keys: bool,
189    /// Show mouse events
190    pub show_mouse: bool,
191    /// Show focus events
192    pub show_focus: bool,
193    /// Show resize events
194    pub show_resize: bool,
195    /// Show custom events
196    pub show_custom: bool,
197    /// Filter by target
198    pub target_filter: Option<String>,
199    /// Only show handled events
200    pub only_handled: bool,
201}
202
203impl EventFilter {
204    /// Create filter that shows all events
205    pub fn all() -> Self {
206        Self {
207            show_keys: true,
208            show_mouse: true,
209            show_focus: true,
210            show_resize: true,
211            show_custom: true,
212            target_filter: None,
213            only_handled: false,
214        }
215    }
216
217    /// Create filter for keyboard events only
218    pub fn keys_only() -> Self {
219        Self {
220            show_keys: true,
221            ..Default::default()
222        }
223    }
224
225    /// Create filter for mouse events only
226    pub fn mouse_only() -> Self {
227        Self {
228            show_mouse: true,
229            ..Default::default()
230        }
231    }
232
233    /// Check if event matches filter
234    pub fn matches(&self, event: &LoggedEvent) -> bool {
235        // Check event type
236        let type_match = match event.event_type {
237            EventType::KeyPress | EventType::KeyRelease => self.show_keys,
238            EventType::MouseClick | EventType::MouseMove | EventType::MouseScroll => {
239                self.show_mouse
240            }
241            EventType::FocusIn | EventType::FocusOut => self.show_focus,
242            EventType::Resize => self.show_resize,
243            EventType::Custom => self.show_custom,
244        };
245
246        if !type_match {
247            return false;
248        }
249
250        // Check handled filter
251        if self.only_handled && !event.handled {
252            return false;
253        }
254
255        // Check target filter
256        if let Some(ref filter) = self.target_filter {
257            if let Some(ref target) = event.target {
258                if !target.to_lowercase().contains(&filter.to_lowercase()) {
259                    return false;
260                }
261            } else {
262                return false;
263            }
264        }
265
266        true
267    }
268}
269
270/// Event logger for debugging
271#[derive(Debug)]
272pub struct EventLogger {
273    /// Logged events (ring buffer)
274    events: VecDeque<LoggedEvent>,
275    /// Maximum events to keep
276    max_events: usize,
277    /// Next event ID
278    next_id: u64,
279    /// Current filter
280    filter: EventFilter,
281    /// Selected event index
282    selected: Option<usize>,
283    /// Scroll offset
284    scroll: usize,
285    /// Is paused
286    paused: bool,
287    /// Start time for relative timestamps (for future UI)
288    _start_time: Instant,
289}
290
291impl Default for EventLogger {
292    fn default() -> Self {
293        Self::new()
294    }
295}
296
297impl EventLogger {
298    /// Create new event logger
299    pub fn new() -> Self {
300        Self {
301            events: VecDeque::new(),
302            max_events: 500,
303            next_id: 0,
304            filter: EventFilter::all(),
305            selected: None,
306            scroll: 0,
307            paused: false,
308            _start_time: Instant::now(),
309        }
310    }
311
312    /// Set maximum events to keep
313    pub fn max_events(mut self, max: usize) -> Self {
314        self.max_events = max;
315        self
316    }
317
318    /// Set filter
319    pub fn filter(mut self, filter: EventFilter) -> Self {
320        self.filter = filter;
321        self
322    }
323
324    /// Clear all events
325    pub fn clear(&mut self) {
326        self.events.clear();
327        self.selected = None;
328        self.scroll = 0;
329    }
330
331    /// Pause logging
332    pub fn pause(&mut self) {
333        self.paused = true;
334    }
335
336    /// Resume logging
337    pub fn resume(&mut self) {
338        self.paused = false;
339    }
340
341    /// Toggle pause
342    pub fn toggle_pause(&mut self) {
343        self.paused = !self.paused;
344    }
345
346    /// Is paused
347    pub fn is_paused(&self) -> bool {
348        self.paused
349    }
350
351    /// Log an event
352    pub fn log(&mut self, event_type: EventType, details: impl Into<String>) -> u64 {
353        if self.paused {
354            return 0;
355        }
356
357        let id = self.next_id;
358        self.next_id += 1;
359
360        let event = LoggedEvent::new(id, event_type, details);
361        self.events.push_back(event);
362
363        // Trim if needed
364        while self.events.len() > self.max_events {
365            self.events.pop_front();
366        }
367
368        id
369    }
370
371    /// Log a key event
372    pub fn log_key(&mut self, key: &str, modifiers: &str) -> u64 {
373        let details = if modifiers.is_empty() {
374            key.to_string()
375        } else {
376            format!("{} + {}", modifiers, key)
377        };
378        self.log(EventType::KeyPress, details)
379    }
380
381    /// Log a mouse click
382    pub fn log_click(&mut self, x: u16, y: u16, button: &str) -> u64 {
383        self.log(
384            EventType::MouseClick,
385            format!("{} @ ({}, {})", button, x, y),
386        )
387    }
388
389    /// Log a mouse move
390    pub fn log_move(&mut self, x: u16, y: u16) -> u64 {
391        self.log(EventType::MouseMove, format!("({}, {})", x, y))
392    }
393
394    /// Log focus change
395    pub fn log_focus(&mut self, target: &str, gained: bool) -> u64 {
396        let event_type = if gained {
397            EventType::FocusIn
398        } else {
399            EventType::FocusOut
400        };
401        let mut event = LoggedEvent::new(self.next_id, event_type, target);
402        event.target = Some(target.to_string());
403
404        let id = self.next_id;
405        self.next_id += 1;
406        self.events.push_back(event);
407
408        while self.events.len() > self.max_events {
409            self.events.pop_front();
410        }
411
412        id
413    }
414
415    /// Mark event as handled
416    pub fn mark_handled(&mut self, id: u64) {
417        if let Some(event) = self.events.iter_mut().find(|e| e.id == id) {
418            event.handled = true;
419        }
420    }
421
422    /// Set event target
423    pub fn set_target(&mut self, id: u64, target: impl Into<String>) {
424        if let Some(event) = self.events.iter_mut().find(|e| e.id == id) {
425            event.target = Some(target.into());
426        }
427    }
428
429    /// Get filtered events
430    fn filtered(&self) -> Vec<&LoggedEvent> {
431        self.events
432            .iter()
433            .filter(|e| self.filter.matches(e))
434            .collect()
435    }
436
437    /// Get event count
438    pub fn count(&self) -> usize {
439        self.events.len()
440    }
441
442    /// Get filtered event count
443    pub fn filtered_count(&self) -> usize {
444        self.filtered().len()
445    }
446
447    /// Select next event
448    pub fn select_next(&mut self) {
449        let count = self.filtered().len();
450        if count == 0 {
451            return;
452        }
453
454        self.selected = Some(match self.selected {
455            Some(i) => (i + 1).min(count - 1),
456            None => 0,
457        });
458    }
459
460    /// Select previous event
461    pub fn select_prev(&mut self) {
462        let count = self.filtered().len();
463        if count == 0 {
464            return;
465        }
466
467        self.selected = Some(match self.selected {
468            Some(i) => i.saturating_sub(1),
469            None => 0,
470        });
471    }
472
473    /// Toggle key events filter
474    pub fn toggle_keys(&mut self) {
475        self.filter.show_keys = !self.filter.show_keys;
476    }
477
478    /// Toggle mouse events filter
479    pub fn toggle_mouse(&mut self) {
480        self.filter.show_mouse = !self.filter.show_mouse;
481    }
482
483    /// Toggle focus events filter
484    pub fn toggle_focus(&mut self) {
485        self.filter.show_focus = !self.filter.show_focus;
486    }
487
488    /// Render event logger content
489    pub fn render_content(&self, buffer: &mut Buffer, area: Rect, config: &DevToolsConfig) {
490        let mut ctx = RenderCtx::new(buffer, area.x, area.width, config);
491        let mut y = area.y;
492        let max_y = area.y + area.height;
493
494        // Header
495        let status = if self.paused {
496            "⏸ PAUSED"
497        } else {
498            "● Recording"
499        };
500        let header = format!("{} | {} events", status, self.filtered_count());
501        ctx.draw_text(y, &header, config.accent_color);
502        y += 1;
503
504        // Filter info
505        let mut filters = Vec::new();
506        if self.filter.show_keys {
507            filters.push("Keys");
508        }
509        if self.filter.show_mouse {
510            filters.push("Mouse");
511        }
512        if self.filter.show_focus {
513            filters.push("Focus");
514        }
515        if self.filter.show_resize {
516            filters.push("Resize");
517        }
518        let filter_str = format!("Showing: {}", filters.join(", "));
519        ctx.draw_text(y, &filter_str, config.fg_color);
520        y += 2;
521
522        // Events list (newest first)
523        let filtered: Vec<_> = self.filtered().into_iter().rev().collect();
524        for (i, event) in filtered.iter().enumerate().skip(self.scroll) {
525            if y >= max_y - 2 {
526                break;
527            }
528
529            let is_selected = self.selected == Some(i);
530            Self::render_event(&mut ctx, y, event, is_selected);
531            y += 1;
532        }
533
534        // Selected event details
535        if let Some(idx) = self.selected {
536            if let Some(event) = filtered.get(idx) {
537                if y + 2 < max_y {
538                    y = max_y - 3;
539                    ctx.draw_separator(y);
540                    y += 1;
541                    Self::render_details(&mut ctx, y, event);
542                }
543            }
544        }
545    }
546
547    fn render_event(ctx: &mut RenderCtx<'_>, y: u16, event: &LoggedEvent, selected: bool) {
548        let icon = event.event_type.icon();
549        let handled_mark = if event.handled { "✓" } else { " " };
550        let age = event.age_str();
551
552        // Truncate details if needed
553        let max_details = (ctx.width as usize).saturating_sub(20);
554        let details = if event.details.len() > max_details {
555            format!("{}...", &event.details[..max_details.saturating_sub(3)])
556        } else {
557            event.details.clone()
558        };
559
560        let line = format!("{} {} {} {}", icon, handled_mark, details, age);
561
562        let fg = if selected {
563            ctx.config.bg_color
564        } else {
565            event.event_type.color()
566        };
567        let bg = if selected {
568            Some(ctx.config.accent_color)
569        } else {
570            None
571        };
572
573        for (i, ch) in line.chars().enumerate() {
574            if (i as u16) < ctx.width {
575                if let Some(cell) = ctx.buffer.get_mut(ctx.x + i as u16, y) {
576                    cell.symbol = ch;
577                    cell.fg = Some(fg);
578                    if let Some(b) = bg {
579                        cell.bg = Some(b);
580                    }
581                }
582            }
583        }
584    }
585
586    fn render_details(ctx: &mut RenderCtx<'_>, y: u16, event: &LoggedEvent) {
587        let target = event.target.as_deref().unwrap_or("none");
588        let details = format!(
589            "#{} {} | Target: {} | {}",
590            event.id,
591            event.event_type.label(),
592            target,
593            if event.handled {
594                "Handled"
595            } else {
596                "Not handled"
597            }
598        );
599        ctx.draw_text(y, &details, ctx.config.fg_color);
600    }
601}
602
603#[cfg(test)]
604mod tests {
605    use super::*;
606
607    #[test]
608    fn test_event_type_label() {
609        assert_eq!(EventType::KeyPress.label(), "KeyPress");
610        assert_eq!(EventType::MouseClick.label(), "Click");
611        assert_eq!(EventType::FocusIn.label(), "FocusIn");
612    }
613
614    #[test]
615    fn test_logged_event() {
616        let event = LoggedEvent::new(1, EventType::KeyPress, "Enter")
617            .target("Button#submit")
618            .handled();
619
620        assert_eq!(event.id, 1);
621        assert_eq!(event.event_type, EventType::KeyPress);
622        assert_eq!(event.details, "Enter");
623        assert_eq!(event.target, Some("Button#submit".to_string()));
624        assert!(event.handled);
625    }
626
627    #[test]
628    fn test_event_filter() {
629        let filter = EventFilter::keys_only();
630
631        let key_event = LoggedEvent::new(1, EventType::KeyPress, "A");
632        let mouse_event = LoggedEvent::new(2, EventType::MouseClick, "left");
633
634        assert!(filter.matches(&key_event));
635        assert!(!filter.matches(&mouse_event));
636    }
637
638    #[test]
639    fn test_event_logger_log() {
640        let mut logger = EventLogger::new();
641        let id = logger.log_key("Enter", "Ctrl");
642
643        assert_eq!(logger.count(), 1);
644        assert!(id > 0 || id == 0); // First ID is 0
645    }
646
647    #[test]
648    fn test_event_logger_pause() {
649        let mut logger = EventLogger::new();
650        logger.log_key("A", "");
651        assert_eq!(logger.count(), 1);
652
653        logger.pause();
654        logger.log_key("B", "");
655        assert_eq!(logger.count(), 1); // Should not log while paused
656
657        logger.resume();
658        logger.log_key("C", "");
659        assert_eq!(logger.count(), 2);
660    }
661
662    #[test]
663    fn test_event_logger_clear() {
664        let mut logger = EventLogger::new();
665        logger.log_key("A", "");
666        logger.log_key("B", "");
667        assert_eq!(logger.count(), 2);
668
669        logger.clear();
670        assert_eq!(logger.count(), 0);
671    }
672
673    #[test]
674    fn test_event_logger_max_events() {
675        let mut logger = EventLogger::new().max_events(3);
676
677        for i in 0..5 {
678            logger.log_key(&format!("Key{}", i), "");
679        }
680
681        assert_eq!(logger.count(), 3);
682    }
683
684    #[test]
685    fn test_event_logger_mark_handled() {
686        let mut logger = EventLogger::new();
687        let id = logger.log_key("Enter", "");
688
689        assert!(!logger.events.back().unwrap().handled);
690
691        logger.mark_handled(id);
692        assert!(logger.events.back().unwrap().handled);
693    }
694
695    #[test]
696    fn test_event_filter_all() {
697        let filter = EventFilter::all();
698
699        // All event types should match
700        let events = vec![
701            LoggedEvent::new(1, EventType::KeyPress, "A"),
702            LoggedEvent::new(2, EventType::MouseClick, "left"),
703            LoggedEvent::new(3, EventType::FocusIn, "input"),
704            LoggedEvent::new(4, EventType::Resize, "80x24"),
705            LoggedEvent::new(5, EventType::Custom, "custom"),
706        ];
707
708        for event in &events {
709            assert!(
710                filter.matches(event),
711                "Filter should match {:?}",
712                event.event_type
713            );
714        }
715    }
716}