From 44f49713ebf8ac26094e8a303b5be01b9bdc7518 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 23 Jun 2024 11:34:38 +0200 Subject: [PATCH] Add an input event history tracker to the egui demo lib (#4693) This is great for testing how e.g. keyboard events are seen by egui: ![image](https://github.com/emilk/egui/assets/1148717/b2187060-6533-439c-9f43-fc49b8213c28) * Relevant: https://github.com/emilk/egui/issues/3653 --- crates/egui/src/data/input.rs | 36 ++++- crates/egui/src/data/key.rs | 35 +++-- .../src/demo/demo_app_windows.rs | 1 + .../src/demo/tests/input_event_history.rs | 132 ++++++++++++++++++ .../src/demo/tests/input_test.rs | 4 +- crates/egui_demo_lib/src/demo/tests/mod.rs | 2 + 6 files changed, 193 insertions(+), 17 deletions(-) create mode 100644 crates/egui_demo_lib/src/demo/tests/input_event_history.rs diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 3ee88a49..5041034f 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -544,7 +544,7 @@ pub const NUM_POINTER_BUTTONS: usize = 5; /// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers /// as on mac that is how you type special characters, /// so those key presses are usually not reported to egui. -#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] +#[derive(Clone, Copy, Default, Hash, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct Modifiers { /// Either of the alt keys are down (option ⌥ on Mac). @@ -567,6 +567,40 @@ pub struct Modifiers { pub command: bool, } +impl std::fmt::Debug for Modifiers { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.is_none() { + return write!(f, "Modifiers::NONE"); + } + + let Self { + alt, + ctrl, + shift, + mac_cmd, + command, + } = *self; + + let mut debug = f.debug_struct("Modifiers"); + if alt { + debug.field("alt", &true); + } + if ctrl { + debug.field("ctrl", &true); + } + if shift { + debug.field("shift", &true); + } + if mac_cmd { + debug.field("mac_cmd", &true); + } + if command { + debug.field("command", &true); + } + debug.finish() + } +} + impl Modifiers { pub const NONE: Self = Self { alt: false, diff --git a/crates/egui/src/data/key.rs b/crates/egui/src/data/key.rs index 66fe6b93..61babb3f 100644 --- a/crates/egui/src/data/key.rs +++ b/crates/egui/src/data/key.rs @@ -1,10 +1,12 @@ /// Keyboard keys. /// -/// egui usually uses logical keys, i.e. after applying any user keymap. -// TODO(emilk): split into `LogicalKey` and `PhysicalKey` +/// egui usually uses logical keys, i.e. after applying any user keymap.\ +// See comment at the end of `Key { … }` on how to add new keys. #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum Key { + // ---------------------------------------------- + // Commands: ArrowDown, ArrowLeft, ArrowRight, @@ -73,34 +75,34 @@ pub enum Key { // ---------------------------------------------- // Digits: - /// Either from the main row or from the numpad. + /// `0` (from main row or numpad) Num0, - /// Either from the main row or from the numpad. + /// `1` (from main row or numpad) Num1, - /// Either from the main row or from the numpad. + /// `2` (from main row or numpad) Num2, - /// Either from the main row or from the numpad. + /// `3` (from main row or numpad) Num3, - /// Either from the main row or from the numpad. + /// `4` (from main row or numpad) Num4, - /// Either from the main row or from the numpad. + /// `5` (from main row or numpad) Num5, - /// Either from the main row or from the numpad. + /// `6` (from main row or numpad) Num6, - /// Either from the main row or from the numpad. + /// `7` (from main row or numpad) Num7, - /// Either from the main row or from the numpad. + /// `8` (from main row or numpad) Num8, - /// Either from the main row or from the numpad. + /// `9` (from main row or numpad) Num9, // ---------------------------------------------- @@ -169,14 +171,19 @@ pub enum Key { F33, F34, F35, - // When adding keys, remember to also update `crates/egui-winit/src/lib.rs` - // and [`Self::ALL`]. + // When adding keys, remember to also update: + // * crates/egui-winit/src/lib.rs + // * Key::ALL + // * Key::from_name + // You should test that it works using the "Input Event History" window in the egui demo app. + // Make sure to test both natively and on web! // Also: don't add keys last; add them to the group they best belong to. } impl Key { /// All egui keys pub const ALL: &'static [Self] = &[ + // Commands: Self::ArrowDown, Self::ArrowLeft, Self::ArrowRight, diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 5e344c2e..3d4eac39 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -108,6 +108,7 @@ impl Default for Tests { Self::from_demos(vec![ Box::::default(), Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), diff --git a/crates/egui_demo_lib/src/demo/tests/input_event_history.rs b/crates/egui_demo_lib/src/demo/tests/input_event_history.rs new file mode 100644 index 00000000..a0b3d897 --- /dev/null +++ b/crates/egui_demo_lib/src/demo/tests/input_event_history.rs @@ -0,0 +1,132 @@ +//! Show the history of all the input events to + +struct HistoryEntry { + summary: String, + entries: Vec, +} + +#[derive(Default)] +struct DeduplicatedHistory { + history: std::collections::VecDeque, +} + +impl DeduplicatedHistory { + fn add(&mut self, summary: String, full: String) { + if let Some(entry) = self.history.back_mut() { + if entry.summary == summary { + entry.entries.push(full); + return; + } + } + self.history.push_back(HistoryEntry { + summary, + entries: vec![full], + }); + if self.history.len() > 100 { + self.history.pop_front(); + } + } + + fn ui(&self, ui: &mut egui::Ui) { + egui::ScrollArea::vertical() + .auto_shrink(false) + .show(ui, |ui| { + ui.spacing_mut().item_spacing.y = 4.0; + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + + for HistoryEntry { summary, entries } in self.history.iter().rev() { + ui.horizontal(|ui| { + let response = ui.code(summary); + if entries.len() < 2 { + response + } else { + response | ui.weak(format!(" x{}", entries.len())) + } + }) + .inner + .on_hover_ui(|ui| { + ui.spacing_mut().item_spacing.y = 4.0; + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + for entry in entries.iter().rev() { + ui.code(entry); + } + }); + } + }); + } +} + +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[derive(Default)] +pub struct InputEventHistory { + #[cfg_attr(feature = "serde", serde(skip))] + history: DeduplicatedHistory, + + include_pointer_movements: bool, +} + +impl crate::Demo for InputEventHistory { + fn name(&self) -> &'static str { + "Input Event History" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + egui::Window::new(self.name()) + .default_width(800.0) + .open(open) + .resizable(true) + .scroll(false) + .show(ctx, |ui| { + use crate::View as _; + self.ui(ui); + }); + } +} + +impl crate::View for InputEventHistory { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.input(|i| { + for event in &i.raw.events { + if !self.include_pointer_movements + && matches!( + event, + egui::Event::PointerMoved(_) | egui::Event::MouseMoved(_) + ) + { + continue; + } + + let summary = event_summary(event); + let full = format!("{event:#?}"); + self.history.add(summary, full); + } + }); + + ui.vertical_centered(|ui| { + ui.add(crate::egui_github_link_file!()); + }); + + ui.label("Recent history of raw input events to egui."); + ui.label("Hover any entry for details."); + ui.checkbox( + &mut self.include_pointer_movements, + "Include pointer/mouse movements", + ); + + ui.add_space(8.0); + + self.history.ui(ui); + } +} + +fn event_summary(event: &egui::Event) -> String { + match event { + egui::Event::PointerMoved(_) => "PointerMoved { .. }".to_owned(), + egui::Event::MouseMoved(_) => "MouseMoved { .. }".to_owned(), + egui::Event::Zoom(_) => "Zoom { .. }".to_owned(), + egui::Event::Touch { phase, .. } => format!("Zoom {{ phase: {phase:?}, .. }}"), + egui::Event::MouseWheel { unit, .. } => format!("MouseWheel {{ unit: {unit:?}, .. }}"), + + _ => format!("{event:?}"), + } +} diff --git a/crates/egui_demo_lib/src/demo/tests/input_test.rs b/crates/egui_demo_lib/src/demo/tests/input_test.rs index 4b10bdf4..f6d3a310 100644 --- a/crates/egui_demo_lib/src/demo/tests/input_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/input_test.rs @@ -91,8 +91,8 @@ impl crate::View for InputTest { ui.checkbox(&mut self.late_interaction, "Use Response::interact"); ui.label("This tests how egui::Response reports events.\n\ - The different buttons are sensitive to different things.\n\ - Try interacting with them with any mouse button by clicking, double-clicking, triple-clicking, or dragging them."); + The different buttons are sensitive to different things.\n\ + Try interacting with them with any mouse button by clicking, double-clicking, triple-clicking, or dragging them."); ui.columns(4, |columns| { for (i, (sense_name, sense)) in [ diff --git a/crates/egui_demo_lib/src/demo/tests/mod.rs b/crates/egui_demo_lib/src/demo/tests/mod.rs index 8daf61fc..497f384c 100644 --- a/crates/egui_demo_lib/src/demo/tests/mod.rs +++ b/crates/egui_demo_lib/src/demo/tests/mod.rs @@ -1,5 +1,6 @@ mod cursor_test; mod id_test; +mod input_event_history; mod input_test; mod layout_test; mod manual_layout_test; @@ -8,6 +9,7 @@ mod window_resize_test; pub use cursor_test::CursorTest; pub use id_test::IdTest; +pub use input_event_history::InputEventHistory; pub use input_test::InputTest; pub use layout_test::LayoutTest; pub use manual_layout_test::ManualLayoutTest;