diff --git a/Cargo.lock b/Cargo.lock index 92948c45..ba8d5a17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1072,6 +1072,15 @@ dependencies = [ "env_logger", ] +[[package]] +name = "custom_keypad" +version = "0.1.0" +dependencies = [ + "eframe", + "egui_extras", + "env_logger", +] + [[package]] name = "custom_plot_manipulation" version = "0.1.0" diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index e26bece3..16a8a689 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -196,6 +196,24 @@ pub trait App { fn persist_egui_memory(&self) -> bool { true } + + /// A hook for manipulating or filtering raw input before it is processed by [`Self::update`]. + /// + /// This function provides a way to modify or filter input events before they are processed by egui. + /// + /// It can be used to prevent specific keyboard shortcuts or mouse events from being processed by egui. + /// + /// Additionally, it can be used to inject custom keyboard or mouse events into the input stream, which can be useful for implementing features like a virtual keyboard. + /// + /// # Arguments + /// + /// * `_ctx` - The context of the egui, which provides access to the current state of the egui. + /// * `_raw_input` - The raw input events that are about to be processed. This can be modified to change the input that egui processes. + /// + /// # Note + /// + /// This function does not return a value. Any changes to the input should be made directly to `_raw_input`. + fn raw_input_hook(&mut self, _ctx: &egui::Context, _raw_input: &mut egui::RawInput) {} } /// Selects the level of hardware graphics acceleration. diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index ee54b521..f27d0112 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -274,6 +274,8 @@ impl EpiIntegration { let close_requested = raw_input.viewport().close_requested(); + app.raw_input_hook(&self.egui_ctx, &mut raw_input); + let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { if let Some(viewport_ui_cb) = viewport_ui_cb { // Child viewport diff --git a/examples/custom_keypad/Cargo.toml b/examples/custom_keypad/Cargo.toml new file mode 100644 index 00000000..1557b35c --- /dev/null +++ b/examples/custom_keypad/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "custom_keypad" +version = "0.1.0" +authors = ["Varphone Wong "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.72" +publish = false + + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO +] } + +# For image support: +egui_extras = { workspace = true, features = ["default", "image"] } + +env_logger = { version = "0.10", default-features = false, features = [ + "auto-color", + "humantime", +] } diff --git a/examples/custom_keypad/README.md b/examples/custom_keypad/README.md new file mode 100644 index 00000000..9e5cdf7e --- /dev/null +++ b/examples/custom_keypad/README.md @@ -0,0 +1,7 @@ +Example showing how to implements a custom keypad. + +```sh +cargo run -p custom_keypad +``` + +![](screenshot.png) diff --git a/examples/custom_keypad/screenshot.png b/examples/custom_keypad/screenshot.png new file mode 100644 index 00000000..632459e5 Binary files /dev/null and b/examples/custom_keypad/screenshot.png differ diff --git a/examples/custom_keypad/src/keypad.rs b/examples/custom_keypad/src/keypad.rs new file mode 100644 index 00000000..81800f47 --- /dev/null +++ b/examples/custom_keypad/src/keypad.rs @@ -0,0 +1,255 @@ +use eframe::egui::{self, pos2, vec2, Button, Ui, Vec2}; + +#[derive(Clone, Copy, Debug, Default, PartialEq)] +enum Transition { + #[default] + None, + CloseOnNextFrame, + CloseImmediately, +} + +#[derive(Clone, Debug)] +struct State { + open: bool, + closable: bool, + close_on_next_frame: bool, + start_pos: egui::Pos2, + focus: Option, + events: Option>, +} + +impl State { + fn new() -> Self { + Self { + open: false, + closable: false, + close_on_next_frame: false, + start_pos: pos2(100.0, 100.0), + focus: None, + events: None, + } + } + + fn queue_char(&mut self, c: char) { + let events = self.events.get_or_insert(vec![]); + if let Some(key) = egui::Key::from_name(&c.to_string()) { + events.push(egui::Event::Key { + key, + physical_key: Some(key), + pressed: true, + repeat: false, + modifiers: Default::default(), + }); + } + events.push(egui::Event::Text(c.to_string())); + } + + fn queue_key(&mut self, key: egui::Key) { + let events = self.events.get_or_insert(vec![]); + events.push(egui::Event::Key { + key, + physical_key: Some(key), + pressed: true, + repeat: false, + modifiers: Default::default(), + }); + } +} + +impl Default for State { + fn default() -> Self { + Self::new() + } +} + +/// A simple keypad widget. +pub struct Keypad { + id: egui::Id, +} + +impl Keypad { + pub fn new() -> Self { + Self { + id: egui::Id::new("keypad"), + } + } + + pub fn bump_events(&self, ctx: &egui::Context, raw_input: &mut egui::RawInput) { + let events = ctx.memory_mut(|m| { + m.data + .get_temp_mut_or_default::(self.id) + .events + .take() + }); + if let Some(mut events) = events { + events.append(&mut raw_input.events); + raw_input.events = events; + } + } + + fn buttons(ui: &mut Ui, state: &mut State) -> Transition { + let mut trans = Transition::None; + ui.vertical(|ui| { + let window_margin = ui.spacing().window_margin; + let size_1x1 = vec2(32.0, 26.0); + let _size_1x2 = vec2(32.0, 52.0 + window_margin.top); + let _size_2x1 = vec2(64.0 + window_margin.left, 26.0); + + ui.spacing_mut().item_spacing = Vec2::splat(window_margin.left); + + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("1")).clicked() { + state.queue_char('1'); + } + if ui.add_sized(size_1x1, Button::new("2")).clicked() { + state.queue_char('2'); + } + if ui.add_sized(size_1x1, Button::new("3")).clicked() { + state.queue_char('3'); + } + if ui.add_sized(size_1x1, Button::new("⏮")).clicked() { + state.queue_key(egui::Key::Home); + } + if ui.add_sized(size_1x1, Button::new("🔙")).clicked() { + state.queue_key(egui::Key::Backspace); + } + }); + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("4")).clicked() { + state.queue_char('4'); + } + if ui.add_sized(size_1x1, Button::new("5")).clicked() { + state.queue_char('5'); + } + if ui.add_sized(size_1x1, Button::new("6")).clicked() { + state.queue_char('6'); + } + if ui.add_sized(size_1x1, Button::new("⏭")).clicked() { + state.queue_key(egui::Key::End); + } + if ui.add_sized(size_1x1, Button::new("⎆")).clicked() { + state.queue_key(egui::Key::Enter); + trans = Transition::CloseOnNextFrame; + } + }); + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("7")).clicked() { + state.queue_char('7'); + } + if ui.add_sized(size_1x1, Button::new("8")).clicked() { + state.queue_char('8'); + } + if ui.add_sized(size_1x1, Button::new("9")).clicked() { + state.queue_char('9'); + } + if ui.add_sized(size_1x1, Button::new("⏶")).clicked() { + state.queue_key(egui::Key::ArrowUp); + } + if ui.add_sized(size_1x1, Button::new("⌨")).clicked() { + trans = Transition::CloseImmediately; + } + }); + ui.horizontal(|ui| { + if ui.add_sized(size_1x1, Button::new("0")).clicked() { + state.queue_char('0'); + } + if ui.add_sized(size_1x1, Button::new(".")).clicked() { + state.queue_char('.'); + } + if ui.add_sized(size_1x1, Button::new("⏴")).clicked() { + state.queue_key(egui::Key::ArrowLeft); + } + if ui.add_sized(size_1x1, Button::new("⏷")).clicked() { + state.queue_key(egui::Key::ArrowDown); + } + if ui.add_sized(size_1x1, Button::new("⏵")).clicked() { + state.queue_key(egui::Key::ArrowRight); + } + }); + }); + + trans + } + + pub fn show(&self, ctx: &egui::Context) { + let (focus, mut state) = ctx.memory(|m| { + ( + m.focus(), + m.data.get_temp::(self.id).unwrap_or_default(), + ) + }); + + let mut is_first_show = false; + if ctx.wants_keyboard_input() && state.focus != focus { + let y = ctx.style().spacing.interact_size.y * 1.25; + state.open = true; + state.start_pos = ctx.input(|i| { + i.pointer + .hover_pos() + .map_or(pos2(100.0, 100.0), |p| p + vec2(0.0, y)) + }); + state.focus = focus; + is_first_show = true; + } + + if state.close_on_next_frame { + state.open = false; + state.close_on_next_frame = false; + state.focus = None; + } + + let mut open = state.open; + + let win = egui::Window::new("⌨ Keypad"); + let win = if is_first_show { + win.current_pos(state.start_pos) + } else { + win.default_pos(state.start_pos) + }; + let resp = win + .movable(true) + .resizable(false) + .open(&mut open) + .show(ctx, |ui| Self::buttons(ui, &mut state)); + + state.open = open; + + if let Some(resp) = resp { + match resp.inner { + Some(Transition::CloseOnNextFrame) => { + state.close_on_next_frame = true; + } + Some(Transition::CloseImmediately) => { + state.open = false; + state.focus = None; + } + _ => {} + } + if !state.closable && resp.response.hovered() { + state.closable = true; + } + if state.closable && resp.response.clicked_elsewhere() { + state.open = false; + state.closable = false; + state.focus = None; + } + if is_first_show { + ctx.move_to_top(resp.response.layer_id); + } + } + + if let (true, Some(focus)) = (state.open, state.focus) { + ctx.memory_mut(|m| { + m.request_focus(focus); + }); + } + + ctx.memory_mut(|m| m.data.insert_temp(self.id, state)); + } +} + +impl Default for Keypad { + fn default() -> Self { + Self::new() + } +} diff --git a/examples/custom_keypad/src/main.rs b/examples/custom_keypad/src/main.rs new file mode 100644 index 00000000..5cb26240 --- /dev/null +++ b/examples/custom_keypad/src/main.rs @@ -0,0 +1,68 @@ +// #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release +use eframe::egui; + +mod keypad; +use keypad::Keypad; + +fn main() -> Result<(), eframe::Error> { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([640.0, 480.0]), + ..Default::default() + }; + eframe::run_native( + "Custom Keypad App", + options, + Box::new(|cc| { + // Use the dark theme + cc.egui_ctx.set_visuals(egui::Visuals::dark()); + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + + Box::::default() + }), + ) +} + +struct MyApp { + name: String, + age: u32, + keypad: Keypad, +} + +impl MyApp {} + +impl Default for MyApp { + fn default() -> Self { + Self { + name: "Arthur".to_owned(), + age: 42, + keypad: Keypad::new(), + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::Window::new("Custom Keypad") + .default_pos([100.0, 100.0]) + .title_bar(true) + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.label("Your name: "); + ui.text_edit_singleline(&mut self.name); + }); + ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); + if ui.button("Increment").clicked() { + self.age += 1; + } + ui.label(format!("Hello '{}', age {}", self.name, self.age)); + }); + + self.keypad.show(ctx); + } + + fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) { + self.keypad.bump_events(ctx, raw_input); + } +}