Skip to content

Commit

Permalink
eframe: Added App::raw_input_hook allows for the manipulation or fi…
Browse files Browse the repository at this point in the history
…ltering 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)
  • Loading branch information
varphone committed Mar 12, 2024
1 parent 00a399b commit 827fdef
Show file tree
Hide file tree
Showing 8 changed files with 382 additions and 0 deletions.
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions crates/eframe/src/epi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions crates/eframe/src/native/epi_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions examples/custom_keypad/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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",
] }
7 changes: 7 additions & 0 deletions examples/custom_keypad/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Example showing how to implements a custom keypad.

```sh
cargo run -p custom_keypad
```

![](screenshot.png)
Binary file added examples/custom_keypad/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
255 changes: 255 additions & 0 deletions examples/custom_keypad/src/keypad.rs
Original file line number Diff line number Diff line change
@@ -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()
}
}
Loading

0 comments on commit 827fdef

Please sign in to comment.