Skip to content

Commit cffa313

Browse files
committed
Added an example to demonstrates how to implement a custom virtual keyboard
1 parent c85cd2b commit cffa313

File tree

6 files changed

+362
-0
lines changed

6 files changed

+362
-0
lines changed

Cargo.lock

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/custom_keypad/Cargo.toml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "custom_keypad"
3+
version = "0.1.0"
4+
authors = ["Varphone Wong <varphone@qq.com>"]
5+
license = "MIT OR Apache-2.0"
6+
edition = "2021"
7+
rust-version = "1.72"
8+
publish = false
9+
10+
11+
[dependencies]
12+
eframe = { workspace = true, features = [
13+
"default",
14+
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
15+
] }
16+
17+
# For image support:
18+
egui_extras = { workspace = true, features = ["default", "image"] }
19+
20+
env_logger = { version = "0.10", default-features = false, features = [
21+
"auto-color",
22+
"humantime",
23+
] }

examples/custom_keypad/README.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Example showing how to implements a custom keypad.
2+
3+
```sh
4+
cargo run -p custom_keypad
5+
```
6+
7+
![](screenshot.png)

examples/custom_keypad/screenshot.png

38.9 KB
Loading

examples/custom_keypad/src/keypad.rs

+255
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
use eframe::egui::{self, pos2, vec2, Button, Ui, Vec2};
2+
3+
#[derive(Clone, Copy, Debug, Default, PartialEq)]
4+
enum Transition {
5+
#[default]
6+
None,
7+
CloseOnNextFrame,
8+
CloseImmediately,
9+
}
10+
11+
#[derive(Clone, Debug)]
12+
struct State {
13+
open: bool,
14+
closable: bool,
15+
close_on_next_frame: bool,
16+
start_pos: egui::Pos2,
17+
focus: Option<egui::Id>,
18+
events: Option<Vec<egui::Event>>,
19+
}
20+
21+
impl State {
22+
fn new() -> Self {
23+
Self {
24+
open: false,
25+
closable: false,
26+
close_on_next_frame: false,
27+
start_pos: pos2(100.0, 100.0),
28+
focus: None,
29+
events: None,
30+
}
31+
}
32+
33+
fn queue_char(&mut self, c: char) {
34+
let events = self.events.get_or_insert(vec![]);
35+
if let Some(key) = egui::Key::from_name(&c.to_string()) {
36+
events.push(egui::Event::Key {
37+
key,
38+
physical_key: Some(key),
39+
pressed: true,
40+
repeat: false,
41+
modifiers: Default::default(),
42+
});
43+
}
44+
events.push(egui::Event::Text(c.to_string()));
45+
}
46+
47+
fn queue_key(&mut self, key: egui::Key) {
48+
let events = self.events.get_or_insert(vec![]);
49+
events.push(egui::Event::Key {
50+
key,
51+
physical_key: Some(key),
52+
pressed: true,
53+
repeat: false,
54+
modifiers: Default::default(),
55+
});
56+
}
57+
}
58+
59+
impl Default for State {
60+
fn default() -> Self {
61+
Self::new()
62+
}
63+
}
64+
65+
/// A simple keypad widget.
66+
pub struct Keypad {
67+
id: egui::Id,
68+
}
69+
70+
impl Keypad {
71+
pub fn new() -> Self {
72+
Self {
73+
id: egui::Id::new("keypad"),
74+
}
75+
}
76+
77+
pub fn bump_events(&self, ctx: &egui::Context, raw_input: &mut egui::RawInput) {
78+
let events = ctx.memory_mut(|m| {
79+
m.data
80+
.get_temp_mut_or_default::<State>(self.id)
81+
.events
82+
.take()
83+
});
84+
if let Some(mut events) = events {
85+
events.append(&mut raw_input.events);
86+
raw_input.events = events;
87+
}
88+
}
89+
90+
fn buttons(ui: &mut Ui, state: &mut State) -> Transition {
91+
let mut trans = Transition::None;
92+
ui.vertical(|ui| {
93+
let window_margin = ui.spacing().window_margin;
94+
let size_1x1 = vec2(32.0, 26.0);
95+
let _size_1x2 = vec2(32.0, 52.0 + window_margin.top);
96+
let _size_2x1 = vec2(64.0 + window_margin.left, 26.0);
97+
98+
ui.spacing_mut().item_spacing = Vec2::splat(window_margin.left);
99+
100+
ui.horizontal(|ui| {
101+
if ui.add_sized(size_1x1, Button::new("1")).clicked() {
102+
state.queue_char('1');
103+
}
104+
if ui.add_sized(size_1x1, Button::new("2")).clicked() {
105+
state.queue_char('2');
106+
}
107+
if ui.add_sized(size_1x1, Button::new("3")).clicked() {
108+
state.queue_char('3');
109+
}
110+
if ui.add_sized(size_1x1, Button::new("⏮")).clicked() {
111+
state.queue_key(egui::Key::Home);
112+
}
113+
if ui.add_sized(size_1x1, Button::new("🔙")).clicked() {
114+
state.queue_key(egui::Key::Backspace);
115+
}
116+
});
117+
ui.horizontal(|ui| {
118+
if ui.add_sized(size_1x1, Button::new("4")).clicked() {
119+
state.queue_char('4');
120+
}
121+
if ui.add_sized(size_1x1, Button::new("5")).clicked() {
122+
state.queue_char('5');
123+
}
124+
if ui.add_sized(size_1x1, Button::new("6")).clicked() {
125+
state.queue_char('6');
126+
}
127+
if ui.add_sized(size_1x1, Button::new("⏭")).clicked() {
128+
state.queue_key(egui::Key::End);
129+
}
130+
if ui.add_sized(size_1x1, Button::new("⎆")).clicked() {
131+
state.queue_key(egui::Key::Enter);
132+
trans = Transition::CloseOnNextFrame;
133+
}
134+
});
135+
ui.horizontal(|ui| {
136+
if ui.add_sized(size_1x1, Button::new("7")).clicked() {
137+
state.queue_char('7');
138+
}
139+
if ui.add_sized(size_1x1, Button::new("8")).clicked() {
140+
state.queue_char('8');
141+
}
142+
if ui.add_sized(size_1x1, Button::new("9")).clicked() {
143+
state.queue_char('9');
144+
}
145+
if ui.add_sized(size_1x1, Button::new("⏶")).clicked() {
146+
state.queue_key(egui::Key::ArrowUp);
147+
}
148+
if ui.add_sized(size_1x1, Button::new("⌨")).clicked() {
149+
trans = Transition::CloseImmediately;
150+
}
151+
});
152+
ui.horizontal(|ui| {
153+
if ui.add_sized(size_1x1, Button::new("0")).clicked() {
154+
state.queue_char('0');
155+
}
156+
if ui.add_sized(size_1x1, Button::new(".")).clicked() {
157+
state.queue_char('.');
158+
}
159+
if ui.add_sized(size_1x1, Button::new("⏴")).clicked() {
160+
state.queue_key(egui::Key::ArrowLeft);
161+
}
162+
if ui.add_sized(size_1x1, Button::new("⏷")).clicked() {
163+
state.queue_key(egui::Key::ArrowDown);
164+
}
165+
if ui.add_sized(size_1x1, Button::new("⏵")).clicked() {
166+
state.queue_key(egui::Key::ArrowRight);
167+
}
168+
});
169+
});
170+
171+
trans
172+
}
173+
174+
pub fn show(&self, ctx: &egui::Context) {
175+
let (focus, mut state) = ctx.memory(|m| {
176+
(
177+
m.focus(),
178+
m.data.get_temp::<State>(self.id).unwrap_or_default(),
179+
)
180+
});
181+
182+
let mut is_first_show = false;
183+
if ctx.wants_keyboard_input() && state.focus != focus {
184+
let y = ctx.style().spacing.interact_size.y * 1.25;
185+
state.open = true;
186+
state.start_pos = ctx.input(|i| {
187+
i.pointer
188+
.hover_pos()
189+
.map_or(pos2(100.0, 100.0), |p| p + vec2(0.0, y))
190+
});
191+
state.focus = focus;
192+
is_first_show = true;
193+
}
194+
195+
if state.close_on_next_frame {
196+
state.open = false;
197+
state.close_on_next_frame = false;
198+
state.focus = None;
199+
}
200+
201+
let mut open = state.open;
202+
203+
let win = egui::Window::new("⌨ Keypad");
204+
let win = if is_first_show {
205+
win.current_pos(state.start_pos)
206+
} else {
207+
win.default_pos(state.start_pos)
208+
};
209+
let resp = win
210+
.movable(true)
211+
.resizable(false)
212+
.open(&mut open)
213+
.show(ctx, |ui| Self::buttons(ui, &mut state));
214+
215+
state.open = open;
216+
217+
if let Some(resp) = resp {
218+
match resp.inner {
219+
Some(Transition::CloseOnNextFrame) => {
220+
state.close_on_next_frame = true;
221+
}
222+
Some(Transition::CloseImmediately) => {
223+
state.open = false;
224+
state.focus = None;
225+
}
226+
_ => {}
227+
}
228+
if !state.closable && resp.response.hovered() {
229+
state.closable = true;
230+
}
231+
if state.closable && resp.response.clicked_elsewhere() {
232+
state.open = false;
233+
state.closable = false;
234+
state.focus = None;
235+
}
236+
if is_first_show {
237+
ctx.move_to_top(resp.response.layer_id);
238+
}
239+
}
240+
241+
if let (true, Some(focus)) = (state.open, state.focus) {
242+
ctx.memory_mut(|m| {
243+
m.request_focus(focus);
244+
});
245+
}
246+
247+
ctx.memory_mut(|m| m.data.insert_temp(self.id, state));
248+
}
249+
}
250+
251+
impl Default for Keypad {
252+
fn default() -> Self {
253+
Self::new()
254+
}
255+
}

