use super::super::helpers::{draw_separator, draw_text_overlay};
use super::super::DevToolsConfig;
use super::types::{EventFilter, EventType, LoggedEvent};
use crate::layout::Rect;
use crate::render::Buffer;
use crate::style::Color;
use std::collections::VecDeque;
use std::time::Instant;
struct RenderCtx<'a> {
buffer: &'a mut Buffer,
x: u16,
width: u16,
config: &'a DevToolsConfig,
}
impl<'a> RenderCtx<'a> {
fn new(buffer: &'a mut Buffer, x: u16, width: u16, config: &'a DevToolsConfig) -> Self {
Self {
buffer,
x,
width,
config,
}
}
fn draw_text(&mut self, y: u16, text: &str, color: Color) {
draw_text_overlay(self.buffer, self.x, y, text, color);
}
fn draw_separator(&mut self, y: u16) {
draw_separator(self.buffer, self.x, y, self.width, self.config.accent_color);
}
}
#[derive(Debug)]
pub struct EventLogger {
events: VecDeque<LoggedEvent>,
max_events: usize,
next_id: u64,
filter: EventFilter,
selected: Option<usize>,
scroll: usize,
paused: bool,
_start_time: Instant,
}
impl Default for EventLogger {
fn default() -> Self {
Self::new()
}
}
impl EventLogger {
pub fn new() -> Self {
Self {
events: VecDeque::new(),
max_events: 500,
next_id: 0,
filter: EventFilter::all(),
selected: None,
scroll: 0,
paused: false,
_start_time: Instant::now(),
}
}
pub fn max_events(mut self, max: usize) -> Self {
self.max_events = max;
self
}
pub fn filter(mut self, filter: EventFilter) -> Self {
self.filter = filter;
self
}
pub fn clear(&mut self) {
self.events.clear();
self.selected = None;
self.scroll = 0;
}
pub fn pause(&mut self) {
self.paused = true;
}
pub fn resume(&mut self) {
self.paused = false;
}
pub fn toggle_pause(&mut self) {
self.paused = !self.paused;
}
pub fn is_paused(&self) -> bool {
self.paused
}
pub fn log(&mut self, event_type: EventType, details: impl Into<String>) -> u64 {
if self.paused {
return 0;
}
let id = self.next_id;
self.next_id += 1;
let event = LoggedEvent::new(id, event_type, details);
self.events.push_back(event);
while self.events.len() > self.max_events {
self.events.pop_front();
}
id
}
pub fn log_key(&mut self, key: &str, modifiers: &str) -> u64 {
let details = if modifiers.is_empty() {
key.to_string()
} else {
format!("{} + {}", modifiers, key)
};
self.log(EventType::KeyPress, details)
}
pub fn log_click(&mut self, x: u16, y: u16, button: &str) -> u64 {
self.log(
EventType::MouseClick,
format!("{} @ ({}, {})", button, x, y),
)
}
pub fn log_move(&mut self, x: u16, y: u16) -> u64 {
self.log(EventType::MouseMove, format!("({}, {})", x, y))
}
pub fn log_focus(&mut self, target: &str, gained: bool) -> u64 {
let event_type = if gained {
EventType::FocusIn
} else {
EventType::FocusOut
};
let mut event = LoggedEvent::new(self.next_id, event_type, target);
event.target = Some(target.to_string());
let id = self.next_id;
self.next_id += 1;
self.events.push_back(event);
while self.events.len() > self.max_events {
self.events.pop_front();
}
id
}
pub fn mark_handled(&mut self, id: u64) {
if let Some(event) = self.events.iter_mut().find(|e| e.id == id) {
event.handled = true;
}
}
pub fn set_target(&mut self, id: u64, target: impl Into<String>) {
if let Some(event) = self.events.iter_mut().find(|e| e.id == id) {
event.target = Some(target.into());
}
}
fn filtered(&self) -> Vec<&LoggedEvent> {
self.events
.iter()
.filter(|e| self.filter.matches(e))
.collect()
}
pub fn count(&self) -> usize {
self.events.len()
}
pub fn filtered_count(&self) -> usize {
self.filtered().len()
}
pub fn select_next(&mut self) {
let count = self.filtered().len();
if count == 0 {
return;
}
self.selected = Some(match self.selected {
Some(i) => (i + 1).min(count - 1),
None => 0,
});
}
pub fn select_prev(&mut self) {
let count = self.filtered().len();
if count == 0 {
return;
}
self.selected = Some(match self.selected {
Some(i) => i.saturating_sub(1),
None => 0,
});
}
pub fn toggle_keys(&mut self) {
self.filter.show_keys = !self.filter.show_keys;
}
pub fn toggle_mouse(&mut self) {
self.filter.show_mouse = !self.filter.show_mouse;
}
pub fn toggle_focus(&mut self) {
self.filter.show_focus = !self.filter.show_focus;
}
pub fn render_content(&self, buffer: &mut Buffer, area: Rect, config: &DevToolsConfig) {
let mut ctx = RenderCtx::new(buffer, area.x, area.width, config);
let mut y = area.y;
let max_y = area.y + area.height;
let status = if self.paused {
"⏸ PAUSED"
} else {
"● Recording"
};
let header = format!("{} | {} events", status, self.filtered_count());
ctx.draw_text(y, &header, config.accent_color);
y += 1;
let mut filters = Vec::new();
if self.filter.show_keys {
filters.push("Keys");
}
if self.filter.show_mouse {
filters.push("Mouse");
}
if self.filter.show_focus {
filters.push("Focus");
}
if self.filter.show_resize {
filters.push("Resize");
}
let filter_str = format!("Showing: {}", filters.join(", "));
ctx.draw_text(y, &filter_str, config.fg_color);
y += 2;
let filtered: Vec<_> = self.filtered().into_iter().rev().collect();
for (i, event) in filtered.iter().enumerate().skip(self.scroll) {
if y >= max_y - 2 {
break;
}
let is_selected = self.selected == Some(i);
Self::render_event(&mut ctx, y, event, is_selected);
y += 1;
}
if let Some(idx) = self.selected {
if let Some(event) = filtered.get(idx) {
if y + 2 < max_y {
y = max_y - 3;
ctx.draw_separator(y);
y += 1;
Self::render_details(&mut ctx, y, event);
}
}
}
}
fn render_event(ctx: &mut RenderCtx<'_>, y: u16, event: &LoggedEvent, selected: bool) {
let icon = event.event_type.icon();
let handled_mark = if event.handled { "✓" } else { " " };
let age = event.age_str();
let max_details = (ctx.width as usize).saturating_sub(20);
let details = if event.details.len() > max_details {
format!("{}...", &event.details[..max_details.saturating_sub(3)])
} else {
event.details.clone()
};
let line = format!("{} {} {} {}", icon, handled_mark, details, age);
let fg = if selected {
ctx.config.bg_color
} else {
event.event_type.color()
};
let bg = if selected {
Some(ctx.config.accent_color)
} else {
None
};
for (i, ch) in line.chars().enumerate() {
if (i as u16) < ctx.width {
if let Some(cell) = ctx.buffer.get_mut(ctx.x + i as u16, y) {
cell.symbol = ch;
cell.fg = Some(fg);
if let Some(b) = bg {
cell.bg = Some(b);
}
}
}
}
}
fn render_details(ctx: &mut RenderCtx<'_>, y: u16, event: &LoggedEvent) {
let target = event.target.as_deref().unwrap_or("none");
let details = format!(
"#{} {} | Target: {} | {}",
event.id,
event.event_type.label(),
target,
if event.handled {
"Handled"
} else {
"Not handled"
}
);
ctx.draw_text(y, &details, ctx.config.fg_color);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_event_type_label() {
assert_eq!(EventType::KeyPress.label(), "KeyPress");
assert_eq!(EventType::MouseClick.label(), "Click");
assert_eq!(EventType::FocusIn.label(), "FocusIn");
}
#[test]
fn test_logged_event() {
let event = LoggedEvent::new(1, EventType::KeyPress, "Enter")
.target("Button#submit")
.handled();
assert_eq!(event.id, 1);
assert_eq!(event.event_type, EventType::KeyPress);
assert_eq!(event.details, "Enter");
assert_eq!(event.target, Some("Button#submit".to_string()));
assert!(event.handled);
}
#[test]
fn test_event_filter() {
let filter = EventFilter::keys_only();
let key_event = LoggedEvent::new(1, EventType::KeyPress, "A");
let mouse_event = LoggedEvent::new(2, EventType::MouseClick, "left");
assert!(filter.matches(&key_event));
assert!(!filter.matches(&mouse_event));
}
#[test]
fn test_event_logger_log() {
let mut logger = EventLogger::new();
let id = logger.log_key("Enter", "Ctrl");
assert_eq!(logger.count(), 1);
assert!(id > 0 || id == 0); }
#[test]
fn test_event_logger_pause() {
let mut logger = EventLogger::new();
logger.log_key("A", "");
assert_eq!(logger.count(), 1);
logger.pause();
logger.log_key("B", "");
assert_eq!(logger.count(), 1);
logger.resume();
logger.log_key("C", "");
assert_eq!(logger.count(), 2);
}
#[test]
fn test_event_logger_clear() {
let mut logger = EventLogger::new();
logger.log_key("A", "");
logger.log_key("B", "");
assert_eq!(logger.count(), 2);
logger.clear();
assert_eq!(logger.count(), 0);
}
#[test]
fn test_event_logger_max_events() {
let mut logger = EventLogger::new().max_events(3);
for i in 0..5 {
logger.log_key(&format!("Key{}", i), "");
}
assert_eq!(logger.count(), 3);
}
#[test]
fn test_event_logger_mark_handled() {
let mut logger = EventLogger::new();
let id = logger.log_key("Enter", "");
assert!(!logger.events.back().unwrap().handled);
logger.mark_handled(id);
assert!(logger.events.back().unwrap().handled);
}
#[test]
fn test_event_filter_all() {
let filter = EventFilter::all();
let events = vec![
LoggedEvent::new(1, EventType::KeyPress, "A"),
LoggedEvent::new(2, EventType::MouseClick, "left"),
LoggedEvent::new(3, EventType::FocusIn, "input"),
LoggedEvent::new(4, EventType::Resize, "80x24"),
LoggedEvent::new(5, EventType::Custom, "custom"),
];
for event in &events {
assert!(
filter.matches(event),
"Filter should match {:?}",
event.event_type
);
}
}
#[test]
fn test_event_type_icon() {
assert_eq!(EventType::KeyPress.icon(), "⌨");
assert_eq!(EventType::KeyRelease.icon(), "⌨");
assert_eq!(EventType::MouseClick.icon(), "●");
assert_eq!(EventType::MouseMove.icon(), "→");
assert_eq!(EventType::MouseScroll.icon(), "↕");
assert_eq!(EventType::FocusIn.icon(), "◉");
assert_eq!(EventType::FocusOut.icon(), "○");
assert_eq!(EventType::Resize.icon(), "⊡");
assert_eq!(EventType::Custom.icon(), "★");
}
#[test]
fn test_event_type_color() {
let key_color = EventType::KeyPress.color();
let release_color = EventType::KeyRelease.color();
assert_eq!(key_color, release_color);
let click_color = EventType::MouseClick.color();
assert_ne!(click_color, key_color);
let move_color = EventType::MouseMove.color();
let scroll_color = EventType::MouseScroll.color();
assert_ne!(move_color, scroll_color);
let focus_in = EventType::FocusIn.color();
let focus_out = EventType::FocusOut.color();
assert_eq!(focus_in, focus_out);
let resize_color = EventType::Resize.color();
let custom_color = EventType::Custom.color();
assert_ne!(resize_color, custom_color);
}
#[test]
fn test_event_type_all_labels() {
assert_eq!(EventType::KeyRelease.label(), "KeyRelease");
assert_eq!(EventType::MouseMove.label(), "Move");
assert_eq!(EventType::MouseScroll.label(), "Scroll");
assert_eq!(EventType::FocusOut.label(), "FocusOut");
assert_eq!(EventType::Resize.label(), "Resize");
assert_eq!(EventType::Custom.label(), "Custom");
}
#[test]
fn test_logged_event_stopped() {
let event = LoggedEvent::new(1, EventType::KeyPress, "A").stopped();
assert!(!event.propagated);
}
#[test]
fn test_logged_event_age_str() {
let event = LoggedEvent::new(1, EventType::KeyPress, "A");
let age_str = event.age_str();
assert!(age_str.contains("ms ago"));
}
#[test]
fn test_event_filter_mouse_only() {
let filter = EventFilter::mouse_only();
let mouse_event = LoggedEvent::new(1, EventType::MouseClick, "left");
let key_event = LoggedEvent::new(2, EventType::KeyPress, "A");
assert!(filter.matches(&mouse_event));
assert!(!filter.matches(&key_event));
}
#[test]
fn test_event_filter_only_handled() {
let mut filter = EventFilter::all();
filter.only_handled = true;
let handled_event = LoggedEvent::new(1, EventType::KeyPress, "A").handled();
let unhandled_event = LoggedEvent::new(2, EventType::KeyPress, "B");
assert!(filter.matches(&handled_event));
assert!(!filter.matches(&unhandled_event));
}
#[test]
fn test_event_filter_target_filter() {
let mut filter = EventFilter::all();
filter.target_filter = Some("Button".to_string());
let mut matched_event = LoggedEvent::new(1, EventType::KeyPress, "A");
matched_event.target = Some("Button#submit".to_string());
let mut unmatched_event = LoggedEvent::new(2, EventType::KeyPress, "A");
unmatched_event.target = Some("Input#name".to_string());
let no_target_event = LoggedEvent::new(3, EventType::KeyPress, "A");
assert!(filter.matches(&matched_event));
assert!(!filter.matches(&unmatched_event));
assert!(!filter.matches(&no_target_event));
}
#[test]
fn test_event_logger_log_click() {
let mut logger = EventLogger::new();
let id = logger.log_click(10, 20, "left");
assert!(id == 0 || id > 0);
assert_eq!(logger.count(), 1);
}
#[test]
fn test_event_logger_log_move() {
let mut logger = EventLogger::new();
logger.log_move(50, 60);
assert_eq!(logger.count(), 1);
}
#[test]
fn test_event_logger_log_focus() {
let mut logger = EventLogger::new();
logger.log_focus("Input#name", true);
logger.log_focus("Input#name", false);
assert_eq!(logger.count(), 2);
}
#[test]
fn test_event_logger_set_target() {
let mut logger = EventLogger::new();
let id = logger.log_key("Enter", "");
logger.set_target(id, "Button#submit");
assert_eq!(
logger.events.back().unwrap().target,
Some("Button#submit".to_string())
);
}
#[test]
fn test_event_logger_toggle_pause() {
let mut logger = EventLogger::new();
assert!(!logger.is_paused());
logger.toggle_pause();
assert!(logger.is_paused());
logger.toggle_pause();
assert!(!logger.is_paused());
}
#[test]
fn test_event_logger_select_next_prev() {
let mut logger = EventLogger::new();
logger.log_key("A", "");
logger.log_key("B", "");
logger.log_key("C", "");
logger.select_next();
assert!(logger.selected.is_some());
logger.select_next();
logger.select_prev();
}
#[test]
fn test_event_logger_select_empty() {
let mut logger = EventLogger::new();
logger.select_next();
logger.select_prev();
}
#[test]
fn test_event_logger_filtered_count() {
let mut logger = EventLogger::new().filter(EventFilter::keys_only());
logger.log_key("A", "");
logger.log_click(0, 0, "left");
assert!(logger.filtered_count() <= logger.count());
}
#[test]
fn test_event_logger_toggle_filters() {
let mut logger = EventLogger::new();
logger.toggle_keys();
assert!(!logger.filter.show_keys);
logger.toggle_mouse();
assert!(!logger.filter.show_mouse);
logger.toggle_focus();
assert!(!logger.filter.show_focus);
}
#[test]
fn test_event_filter_default() {
let filter = EventFilter::default();
assert!(!filter.show_keys);
assert!(!filter.show_mouse);
assert!(!filter.show_focus);
}
}