1use super::DevToolsConfig;
4use crate::layout::Rect;
5use crate::render::Buffer;
6use crate::style::Color;
7use std::collections::VecDeque;
8use std::time::{Duration, Instant};
9
10struct 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub enum EventType {
50 KeyPress,
52 KeyRelease,
54 MouseClick,
56 MouseMove,
58 MouseScroll,
60 FocusIn,
62 FocusOut,
64 Resize,
66 Custom,
68}
69
70impl EventType {
71 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 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 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#[derive(Debug, Clone)]
117pub struct LoggedEvent {
118 pub id: u64,
120 pub event_type: EventType,
122 pub details: String,
124 pub target: Option<String>,
126 pub timestamp: Instant,
128 pub handled: bool,
130 pub propagated: bool,
132}
133
134impl LoggedEvent {
135 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 pub fn target(mut self, target: impl Into<String>) -> Self {
150 self.target = Some(target.into());
151 self
152 }
153
154 pub fn handled(mut self) -> Self {
156 self.handled = true;
157 self
158 }
159
160 pub fn stopped(mut self) -> Self {
162 self.propagated = false;
163 self
164 }
165
166 pub fn age(&self) -> Duration {
168 self.timestamp.elapsed()
169 }
170
171 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#[derive(Debug, Clone, Default)]
186pub struct EventFilter {
187 pub show_keys: bool,
189 pub show_mouse: bool,
191 pub show_focus: bool,
193 pub show_resize: bool,
195 pub show_custom: bool,
197 pub target_filter: Option<String>,
199 pub only_handled: bool,
201}
202
203impl EventFilter {
204 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 pub fn keys_only() -> Self {
219 Self {
220 show_keys: true,
221 ..Default::default()
222 }
223 }
224
225 pub fn mouse_only() -> Self {
227 Self {
228 show_mouse: true,
229 ..Default::default()
230 }
231 }
232
233 pub fn matches(&self, event: &LoggedEvent) -> bool {
235 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 if self.only_handled && !event.handled {
252 return false;
253 }
254
255 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#[derive(Debug)]
272pub struct EventLogger {
273 events: VecDeque<LoggedEvent>,
275 max_events: usize,
277 next_id: u64,
279 filter: EventFilter,
281 selected: Option<usize>,
283 scroll: usize,
285 paused: bool,
287 _start_time: Instant,
289}
290
291impl Default for EventLogger {
292 fn default() -> Self {
293 Self::new()
294 }
295}
296
297impl EventLogger {
298 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 pub fn max_events(mut self, max: usize) -> Self {
314 self.max_events = max;
315 self
316 }
317
318 pub fn filter(mut self, filter: EventFilter) -> Self {
320 self.filter = filter;
321 self
322 }
323
324 pub fn clear(&mut self) {
326 self.events.clear();
327 self.selected = None;
328 self.scroll = 0;
329 }
330
331 pub fn pause(&mut self) {
333 self.paused = true;
334 }
335
336 pub fn resume(&mut self) {
338 self.paused = false;
339 }
340
341 pub fn toggle_pause(&mut self) {
343 self.paused = !self.paused;
344 }
345
346 pub fn is_paused(&self) -> bool {
348 self.paused
349 }
350
351 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 while self.events.len() > self.max_events {
365 self.events.pop_front();
366 }
367
368 id
369 }
370
371 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 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 pub fn log_move(&mut self, x: u16, y: u16) -> u64 {
391 self.log(EventType::MouseMove, format!("({}, {})", x, y))
392 }
393
394 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 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 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 fn filtered(&self) -> Vec<&LoggedEvent> {
431 self.events
432 .iter()
433 .filter(|e| self.filter.matches(e))
434 .collect()
435 }
436
437 pub fn count(&self) -> usize {
439 self.events.len()
440 }
441
442 pub fn filtered_count(&self) -> usize {
444 self.filtered().len()
445 }
446
447 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 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 pub fn toggle_keys(&mut self) {
475 self.filter.show_keys = !self.filter.show_keys;
476 }
477
478 pub fn toggle_mouse(&mut self) {
480 self.filter.show_mouse = !self.filter.show_mouse;
481 }
482
483 pub fn toggle_focus(&mut self) {
485 self.filter.show_focus = !self.filter.show_focus;
486 }
487
488 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 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 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 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 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 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); }
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); 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 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}