use bevy::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, States, Hash)]
pub enum VirtualKeyboardState {
#[default]
Hidden,
Visible,
}
#[derive(Debug, Clone, Resource)]
pub struct VirtualKeyboardConfig {
pub row1: String,
pub row2: String,
pub row3: String,
pub numbers: String,
pub symbols1: String,
pub symbols2: String,
pub symbols3: String,
pub key_width: f32,
pub key_height: f32,
pub key_spacing: f32,
pub background_color: Color,
pub key_color: Color,
pub key_hover_color: Color,
pub key_pressed_color: Color,
pub text_color: Color,
}
impl Default for VirtualKeyboardConfig {
fn default() -> Self {
Self {
row1: "qwertyuiop".to_string(),
row2: "asdfghjkl'".to_string(),
row3: "zxcvbnm,.?".to_string(),
numbers: "1234567890".to_string(),
symbols1: "!@#$%^&*()".to_string(),
symbols2: "`~_-+=:;'\"".to_string(),
symbols3: "<>,.?/\\|".to_string(),
key_width: 60.0,
key_height: 50.0,
key_spacing: 5.0,
background_color: Color::srgba(0.0, 0.0, 0.0, 0.9),
key_color: Color::srgb(0.2, 0.2, 0.2),
key_hover_color: Color::srgb(0.3, 0.3, 0.3),
key_pressed_color: Color::srgb(0.1, 0.4, 0.8),
text_color: Color::WHITE,
}
}
}
#[derive(Debug, Clone, Default, Resource)]
pub struct VirtualKeyboard {
pub buffer: String,
pub max_length: Option<usize>,
pub cursor: usize,
pub shift_active: bool,
pub current_page: usize,
pub focused_key: usize,
pub keys_per_row: usize,
pub prompt: String,
pub allow: Option<String>,
pub exclude: Option<String>,
}
impl VirtualKeyboard {
pub fn new(prompt: impl Into<String>) -> Self {
Self {
prompt: prompt.into(),
keys_per_row: 10,
..default()
}
}
#[must_use]
pub fn with_max_length(mut self, length: usize) -> Self {
self.max_length = Some(length);
self
}
#[must_use]
pub fn with_allow(mut self, chars: impl Into<String>) -> Self {
self.allow = Some(chars.into());
self
}
#[must_use]
pub fn with_exclude(mut self, chars: impl Into<String>) -> Self {
self.exclude = Some(chars.into());
self
}
#[must_use]
pub fn with_value(mut self, value: impl Into<String>) -> Self {
self.buffer = value.into();
self.cursor = self.buffer.len();
self
}
#[must_use]
pub fn is_char_allowed(&self, c: char) -> bool {
if let Some(ref allow) = self.allow
&& !allow.contains(c)
{
return false;
}
if let Some(ref exclude) = self.exclude
&& exclude.contains(c)
{
return false;
}
true
}
pub fn add_char(&mut self, c: char) {
if !self.is_char_allowed(c) {
return;
}
if let Some(max) = self.max_length
&& self.buffer.len() >= max
{
return;
}
let c = if self.shift_active {
c.to_uppercase().next().unwrap_or(c)
} else {
c
};
self.buffer.insert(self.cursor, c);
self.cursor += 1;
self.shift_active = false;
}
pub fn backspace(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
self.buffer.remove(self.cursor);
}
}
pub fn cursor_left(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
}
}
pub fn cursor_right(&mut self) {
if self.cursor < self.buffer.len() {
self.cursor += 1;
}
}
pub fn toggle_shift(&mut self) {
self.shift_active = !self.shift_active;
}
pub fn toggle_page(&mut self) {
self.current_page = usize::from(self.current_page == 0);
}
pub fn add_space(&mut self) {
self.add_char(' ');
}
pub fn clear(&mut self) {
self.buffer.clear();
self.cursor = 0;
}
#[must_use]
pub fn value(&self) -> &str {
&self.buffer
}
pub fn move_focus(&mut self, direction: FocusDirection, total_keys: usize) {
match direction {
FocusDirection::Up => {
if self.focused_key >= self.keys_per_row {
self.focused_key -= self.keys_per_row;
}
}
FocusDirection::Down => {
if self.focused_key + self.keys_per_row < total_keys {
self.focused_key += self.keys_per_row;
}
}
FocusDirection::Left => {
if self.focused_key > 0 {
self.focused_key -= 1;
}
}
FocusDirection::Right => {
if self.focused_key + 1 < total_keys {
self.focused_key += 1;
}
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FocusDirection {
Up,
Down,
Left,
Right,
}
#[derive(Debug, Clone, Message)]
pub struct ShowVirtualKeyboard {
pub keyboard: VirtualKeyboard,
}
#[derive(Debug, Clone, Message)]
pub struct HideVirtualKeyboard;
#[derive(Debug, Clone, Message)]
pub struct VirtualKeyboardEvent {
pub value: String,
pub confirmed: bool,
}
#[derive(Debug, Clone, Component)]
pub struct VirtualKey {
pub character: char,
pub index: usize,
}
#[derive(Debug, Clone, Component)]
pub struct VirtualKeyboardRoot;
#[derive(Debug, Clone, Component)]
pub struct VirtualKeyboardInput;
pub fn handle_show_keyboard(
mut events: MessageReader<ShowVirtualKeyboard>,
mut keyboard: ResMut<VirtualKeyboard>,
mut next_state: ResMut<NextState<VirtualKeyboardState>>,
) {
for event in events.read() {
*keyboard = event.keyboard.clone();
next_state.set(VirtualKeyboardState::Visible);
}
}
pub fn handle_hide_keyboard(
mut events: MessageReader<HideVirtualKeyboard>,
mut next_state: ResMut<NextState<VirtualKeyboardState>>,
) {
for _ in events.read() {
next_state.set(VirtualKeyboardState::Hidden);
}
}
pub fn handle_keyboard_input(
mut keyboard: ResMut<VirtualKeyboard>,
mut keyboard_events: MessageWriter<VirtualKeyboardEvent>,
mut hide_events: MessageWriter<HideVirtualKeyboard>,
gamepads: Query<&Gamepad>,
config: Res<VirtualKeyboardConfig>,
) {
for gamepad in gamepads.iter() {
if gamepad.just_pressed(GamepadButton::DPadUp) {
keyboard.move_focus(FocusDirection::Up, 40); }
if gamepad.just_pressed(GamepadButton::DPadDown) {
keyboard.move_focus(FocusDirection::Down, 40);
}
if gamepad.just_pressed(GamepadButton::DPadLeft) {
keyboard.move_focus(FocusDirection::Left, 40);
}
if gamepad.just_pressed(GamepadButton::DPadRight) {
keyboard.move_focus(FocusDirection::Right, 40);
}
if gamepad.just_pressed(GamepadButton::South) {
let rows = if keyboard.current_page == 0 {
vec![&config.numbers, &config.row1, &config.row2, &config.row3]
} else {
vec![
&config.numbers,
&config.symbols1,
&config.symbols2,
&config.symbols3,
]
};
let mut current_index = 0;
for row in rows {
for c in row.chars() {
if current_index == keyboard.focused_key {
keyboard.add_char(c);
break;
}
current_index += 1;
}
}
}
if gamepad.just_pressed(GamepadButton::West) {
keyboard.backspace();
}
if gamepad.just_pressed(GamepadButton::North) {
keyboard.add_space();
}
if gamepad.just_pressed(GamepadButton::East) {
keyboard_events.write(VirtualKeyboardEvent {
value: keyboard.buffer.clone(),
confirmed: false,
});
hide_events.write(HideVirtualKeyboard);
}
if gamepad.just_pressed(GamepadButton::Start) {
keyboard_events.write(VirtualKeyboardEvent {
value: keyboard.buffer.clone(),
confirmed: true,
});
hide_events.write(HideVirtualKeyboard);
}
if gamepad.just_pressed(GamepadButton::LeftTrigger2) {
keyboard.toggle_shift();
}
if gamepad.just_pressed(GamepadButton::LeftThumb) {
keyboard.toggle_page();
}
if gamepad.just_pressed(GamepadButton::LeftTrigger) {
keyboard.cursor_left();
}
if gamepad.just_pressed(GamepadButton::RightTrigger) {
keyboard.cursor_right();
}
}
}
pub(crate) fn add_virtual_keyboard_systems(app: &mut App) {
app.init_state::<VirtualKeyboardState>()
.init_resource::<VirtualKeyboard>()
.init_resource::<VirtualKeyboardConfig>()
.add_message::<ShowVirtualKeyboard>()
.add_message::<HideVirtualKeyboard>()
.add_message::<VirtualKeyboardEvent>()
.add_systems(Update, (handle_show_keyboard, handle_hide_keyboard))
.add_systems(
Update,
handle_keyboard_input.run_if(in_state(VirtualKeyboardState::Visible)),
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_virtual_keyboard_state_variants() {
assert_eq!(
VirtualKeyboardState::default(),
VirtualKeyboardState::Hidden
);
assert_ne!(VirtualKeyboardState::Hidden, VirtualKeyboardState::Visible);
}
#[test]
fn test_virtual_keyboard_config_default() {
let config = VirtualKeyboardConfig::default();
assert_eq!(config.row1, "qwertyuiop");
assert_eq!(config.row2, "asdfghjkl'");
assert_eq!(config.row3, "zxcvbnm,.?");
assert_eq!(config.numbers, "1234567890");
assert_eq!(config.key_width, 60.0);
assert_eq!(config.key_height, 50.0);
assert_eq!(config.key_spacing, 5.0);
}
#[test]
fn test_virtual_keyboard_new() {
let kb = VirtualKeyboard::new("Enter name:");
assert_eq!(kb.prompt, "Enter name:");
assert_eq!(kb.keys_per_row, 10);
assert_eq!(kb.buffer, "");
assert_eq!(kb.cursor, 0);
assert!(!kb.shift_active);
assert_eq!(kb.current_page, 0);
}
#[test]
fn test_virtual_keyboard_with_max_length() {
let kb = VirtualKeyboard::new("Test").with_max_length(20);
assert_eq!(kb.max_length, Some(20));
}
#[test]
fn test_virtual_keyboard_with_allow() {
let kb = VirtualKeyboard::new("Test").with_allow("abc123");
assert_eq!(kb.allow, Some("abc123".to_string()));
}
#[test]
fn test_virtual_keyboard_with_exclude() {
let kb = VirtualKeyboard::new("Test").with_exclude("!@#");
assert_eq!(kb.exclude, Some("!@#".to_string()));
}
#[test]
fn test_virtual_keyboard_with_value() {
let kb = VirtualKeyboard::new("Test").with_value("initial");
assert_eq!(kb.buffer, "initial");
assert_eq!(kb.cursor, 7);
}
#[test]
fn test_virtual_keyboard_is_char_allowed() {
let mut kb = VirtualKeyboard::new("Test");
assert!(kb.is_char_allowed('a'));
kb.allow = Some("abc".to_string());
assert!(kb.is_char_allowed('a'));
assert!(!kb.is_char_allowed('z'));
kb.allow = None;
kb.exclude = Some("xyz".to_string());
assert!(kb.is_char_allowed('a'));
assert!(!kb.is_char_allowed('x'));
}
#[test]
fn test_virtual_keyboard_add_char() {
let mut kb = VirtualKeyboard::new("Test");
kb.add_char('h');
kb.add_char('i');
assert_eq!(kb.buffer, "hi");
assert_eq!(kb.cursor, 2);
}
#[test]
fn test_virtual_keyboard_add_char_with_max_length() {
let mut kb = VirtualKeyboard::new("Test").with_max_length(3);
kb.add_char('a');
kb.add_char('b');
kb.add_char('c');
kb.add_char('d'); assert_eq!(kb.buffer, "abc");
}
#[test]
fn test_virtual_keyboard_add_char_with_shift() {
let mut kb = VirtualKeyboard::new("Test");
kb.shift_active = true;
kb.add_char('h');
assert_eq!(kb.buffer, "H");
assert!(!kb.shift_active); }
#[test]
fn test_virtual_keyboard_backspace() {
let mut kb = VirtualKeyboard::new("Test").with_value("Hello");
kb.backspace();
assert_eq!(kb.buffer, "Hell");
assert_eq!(kb.cursor, 4);
kb.cursor = 0;
kb.backspace();
assert_eq!(kb.buffer, "Hell");
}
#[test]
fn test_virtual_keyboard_cursor_movement() {
let mut kb = VirtualKeyboard::new("Test").with_value("Hello");
kb.cursor_left();
assert_eq!(kb.cursor, 4);
kb.cursor_left();
assert_eq!(kb.cursor, 3);
kb.cursor_right();
assert_eq!(kb.cursor, 4);
kb.cursor = 0;
kb.cursor_left();
assert_eq!(kb.cursor, 0);
kb.cursor = kb.buffer.len();
kb.cursor_right();
assert_eq!(kb.cursor, kb.buffer.len());
}
#[test]
fn test_virtual_keyboard_toggle_shift() {
let mut kb = VirtualKeyboard::new("Test");
assert!(!kb.shift_active);
kb.toggle_shift();
assert!(kb.shift_active);
kb.toggle_shift();
assert!(!kb.shift_active);
}
#[test]
fn test_virtual_keyboard_toggle_page() {
let mut kb = VirtualKeyboard::new("Test");
assert_eq!(kb.current_page, 0);
kb.toggle_page();
assert_eq!(kb.current_page, 1);
kb.toggle_page();
assert_eq!(kb.current_page, 0);
}
#[test]
fn test_virtual_keyboard_add_space() {
let mut kb = VirtualKeyboard::new("Test");
kb.add_char('a');
kb.add_space();
kb.add_char('b');
assert_eq!(kb.buffer, "a b");
}
#[test]
fn test_virtual_keyboard_clear() {
let mut kb = VirtualKeyboard::new("Test").with_value("Hello");
kb.clear();
assert_eq!(kb.buffer, "");
assert_eq!(kb.cursor, 0);
}
#[test]
fn test_virtual_keyboard_move_focus() {
let mut kb = VirtualKeyboard::new("Test");
kb.keys_per_row = 10;
kb.focused_key = 0;
kb.move_focus(FocusDirection::Down, 40);
assert_eq!(kb.focused_key, 10);
kb.move_focus(FocusDirection::Right, 40);
assert_eq!(kb.focused_key, 11);
kb.move_focus(FocusDirection::Up, 40);
assert_eq!(kb.focused_key, 1);
kb.move_focus(FocusDirection::Left, 40);
assert_eq!(kb.focused_key, 0);
kb.move_focus(FocusDirection::Left, 40);
assert_eq!(kb.focused_key, 0);
}
#[test]
fn test_focus_direction_variants() {
let all_directions = [
FocusDirection::Up,
FocusDirection::Down,
FocusDirection::Left,
FocusDirection::Right,
];
for (i, &dir1) in all_directions.iter().enumerate() {
for (j, &dir2) in all_directions.iter().enumerate() {
if i != j {
assert_ne!(dir1, dir2);
}
}
}
}
#[test]
fn test_virtual_keyboard_value() {
let kb = VirtualKeyboard::new("Test").with_value("Hello World");
assert_eq!(kb.value(), "Hello World");
}
#[test]
fn test_virtual_keyboard_insert_in_middle() {
let mut kb = VirtualKeyboard::new("Test").with_value("Hllo");
kb.cursor = 1;
kb.add_char('e');
assert_eq!(kb.buffer, "Hello");
}
#[test]
fn test_virtual_keyboard_add_char_not_allowed() {
let mut kb = VirtualKeyboard::new("Test").with_allow("abc");
kb.add_char('z');
assert_eq!(kb.buffer, "");
}
#[test]
fn test_virtual_keyboard_add_char_excluded() {
let mut kb = VirtualKeyboard::new("Test").with_exclude("xyz");
kb.add_char('x');
assert_eq!(kb.buffer, "");
kb.add_char('a');
assert_eq!(kb.buffer, "a");
}
#[test]
fn test_virtual_keyboard_focus_boundary_right() {
let mut kb = VirtualKeyboard::new("Test");
kb.keys_per_row = 10;
kb.focused_key = 39; kb.move_focus(FocusDirection::Right, 40);
assert_eq!(kb.focused_key, 39); }
#[test]
fn test_virtual_keyboard_focus_boundary_up() {
let mut kb = VirtualKeyboard::new("Test");
kb.keys_per_row = 10;
kb.focused_key = 5; kb.move_focus(FocusDirection::Up, 40);
assert_eq!(kb.focused_key, 5); }
#[test]
fn test_virtual_keyboard_focus_boundary_down() {
let mut kb = VirtualKeyboard::new("Test");
kb.keys_per_row = 10;
kb.focused_key = 35; kb.move_focus(FocusDirection::Down, 40);
assert_eq!(kb.focused_key, 35); }
#[test]
fn test_show_virtual_keyboard_event() {
let kb = VirtualKeyboard::new("Enter name:");
let event = ShowVirtualKeyboard { keyboard: kb };
assert_eq!(event.keyboard.prompt, "Enter name:");
}
#[test]
fn test_virtual_keyboard_event_confirmed() {
let event = VirtualKeyboardEvent {
value: "TestValue".to_string(),
confirmed: true,
};
assert!(event.confirmed);
assert_eq!(event.value, "TestValue");
}
#[test]
fn test_virtual_keyboard_event_cancelled() {
let event = VirtualKeyboardEvent {
value: String::new(),
confirmed: false,
};
assert!(!event.confirmed);
}
#[test]
fn test_virtual_keyboard_config_symbols() {
let config = VirtualKeyboardConfig::default();
assert_eq!(config.symbols1, "!@#$%^&*()");
assert_eq!(config.symbols2, "`~_-+=:;'\"");
assert_eq!(config.symbols3, "<>,.?/\\|");
}
#[test]
fn test_virtual_keyboard_config_colors() {
let config = VirtualKeyboardConfig::default();
assert_ne!(config.key_color, config.key_hover_color);
assert_ne!(config.key_color, config.key_pressed_color);
}
}