summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVarphone Wong <varphone@qq.com>2024-03-12 18:06:08 +0800
committerGitHub <noreply@github.com>2024-03-12 11:06:08 +0100
commit827fdefd8343f0e92f203d7dfafa34cb5cf91cc7 (patch)
treebe9522bb286254e2cc758c57009c0435284ab049
parent00a399b2f7eae6bcbe968bdaa7bb77689043bba2 (diff)
eframe: Added `App::raw_input_hook` allows for the manipulation or filtering of raw input events (#4008)
# What's New * eframe: Added `App::raw_input_hook` allows for the manipulation or filtering of raw input events A filter applied to raw input before [`Self::update`] This allows for the manipulation or filtering of input events before they are processed by egui. This can be used to exclude specific keyboard shortcuts, mouse events, etc. Additionally, it can be used to add custom keyboard or mouse events generated by a virtual keyboard. * examples: Added an example to demonstrates how to implement a custom virtual keyboard. [eframe-custom-keypad.webm](https://github.com/emilk/egui/assets/1274171/a9dc8e34-2c35-4172-b7ef-41010b794fb8)
-rw-r--r--Cargo.lock9
-rw-r--r--crates/eframe/src/epi.rs18
-rw-r--r--crates/eframe/src/native/epi_integration.rs2
-rw-r--r--examples/custom_keypad/Cargo.toml23
-rw-r--r--examples/custom_keypad/README.md7
-rw-r--r--examples/custom_keypad/screenshot.pngbin0 -> 39832 bytes
-rw-r--r--examples/custom_keypad/src/keypad.rs255
-rw-r--r--examples/custom_keypad/src/main.rs68
8 files changed, 382 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 92948c45..ba8d5a17 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1073,6 +1073,15 @@ dependencies = [
]
[[package]]
+name = "custom_keypad"
+version = "0.1.0"
+dependencies = [
+ "eframe",
+ "egui_extras",
+ "env_logger",
+]
+
+[[package]]
name = "custom_plot_manipulation"
version = "0.1.0"
dependencies = [
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 <varphone@qq.com>"]
+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
--- /dev/null
+++ b/examples/custom_keypad/screenshot.png
Binary files 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<egui::Id>,
+ events: Option<Vec<egui::Event>>,
+}
+
+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::<State>(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::<State>(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::<MyApp>::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);
+ }
+}