Skip to content

Commit 3d6a5b7

Browse files
Virtual keyboard widget (#20350)
# Objective Minimal virtual keyboard widget implementation. ## Solution Added a `virtual_keyboard` module to `bevy_feathers::controls` with a single function also called `virtual_keyboard`. `virtual_keyboard` takes a list of rows of keys and a system ID for the key press handler and returns a bundle of buttons representing the keys arranged into rows. Lot of limitations: * No modifier keys. * No styling for individual keyboard buttons. * The keyboard buttons can only display text, no image support. * The keys don't repeat when held. * Doesn't have any navigation support for use with a controller. * Doesn't support tall keys that span multiple rows. # I deliberately didn't spend even a moment thinking about the design, this is just meant to be a super basic implementation that I can hook up to the text input widget for testing. Still looks quite neat, even so. ## Testing ``` cargo run --example virtual_keyboard --features="experimental_bevy_feathers" ``` --- ## Showcase <img width="1042" height="573" alt="virtual_keyboard" src="https://github.com/user-attachments/assets/f882cbec-ccdf-4df1-a32e-255c063454d4" /> --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
1 parent 8823aaa commit 3d6a5b7

File tree

6 files changed

+169
-2
lines changed

6 files changed

+169
-2
lines changed

Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3603,6 +3603,18 @@ description = "Demonstrates how to control the relative depth (z-position) of UI
36033603
category = "UI (User Interface)"
36043604
wasm = true
36053605

3606+
[[example]]
3607+
name = "virtual_keyboard"
3608+
path = "examples/ui/virtual_keyboard.rs"
3609+
doc-scrape-examples = true
3610+
required-features = ["experimental_bevy_feathers"]
3611+
3612+
[package.metadata.example.virtual_keyboard]
3613+
name = "Virtual Keyboard"
3614+
description = "Example demonstrating a virtual keyboard widget"
3615+
category = "UI (User Interface)"
3616+
wasm = true
3617+
36063618
[[example]]
36073619
name = "ui_scaling"
36083620
path = "examples/ui/ui_scaling.rs"

crates/bevy_feathers/src/controls/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ mod color_swatch;
77
mod radio;
88
mod slider;
99
mod toggle_switch;
10+
mod virtual_keyboard;
1011

1112
pub use button::{button, ButtonPlugin, ButtonProps, ButtonVariant};
1213
pub use checkbox::{checkbox, CheckboxPlugin, CheckboxProps};
1314
pub use color_swatch::{color_swatch, ColorSwatch, ColorSwatchFg};
1415
pub use radio::{radio, RadioPlugin};
1516
pub use slider::{slider, SliderPlugin, SliderProps};
1617
pub use toggle_switch::{toggle_switch, ToggleSwitchPlugin, ToggleSwitchProps};
18+
pub use virtual_keyboard::virtual_keyboard;
1719

1820
use crate::alpha_pattern::AlphaPatternPlugin;
1921

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use bevy_core_widgets::{Activate, Callback};
2+
use bevy_ecs::{
3+
bundle::Bundle,
4+
component::Component,
5+
hierarchy::{ChildOf, Children},
6+
relationship::RelatedSpawner,
7+
spawn::{Spawn, SpawnRelated, SpawnWith},
8+
system::{In, SystemId},
9+
};
10+
use bevy_input_focus::tab_navigation::TabGroup;
11+
use bevy_ui::Node;
12+
use bevy_ui::Val;
13+
use bevy_ui::{widget::Text, FlexDirection};
14+
15+
use crate::controls::{button, ButtonProps};
16+
17+
/// Function to spawn a virtual keyboard
18+
pub fn virtual_keyboard<T>(
19+
keys: impl Iterator<Item = Vec<(String, T)>> + Send + Sync + 'static,
20+
on_key_press: SystemId<In<Activate>>,
21+
) -> impl Bundle
22+
where
23+
T: Component,
24+
{
25+
(
26+
Node {
27+
flex_direction: FlexDirection::Column,
28+
row_gap: Val::Px(4.),
29+
..Default::default()
30+
},
31+
TabGroup::new(0),
32+
Children::spawn((SpawnWith(move |parent: &mut RelatedSpawner<ChildOf>| {
33+
for row in keys {
34+
parent.spawn((
35+
Node {
36+
flex_direction: FlexDirection::Row,
37+
column_gap: Val::Px(4.),
38+
..Default::default()
39+
},
40+
Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner<ChildOf>| {
41+
for (label, key_id) in row.into_iter() {
42+
parent.spawn(button(
43+
ButtonProps {
44+
on_click: Callback::System(on_key_press),
45+
..Default::default()
46+
},
47+
(key_id,),
48+
Spawn(Text::new(label)),
49+
));
50+
}
51+
})),
52+
));
53+
}
54+
}),)),
55+
)
56+
}

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,7 @@ Example | Description
584584
[UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements
585585
[Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates
586586
[Viewport Node](../examples/ui/viewport_node.rs) | Demonstrates how to create a viewport node with picking support
587+
[Virtual Keyboard](../examples/ui/virtual_keyboard.rs) | Example demonstrating a virtual keyboard widget
587588
[Window Fallthrough](../examples/ui/window_fallthrough.rs) | Illustrates how to access `winit::window::Window`'s `hittest` functionality.
588589

589590
### Usage

examples/ui/virtual_keyboard.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//! Virtual keyboard example
2+
3+
use bevy::{
4+
color::palettes::css::NAVY,
5+
core_widgets::{Activate, CoreWidgetsPlugins},
6+
ecs::relationship::RelatedSpawnerCommands,
7+
feathers::{
8+
controls::virtual_keyboard, dark_theme::create_dark_theme, theme::UiTheme, FeathersPlugin,
9+
},
10+
input_focus::{tab_navigation::TabNavigationPlugin, InputDispatchPlugin},
11+
prelude::*,
12+
winit::WinitSettings,
13+
};
14+
15+
fn main() {
16+
App::new()
17+
.add_plugins((
18+
DefaultPlugins,
19+
CoreWidgetsPlugins,
20+
InputDispatchPlugin,
21+
TabNavigationPlugin,
22+
FeathersPlugin,
23+
))
24+
.insert_resource(UiTheme(create_dark_theme()))
25+
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
26+
.insert_resource(WinitSettings::desktop_app())
27+
.add_systems(Startup, setup)
28+
.run();
29+
}
30+
31+
#[derive(Component)]
32+
struct VirtualKey(String);
33+
34+
fn on_virtual_key_pressed(
35+
In(Activate(virtual_key_entity)): In<Activate>,
36+
virtual_key_query: Query<&VirtualKey>,
37+
) {
38+
if let Ok(VirtualKey(label)) = virtual_key_query.get(virtual_key_entity) {
39+
println!("key pressed: {label}");
40+
}
41+
}
42+
43+
fn setup(mut commands: Commands) {
44+
// ui camera
45+
commands.spawn(Camera2d);
46+
let callback = commands.register_system(on_virtual_key_pressed);
47+
48+
let layout = [
49+
vec!["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", ".", ","],
50+
vec!["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"],
51+
vec!["A", "S", "D", "F", "G", "H", "J", "K", "L", "'"],
52+
vec!["Z", "X", "C", "V", "B", "N", "M", "-", "/"],
53+
vec!["space", "enter", "backspace"],
54+
vec!["left", "right", "up", "down", "home", "end"],
55+
];
56+
57+
let keys_iter = layout.into_iter().map(|row| {
58+
row.into_iter()
59+
.map(|label| {
60+
let label_string = label.to_string();
61+
(label_string.clone(), VirtualKey(label_string))
62+
})
63+
.collect()
64+
});
65+
66+
commands
67+
.spawn(Node {
68+
width: Val::Percent(100.0),
69+
height: Val::Percent(100.0),
70+
align_items: AlignItems::End,
71+
justify_content: JustifyContent::Center,
72+
..default()
73+
})
74+
.with_children(|parent: &mut RelatedSpawnerCommands<ChildOf>| {
75+
parent
76+
.spawn((
77+
Node {
78+
flex_direction: FlexDirection::Column,
79+
border: Val::Px(5.).into(),
80+
row_gap: Val::Px(5.),
81+
padding: Val::Px(5.).into(),
82+
align_items: AlignItems::Center,
83+
margin: Val::Px(25.).into(),
84+
..Default::default()
85+
},
86+
BackgroundColor(NAVY.into()),
87+
BorderColor::all(Color::WHITE),
88+
BorderRadius::all(Val::Px(10.)),
89+
))
90+
.with_children(|parent: &mut RelatedSpawnerCommands<ChildOf>| {
91+
parent.spawn(Text::new("virtual keyboard"));
92+
parent.spawn(virtual_keyboard(keys_iter, callback));
93+
});
94+
});
95+
}

release-content/release-notes/feathers.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Bevy Feathers
3-
authors: ["@viridia", "@Atlas16A"]
4-
pull_requests: [19730, 19900, 19928, 20237, 20169]
3+
authors: ["@viridia", "@Atlas16A", "@ickshonpe"]
4+
pull_requests: [19730, 19900, 19928, 20237, 20169, 20350]
55
---
66

77
To make it easier for Bevy engine developers and third-party tool creators to make comfortable, visually cohesive tooling,
@@ -15,6 +15,7 @@ we're pleased to introduce "Feathers" - a comprehensive widget set that offers:
1515
- Robust theming support ensuring consistent visual styling across applications
1616
- Accessibility features with built-in screen reader and assistive technology support
1717
- Interactive cursor behavior that changes appropriately when hovering over widgets
18+
- A virtual keyboard suitable for touchscreen text input
1819

1920
Feathers isn't meant as a toolkit for building exciting and cool game UIs: it has a somewhat plain
2021
and utilitarian look and feel suitable for editors and graphical utilities. That being said, using

0 commit comments

Comments
 (0)