egui_arbor/event_log.rs
1//! Event logging system for tracking outliner interactions.
2//!
3//! This module provides types for logging and tracking user interactions with the outliner,
4//! including selections, visibility changes, lock toggles, drag-drop operations, and renames.
5//!
6//! # Examples
7//!
8//! ```
9//! use egui_arbor::event_log::{EventLog, EventType};
10//!
11//! let mut log = EventLog::<u64>::new(10); // Keep last 10 events
12//!
13//! log.log("Selected node 5", EventType::Selection, Some(5));
14//! log.log("Renamed node 3", EventType::Rename, Some(3));
15//!
16//! for entry in log.entries() {
17//! println!("{}: {}", entry.event_type_str(), entry.message);
18//! }
19//! ```
20
21use std::collections::VecDeque;
22use std::time::SystemTime;
23
24/// Type of event that occurred in the outliner.
25///
26/// This enum categorizes different types of user interactions for logging
27/// and filtering purposes.
28#[derive(Clone, Debug, PartialEq, Eq, Hash)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub enum EventType {
31 /// Node selection or deselection event.
32 Selection,
33
34 /// Visibility toggle event (show/hide).
35 Visibility,
36
37 /// Lock state toggle event.
38 Lock,
39
40 /// Drag-and-drop operation event.
41 DragDrop,
42
43 /// Node rename event.
44 Rename,
45
46 /// Custom event type with a string identifier.
47 Custom(String),
48}
49
50impl EventType {
51 /// Returns a string representation of the event type.
52 ///
53 /// # Examples
54 ///
55 /// ```
56 /// use egui_arbor::event_log::EventType;
57 ///
58 /// assert_eq!(EventType::Selection.as_str(), "Selection");
59 /// assert_eq!(EventType::Custom("MyEvent".into()).as_str(), "MyEvent");
60 /// ```
61 pub fn as_str(&self) -> &str {
62 match self {
63 EventType::Selection => "Selection",
64 EventType::Visibility => "Visibility",
65 EventType::Lock => "Lock",
66 EventType::DragDrop => "DragDrop",
67 EventType::Rename => "Rename",
68 EventType::Custom(s) => s.as_str(),
69 }
70 }
71}
72
73/// A single log entry recording an event.
74///
75/// Each entry contains:
76/// - A timestamp of when the event occurred
77/// - A human-readable message describing the event
78/// - The type of event
79/// - An optional node ID associated with the event
80#[derive(Clone, Debug)]
81#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
82pub struct LogEntry<Id> {
83 /// When the event occurred.
84 pub timestamp: SystemTime,
85
86 /// Human-readable description of the event.
87 pub message: String,
88
89 /// The type of event that occurred.
90 pub event_type: EventType,
91
92 /// The ID of the node involved in the event, if applicable.
93 pub node_id: Option<Id>,
94}
95
96impl<Id> LogEntry<Id> {
97 /// Creates a new log entry.
98 ///
99 /// # Arguments
100 ///
101 /// * `message` - Human-readable description of the event
102 /// * `event_type` - The type of event
103 /// * `node_id` - Optional ID of the node involved
104 ///
105 /// # Examples
106 ///
107 /// ```
108 /// use egui_arbor::event_log::{LogEntry, EventType};
109 ///
110 /// let entry = LogEntry::new(
111 /// "Selected node".to_string(),
112 /// EventType::Selection,
113 /// Some(42u64),
114 /// );
115 /// ```
116 pub fn new(message: String, event_type: EventType, node_id: Option<Id>) -> Self {
117 Self {
118 timestamp: SystemTime::now(),
119 message,
120 event_type,
121 node_id,
122 }
123 }
124
125 /// Returns the event type as a string.
126 ///
127 /// # Examples
128 ///
129 /// ```
130 /// use egui_arbor::event_log::{LogEntry, EventType};
131 ///
132 /// let entry = LogEntry::<u64>::new("test".into(), EventType::Selection, None);
133 /// assert_eq!(entry.event_type_str(), "Selection");
134 /// ```
135 pub fn event_type_str(&self) -> &str {
136 self.event_type.as_str()
137 }
138
139 /// Returns the elapsed time since this event occurred.
140 ///
141 /// # Returns
142 ///
143 /// `Ok(Duration)` if the elapsed time could be calculated, or an error if
144 /// the system time has moved backwards.
145 ///
146 /// # Examples
147 ///
148 /// ```
149 /// use egui_arbor::event_log::{LogEntry, EventType};
150 ///
151 /// let entry = LogEntry::<u64>::new("test".into(), EventType::Selection, None);
152 /// if let Ok(elapsed) = entry.elapsed() {
153 /// println!("Event occurred {} seconds ago", elapsed.as_secs());
154 /// }
155 /// ```
156 pub fn elapsed(&self) -> Result<std::time::Duration, std::time::SystemTimeError> {
157 self.timestamp.elapsed()
158 }
159
160 /// Formats the elapsed time as a human-readable string.
161 ///
162 /// Returns strings like "5s ago", "2m ago", "1h ago", etc.
163 ///
164 /// # Examples
165 ///
166 /// ```
167 /// use egui_arbor::event_log::{LogEntry, EventType};
168 ///
169 /// let entry = LogEntry::<u64>::new("test".into(), EventType::Selection, None);
170 /// println!("{}", entry.format_elapsed());
171 /// ```
172 pub fn format_elapsed(&self) -> String {
173 match self.elapsed() {
174 Ok(duration) => {
175 let secs = duration.as_secs();
176 if secs < 60 {
177 format!("{}s ago", secs)
178 } else if secs < 3600 {
179 format!("{}m ago", secs / 60)
180 } else if secs < 86400 {
181 format!("{}h ago", secs / 3600)
182 } else {
183 format!("{}d ago", secs / 86400)
184 }
185 }
186 Err(_) => "unknown".to_string(),
187 }
188 }
189}
190
191/// Event log for tracking outliner interactions.
192///
193/// This structure maintains a circular buffer of recent events, automatically
194/// discarding old events when the maximum capacity is reached.
195///
196/// # Examples
197///
198/// ```
199/// use egui_arbor::event_log::{EventLog, EventType};
200///
201/// let mut log = EventLog::<u64>::new(100);
202///
203/// log.log("Node 5 selected", EventType::Selection, Some(5));
204/// log.log("Node 3 renamed to 'New Name'", EventType::Rename, Some(3));
205///
206/// assert_eq!(log.len(), 2);
207///
208/// for entry in log.entries() {
209/// println!("{}: {}", entry.event_type_str(), entry.message);
210/// }
211/// ```
212#[derive(Clone, Debug)]
213#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
214pub struct EventLog<Id> {
215 /// The log entries, with most recent first.
216 entries: VecDeque<LogEntry<Id>>,
217
218 /// Maximum number of entries to keep.
219 max_entries: usize,
220}
221
222impl<Id> EventLog<Id> {
223 /// Creates a new event log with the specified maximum capacity.
224 ///
225 /// # Arguments
226 ///
227 /// * `max_entries` - Maximum number of entries to keep. When this limit is
228 /// reached, the oldest entries are discarded.
229 ///
230 /// # Examples
231 ///
232 /// ```
233 /// use egui_arbor::event_log::EventLog;
234 ///
235 /// let log = EventLog::<u64>::new(50);
236 /// ```
237 pub fn new(max_entries: usize) -> Self {
238 Self {
239 entries: VecDeque::with_capacity(max_entries),
240 max_entries,
241 }
242 }
243
244 /// Logs a new event.
245 ///
246 /// The event is added to the front of the log (most recent). If the log
247 /// is at capacity, the oldest event is removed.
248 ///
249 /// # Arguments
250 ///
251 /// * `message` - Human-readable description of the event
252 /// * `event_type` - The type of event
253 /// * `node_id` - Optional ID of the node involved
254 ///
255 /// # Examples
256 ///
257 /// ```
258 /// use egui_arbor::event_log::{EventLog, EventType};
259 ///
260 /// let mut log = EventLog::new(10);
261 /// log.log("Selected node 5", EventType::Selection, Some(5u64));
262 /// ```
263 pub fn log(&mut self, message: impl Into<String>, event_type: EventType, node_id: Option<Id>) {
264 self.entries.push_front(LogEntry::new(
265 message.into(),
266 event_type,
267 node_id,
268 ));
269
270 if self.entries.len() > self.max_entries {
271 self.entries.pop_back();
272 }
273 }
274
275 /// Returns a slice of all log entries, with most recent first.
276 ///
277 /// # Examples
278 ///
279 /// ```
280 /// use egui_arbor::event_log::{EventLog, EventType};
281 ///
282 /// let mut log = EventLog::new(10);
283 /// log.log("Event 1", EventType::Selection, Some(1u64));
284 /// log.log("Event 2", EventType::Rename, Some(2u64));
285 ///
286 /// assert_eq!(log.entries().count(), 2);
287 /// let entries: Vec<_> = log.entries().collect();
288 /// assert_eq!(entries[0].message, "Event 2"); // Most recent first
289 /// ```
290 pub fn entries(&self) -> impl Iterator<Item = &LogEntry<Id>> {
291 self.entries.iter()
292 }
293
294 /// Returns the number of entries in the log.
295 ///
296 /// # Examples
297 ///
298 /// ```
299 /// use egui_arbor::event_log::{EventLog, EventType};
300 ///
301 /// let mut log = EventLog::<u64>::new(10);
302 /// assert_eq!(log.len(), 0);
303 ///
304 /// log.log("Event", EventType::Selection, None);
305 /// assert_eq!(log.len(), 1);
306 /// ```
307 pub fn len(&self) -> usize {
308 self.entries.len()
309 }
310
311 /// Returns `true` if the log contains no entries.
312 ///
313 /// # Examples
314 ///
315 /// ```
316 /// use egui_arbor::event_log::EventLog;
317 ///
318 /// let log = EventLog::<u64>::new(10);
319 /// assert!(log.is_empty());
320 /// ```
321 pub fn is_empty(&self) -> bool {
322 self.entries.is_empty()
323 }
324
325 /// Clears all entries from the log.
326 ///
327 /// # Examples
328 ///
329 /// ```
330 /// use egui_arbor::event_log::{EventLog, EventType};
331 ///
332 /// let mut log = EventLog::<u64>::new(10);
333 /// log.log("Event", EventType::Selection, None);
334 /// assert!(!log.is_empty());
335 ///
336 /// log.clear();
337 /// assert!(log.is_empty());
338 /// ```
339 pub fn clear(&mut self) {
340 self.entries.clear();
341 }
342
343 /// Returns the maximum number of entries this log can hold.
344 ///
345 /// # Examples
346 ///
347 /// ```
348 /// use egui_arbor::event_log::EventLog;
349 ///
350 /// let log = EventLog::<u64>::new(50);
351 /// assert_eq!(log.max_entries(), 50);
352 /// ```
353 pub fn max_entries(&self) -> usize {
354 self.max_entries
355 }
356
357 /// Sets the maximum number of entries this log can hold.
358 ///
359 /// If the new maximum is less than the current number of entries,
360 /// the oldest entries are removed to fit the new limit.
361 ///
362 /// # Examples
363 ///
364 /// ```
365 /// use egui_arbor::event_log::{EventLog, EventType};
366 ///
367 /// let mut log = EventLog::new(10);
368 /// for i in 0..10 {
369 /// log.log(format!("Event {}", i), EventType::Selection, Some(i));
370 /// }
371 ///
372 /// log.set_max_entries(5);
373 /// assert_eq!(log.len(), 5);
374 /// ```
375 pub fn set_max_entries(&mut self, max_entries: usize) {
376 self.max_entries = max_entries;
377 while self.entries.len() > max_entries {
378 self.entries.pop_back();
379 }
380 }
381
382 /// Filters entries by event type.
383 ///
384 /// Returns an iterator over entries matching the specified event type.
385 ///
386 /// # Examples
387 ///
388 /// ```
389 /// use egui_arbor::event_log::{EventLog, EventType};
390 ///
391 /// let mut log = EventLog::new(10);
392 /// log.log("Selected", EventType::Selection, Some(1u64));
393 /// log.log("Renamed", EventType::Rename, Some(2u64));
394 /// log.log("Selected again", EventType::Selection, Some(3u64));
395 ///
396 /// let selections: Vec<_> = log.filter_by_type(&EventType::Selection).collect();
397 /// assert_eq!(selections.len(), 2);
398 /// ```
399 pub fn filter_by_type(&self, event_type: &EventType) -> impl Iterator<Item = &LogEntry<Id>> {
400 self.entries.iter().filter(move |entry| &entry.event_type == event_type)
401 }
402}
403
404impl<Id> Default for EventLog<Id> {
405 /// Creates a new event log with a default capacity of 100 entries.
406 fn default() -> Self {
407 Self::new(100)
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn test_event_type_as_str() {
417 assert_eq!(EventType::Selection.as_str(), "Selection");
418 assert_eq!(EventType::Visibility.as_str(), "Visibility");
419 assert_eq!(EventType::Lock.as_str(), "Lock");
420 assert_eq!(EventType::DragDrop.as_str(), "DragDrop");
421 assert_eq!(EventType::Rename.as_str(), "Rename");
422 assert_eq!(EventType::Custom("Test".into()).as_str(), "Test");
423 }
424
425 #[test]
426 fn test_log_entry_creation() {
427 let entry = LogEntry::new(
428 "Test message".to_string(),
429 EventType::Selection,
430 Some(42u64),
431 );
432
433 assert_eq!(entry.message, "Test message");
434 assert_eq!(entry.event_type, EventType::Selection);
435 assert_eq!(entry.node_id, Some(42));
436 }
437
438 #[test]
439 fn test_log_entry_event_type_str() {
440 let entry = LogEntry::<u64>::new(
441 "Test".into(),
442 EventType::Rename,
443 None,
444 );
445
446 assert_eq!(entry.event_type_str(), "Rename");
447 }
448
449 #[test]
450 fn test_event_log_new() {
451 let log = EventLog::<u64>::new(10);
452 assert_eq!(log.max_entries(), 10);
453 assert_eq!(log.len(), 0);
454 assert!(log.is_empty());
455 }
456
457 #[test]
458 fn test_event_log_log() {
459 let mut log = EventLog::new(10);
460
461 log.log("Event 1", EventType::Selection, Some(1u64));
462 assert_eq!(log.len(), 1);
463
464 log.log("Event 2", EventType::Rename, Some(2u64));
465 assert_eq!(log.len(), 2);
466 }
467
468 #[test]
469 fn test_event_log_max_capacity() {
470 let mut log = EventLog::new(3);
471
472 log.log("Event 1", EventType::Selection, Some(1u64));
473 log.log("Event 2", EventType::Selection, Some(2u64));
474 log.log("Event 3", EventType::Selection, Some(3u64));
475 log.log("Event 4", EventType::Selection, Some(4u64));
476
477 assert_eq!(log.len(), 3);
478
479 // Most recent entries should be kept
480 let entries: Vec<_> = log.entries().collect();
481 assert_eq!(entries[0].message, "Event 4");
482 assert_eq!(entries[1].message, "Event 3");
483 assert_eq!(entries[2].message, "Event 2");
484 }
485
486 #[test]
487 fn test_event_log_clear() {
488 let mut log = EventLog::<u64>::new(10);
489
490 log.log("Event 1", EventType::Selection, None);
491 log.log("Event 2", EventType::Selection, None);
492 assert_eq!(log.len(), 2);
493
494 log.clear();
495 assert_eq!(log.len(), 0);
496 assert!(log.is_empty());
497 }
498
499 #[test]
500 fn test_event_log_set_max_entries() {
501 let mut log = EventLog::new(10);
502
503 for i in 0..10 {
504 log.log(format!("Event {}", i), EventType::Selection, Some(i));
505 }
506 assert_eq!(log.len(), 10);
507
508 log.set_max_entries(5);
509 assert_eq!(log.len(), 5);
510 assert_eq!(log.max_entries(), 5);
511
512 // Most recent entries should be kept
513 let entries: Vec<_> = log.entries().collect();
514 assert_eq!(entries[0].node_id, Some(9));
515 assert_eq!(entries[4].node_id, Some(5));
516 }
517
518 #[test]
519 fn test_event_log_filter_by_type() {
520 let mut log = EventLog::new(10);
521
522 log.log("Select 1", EventType::Selection, Some(1u64));
523 log.log("Rename 1", EventType::Rename, Some(2u64));
524 log.log("Select 2", EventType::Selection, Some(3u64));
525 log.log("Lock 1", EventType::Lock, Some(4u64));
526 log.log("Select 3", EventType::Selection, Some(5u64));
527
528 let selections: Vec<_> = log.filter_by_type(&EventType::Selection).collect();
529 assert_eq!(selections.len(), 3);
530
531 let renames: Vec<_> = log.filter_by_type(&EventType::Rename).collect();
532 assert_eq!(renames.len(), 1);
533
534 let visibility: Vec<_> = log.filter_by_type(&EventType::Visibility).collect();
535 assert_eq!(visibility.len(), 0);
536 }
537
538 #[test]
539 fn test_event_log_default() {
540 let log = EventLog::<u64>::default();
541 assert_eq!(log.max_entries(), 100);
542 assert!(log.is_empty());
543 }
544
545 #[test]
546 fn test_log_entry_format_elapsed() {
547 let entry = LogEntry::<u64>::new(
548 "Test".into(),
549 EventType::Selection,
550 None,
551 );
552
553 let formatted = entry.format_elapsed();
554 assert!(formatted.ends_with("ago"));
555 }
556}