examples/custom_keypad/src/main.rs

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
2+
use eframe::egui;
3+
4+
mod keypad;
5+
use keypad::Keypad;
6+
7+
fn main() -> Result<(), eframe::Error> {
8+
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
9+
let options = eframe::NativeOptions {
10+
viewport: egui::ViewportBuilder::default().with_inner_size([640.0, 480.0]),
11+
..Default::default()
12+
};
13+
eframe::run_native(
14+
"Custom Keypad App",
15+
options,
16+
Box::new(|cc| {
17+
// Use the dark theme
18+
cc.egui_ctx.set_visuals(egui::Visuals::dark());
19+
// This gives us image support:
20+
egui_extras::install_image_loaders(&cc.egui_ctx);
21+
22+
Box::<MyApp>::default()
23+
}),
24+
)
25+
}
26+
27+
struct MyApp {
28+
name: String,
29+
age: u32,
30+
keypad: Keypad,
31+
}
32+
33+
impl MyApp {}
34+
35+
impl Default for MyApp {
36+
fn default() -> Self {
37+
Self {
38+
name: "Arthur".to_owned(),
39+
age: 42,
40+
keypad: Keypad::new(),
41+
}
42+
}
43+
}
44+
45+
impl eframe::App for MyApp {
46+
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
47+
egui::Window::new("Custom Keypad")
48+
.default_pos([100.0, 100.0])
49+
.title_bar(true)
50+
.show(ctx, |ui| {
51+
ui.horizontal(|ui| {
52+
ui.label("Your name: ");
53+
ui.text_edit_singleline(&mut self.name);
54+
});
55+
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
56+
if ui.button("Increment").clicked() {
57+
self.age += 1;
58+
}
59+
ui.label(format!("Hello '{}', age {}", self.name, self.age));
60+
});
61+
62+
self.keypad.show(ctx);
63+
}
64+
65+
fn filter_raw_input_inplace(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) {
66+
self.keypad.bump_events(ctx, raw_input);
67+
}
68+
}

0 commit comments

Comments
 (0)