diff --git a/Cargo.lock b/Cargo.lock index 46761c4..44ba0f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1238,6 +1238,16 @@ dependencies = [ "wgpu-types 0.17.0", ] +[[package]] +name = "bevy_egui_keyboard" +version = "0.0.0" +dependencies = [ + "bevy", + "bevy_egui 0.25.0 (git+https://github.com/Schmarni-Dev/bevy_egui/?branch=nexus-use-bevy-0.13)", + "bevy_mod_picking", + "egui-picking", +] + [[package]] name = "bevy_encase_derive" version = "0.13.0" diff --git a/Cargo.toml b/Cargo.toml index e2aa7c8..8390e09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "apps/social/common", "apps/social/networking", "apps/social/server", + "crates/bevy_egui_keyboard", "crates/egui-picking", "crates/picking-xr", "crates/replicate/client", @@ -64,6 +65,7 @@ tracing-subscriber = "0.3.18" url = "2.5.0" uuid = "1.7.0" wtransport = "0.1.11" +bevy_egui_keyboard.path = "crates/bevy_egui_keyboard" [workspace.dependencies.derive_more] version = "0.99" diff --git a/crates/bevy_egui_keyboard/Cargo.toml b/crates/bevy_egui_keyboard/Cargo.toml new file mode 100644 index 0000000..1387426 --- /dev/null +++ b/crates/bevy_egui_keyboard/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "bevy_egui_keyboard" +version.workspace = true +license.workspace = true +repository.workspace = true +edition.workspace = true +rust-version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bevy.workspace = true +bevy_egui.workspace = true + +[dev-dependencies] +egui-picking.workspace = true +bevy_mod_picking = { workspace = true, default-features = false, features = [ + "backend_raycast", + "backend_bevy_ui", + "backend_sprite", + "selection", + "highlight", +] } + diff --git a/crates/bevy_egui_keyboard/examples/keyboard.rs b/crates/bevy_egui_keyboard/examples/keyboard.rs new file mode 100644 index 0000000..35b4931 --- /dev/null +++ b/crates/bevy_egui_keyboard/examples/keyboard.rs @@ -0,0 +1,159 @@ +use bevy::input::keyboard::KeyboardInput; +use bevy::window::PrimaryWindow; +use bevy::{ + prelude::*, + render::render_resource::{Extent3d, TextureUsages}, +}; +use bevy_egui::{egui, EguiContexts, EguiPlugin, EguiRenderToTexture}; +use bevy_egui_keyboard::{draw_keyboard, KeyValue, ModifierState}; +use bevy_mod_picking::DefaultPickingPlugins; +use egui_picking::{PickabelEguiPlugin, WorldSpaceUI}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(DefaultPickingPlugins) + .add_plugins(EguiPlugin) + .add_plugins(PickabelEguiPlugin) + .insert_resource(AmbientLight { + color: Color::WHITE, + brightness: 1., + }) + .add_systems(Startup, setup_worldspace) + // Systems that create Egui widgets should be run during the `CoreSet::Update` set, + // or after the `EguiSet::BeginFrame` system (which belongs to the `CoreSet::PreUpdate` set). + .add_systems(Update, (update_screenspace, update_worldspace)) + .run() +} +fn setup_worldspace( + mut images: ResMut>, + mut meshes: ResMut>, + mut materials: ResMut>, + mut commands: Commands, +) { + let output_texture = images.add({ + let size = Extent3d { + width: 512, + height: 512, + depth_or_array_layers: 1, + }; + let mut output_texture = Image { + // You should use `0` so that the pixels are transparent. + data: vec![0; (size.width * size.height * 4) as usize], + ..default() + }; + output_texture.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT; + output_texture.texture_descriptor.size = size; + output_texture + }); + commands.spawn(( + PbrBundle { + mesh: meshes.add(Plane3d::new(Vec3::Y).mesh().size(1.0, 1.0)), + material: materials.add(StandardMaterial { + base_color: Color::WHITE, + base_color_texture: Some(Handle::clone(&output_texture)), + // Remove this if you want it to use the world's lighting. + unlit: true, + + ..default() + }), + transform: Transform::looking_at( + Transform::IDENTITY, + Vec3::Y, + Vec3::splat(1.5), + ), + ..default() + }, + WorldSpaceUI::new(output_texture, 1.0, 1.0), + )); + let output_texture = images.add({ + let size = Extent3d { + width: 512, + height: 512, + depth_or_array_layers: 1, + }; + let mut output_texture = Image { + // You should use `0` so that the pixels are transparent. + data: vec![0; (size.width * size.height * 4) as usize], + ..default() + }; + output_texture.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT; + output_texture.texture_descriptor.size = size; + output_texture + }); + commands.spawn(( + PbrBundle { + mesh: meshes.add(Plane3d::new(Vec3::Y).mesh().size(1.0, 1.0)), + material: materials.add(StandardMaterial { + base_color: Color::WHITE, + base_color_texture: Some(Handle::clone(&output_texture)), + // Remove this if you want it to use the world's lighting. + unlit: true, + + ..default() + }), + transform: Transform::looking_at( + Transform::IDENTITY, + Vec3::Y, + Vec3::splat(1.5), + ) + .with_translation(Vec3::new(0.0, 0.0, 1.0)), + ..default() + }, + WorldSpaceUI::new(output_texture, 1.0, 1.0), + KeyboardBoi, + )); + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(1.5, 1.5, 1.5) + .looking_at(Vec3::new(0., 0., 0.), Vec3::Y), + ..default() + }); +} + +fn update_screenspace(mut contexts: EguiContexts, mut buf: Local) { + egui::Window::new("Screenspace UI").show(contexts.ctx_mut(), |ui| { + ui.label("I'm rendering to screenspace!"); + let buf: &mut String = &mut buf; + ui.text_edit_singleline(buf); + }); +} + +#[derive(Component)] +pub struct KeyboardBoi; + +#[allow(clippy::too_many_arguments)] +fn update_worldspace( + mut contexts: Query< + &mut bevy_egui::EguiContext, + (With, Without), + >, + mut contexts2: Query<&mut bevy_egui::EguiContext, With>, + window: Query>, + _just_pressed: Local>, + mut event_writer: EventWriter, + mut char_writer: EventWriter, + mut previously_pressed: Local>, + mut buf: Local, + mut modifier_state: Local, +) { + for mut ctx in contexts2.iter_mut() { + egui::Window::new("other_ui").show(ctx.get_mut(), |ui| { + draw_keyboard( + ui, + window.get_single().unwrap(), + &mut previously_pressed, + &mut event_writer, + &mut char_writer, + &mut modifier_state, + ); + }); + } + + for mut ctx in contexts.iter_mut() { + egui::Window::new("Worldspace UI").show(ctx.get_mut(), |ui| { + ui.label("I'm rendering to a texture in worldspace!"); + let buf: &mut String = &mut buf; + ui.text_edit_singleline(buf); + }); + } +} diff --git a/crates/bevy_egui_keyboard/src/lib.rs b/crates/bevy_egui_keyboard/src/lib.rs new file mode 100644 index 0000000..f34a1a3 --- /dev/null +++ b/crates/bevy_egui_keyboard/src/lib.rs @@ -0,0 +1,342 @@ +use bevy::input::keyboard::KeyboardInput; +use bevy::input::ButtonState; +use bevy::prelude::*; + +use crate::get_egui_keys::{first_row, fn_row, number_row, second_row, third_row}; +use bevy_egui::egui::{Key, Ui, WidgetText}; +use bevy_egui::systems::bevy_to_egui_physical_key; + +#[derive(Clone, Copy, Default)] +pub struct ModifierState { + caps_lock: bool, +} + +#[derive(Clone, Debug)] +pub enum KeyValue { + Char(String), + Key(Key), + CharKey(String, Key), +} + +impl From for KeyValue { + fn from(value: Key) -> Self { + Self::Key(value) + } +} +impl From for KeyValue { + fn from(value: String) -> Self { + Self::Char(value) + } +} +impl From<&str> for KeyValue { + fn from(value: &str) -> Self { + Self::Char(value.into()) + } +} +impl KeyValue { + pub fn symbol_or_name(&self, modifier_state: &ModifierState) -> String { + match self { + KeyValue::Char(char) => match modifier_state.caps_lock { + true => char.to_uppercase(), + false => char.to_lowercase(), + }, + KeyValue::Key(key) => key.symbol_or_name().to_string(), + KeyValue::CharKey(char, _) => match modifier_state.caps_lock { + true => char.to_uppercase(), + false => char.to_lowercase(), + }, + } + } +} + +pub fn draw_keyboard( + ui: &mut Ui, + primary_window: Entity, + previously_pressed: &mut Local>, + keyboard_writer: &mut EventWriter, + char_writer: &mut EventWriter, + modifier_state: &mut ModifierState, +) { + let curr_modifier_state = *modifier_state; + let mut key_pressed = None; + ui.horizontal(|ui| { + show_row(ui, fn_row(), &mut key_pressed, &curr_modifier_state); + }); + ui.horizontal(|ui| { + show_row(ui, number_row(), &mut key_pressed, &curr_modifier_state) + }); + ui.horizontal(|ui| { + show_row(ui, first_row(), &mut key_pressed, &curr_modifier_state); + }); + ui.horizontal(|ui| { + if ui + .checkbox(&mut modifier_state.caps_lock, "Caps Lock") + .clicked() + { + let button_state = match modifier_state.caps_lock { + true => ButtonState::Pressed, + false => ButtonState::Released, + }; + keyboard_writer.send(KeyboardInput { + key_code: KeyCode::CapsLock, + logical_key: bevy::input::keyboard::Key::CapsLock, + state: button_state, + window: primary_window, + }); + } + show_row(ui, second_row(), &mut key_pressed, &curr_modifier_state); + }); + ui.horizontal(|ui| { + show_row(ui, third_row(), &mut key_pressed, &curr_modifier_state); + }); + + if let Some(key) = key_pressed { + match key.clone() { + KeyValue::Char(char) => { + char_writer.send(ReceivedCharacter { + window: primary_window, + char: match curr_modifier_state.caps_lock { + true => char.to_uppercase(), + false => char.to_lowercase(), + } + .into(), + }); + } + KeyValue::Key(key) => { + let key = convert_egui_key(key); + keyboard_writer.send(KeyboardInput { + key_code: key, + logical_key: bevy::input::keyboard::Key::Character( + bevy_to_egui_physical_key(&key) + .unwrap() + .symbol_or_name() + .parse() + .unwrap(), + ), + state: ButtonState::Pressed, + window: primary_window, + }); + } + KeyValue::CharKey(char, key) => { + char_writer.send(ReceivedCharacter { + window: primary_window, + char: match curr_modifier_state.caps_lock { + true => char.to_uppercase(), + false => char.to_lowercase(), + } + .into(), + }); + + let key = convert_egui_key(key); + keyboard_writer.send(KeyboardInput { + key_code: key, + logical_key: bevy::input::keyboard::Key::Character( + bevy_to_egui_physical_key(&key) + .unwrap() + .symbol_or_name() + .parse() + .unwrap(), + ), + state: ButtonState::Pressed, + window: primary_window, + }); + } + } + previously_pressed.replace(key); + } +} + +fn show_row( + ui: &mut Ui, + row: Vec, + key_code: &mut Option, + modifier_state: &ModifierState, +) { + for key in row { + if let Some(key) = print_key(ui, key, modifier_state) { + key_code.replace(key); + } + } +} + +fn print_key( + ui: &mut Ui, + key: KeyValue, + modifier_state: &ModifierState, +) -> Option { + let text: WidgetText = key.symbol_or_name(modifier_state).into(); + match ui.button(text.monospace()).clicked() { + true => Some(key), + false => None, + } +} + +fn convert_egui_key(key: bevy_egui::egui::Key) -> KeyCode { + match key { + Key::Escape => KeyCode::Escape, + Key::Tab => KeyCode::Tab, + Key::Backspace => KeyCode::Backspace, + Key::Enter => KeyCode::Enter, + Key::Space => KeyCode::Space, + Key::Delete => KeyCode::Delete, + Key::Colon => todo!(), + Key::Comma => KeyCode::Comma, + Key::Backslash => KeyCode::Backslash, + Key::Slash => KeyCode::Slash, + Key::Pipe => todo!(), + Key::Questionmark => todo!(), + Key::OpenBracket => KeyCode::BracketLeft, + Key::CloseBracket => KeyCode::BracketRight, + Key::Backtick => KeyCode::Backquote, + Key::Minus => KeyCode::Minus, + Key::Period => KeyCode::Period, + Key::Plus => todo!(), + Key::Equals => KeyCode::Equal, + Key::Semicolon => KeyCode::Semicolon, + Key::Num0 => KeyCode::Digit0, + Key::Num1 => KeyCode::Digit1, + Key::Num2 => KeyCode::Digit2, + Key::Num3 => KeyCode::Digit3, + Key::Num4 => KeyCode::Digit4, + Key::Num5 => KeyCode::Digit5, + Key::Num6 => KeyCode::Digit6, + Key::Num7 => KeyCode::Digit7, + Key::Num8 => KeyCode::Digit8, + Key::Num9 => KeyCode::Digit9, + Key::A => KeyCode::KeyA, + Key::B => KeyCode::KeyB, + Key::C => KeyCode::KeyC, + Key::D => KeyCode::KeyD, + Key::E => KeyCode::KeyE, + Key::F => KeyCode::KeyF, + Key::G => KeyCode::KeyG, + Key::H => KeyCode::KeyH, + Key::I => KeyCode::KeyI, + Key::J => KeyCode::KeyJ, + Key::K => KeyCode::KeyK, + Key::L => KeyCode::KeyL, + Key::M => KeyCode::KeyM, + Key::N => KeyCode::KeyN, + Key::O => KeyCode::KeyO, + Key::P => KeyCode::KeyP, + Key::Q => KeyCode::KeyQ, + Key::R => KeyCode::KeyR, + Key::S => KeyCode::KeyS, + Key::T => KeyCode::KeyT, + Key::U => KeyCode::KeyU, + Key::V => KeyCode::KeyV, + Key::W => KeyCode::KeyW, + Key::X => KeyCode::KeyX, + Key::Y => KeyCode::KeyY, + Key::Z => KeyCode::KeyZ, + Key::F1 => KeyCode::F1, + Key::F2 => KeyCode::F2, + Key::F3 => KeyCode::F3, + Key::F4 => KeyCode::F4, + Key::F5 => KeyCode::F5, + Key::F6 => KeyCode::F6, + Key::F7 => KeyCode::F7, + Key::F8 => KeyCode::F8, + Key::F9 => KeyCode::F9, + Key::F10 => KeyCode::F10, + Key::F11 => KeyCode::F11, + Key::F12 => KeyCode::F12, + _ => panic!("Unhandled key"), + } +} + +mod get_egui_keys { + // use bevy_egui::egui::Key; + use bevy_egui::egui::Key::*; + + use crate::KeyValue; + use crate::KeyValue::Key; + + pub fn fn_row() -> Vec { + vec![ + Key(Escape), + Key(F1), + Key(F2), + Key(F3), + Key(F4), + Key(F5), + Key(F6), + Key(F7), + Key(F8), + Key(F9), + Key(F10), + Key(F11), + Key(F12), + ] + } + + pub fn number_row() -> Vec { + vec![ + Key(Backtick), + "0".into(), + "1".into(), + "2".into(), + "3".into(), + "4".into(), + "5".into(), + "6".into(), + "7".into(), + "8".into(), + "9".into(), + "0".into(), + "-".into(), + "=".into(), + Key(Backspace), + ] + } + + pub fn first_row() -> Vec { + vec![ + Key(Tab), + "q".into(), + "w".into(), + "e".into(), + "r".into(), + "t".into(), + "y".into(), + "u".into(), + "i".into(), + "o".into(), + "p".into(), + ")".into(), + "(".into(), + "\\".into(), + ] + } + + pub fn second_row() -> Vec { + vec![ + "a".into(), + "s".into(), + "d".into(), + "f".into(), + "g".into(), + "h".into(), + "j".into(), + "k".into(), + "l".into(), + ";".into(), + Key(Enter), + ] + } + + pub fn third_row() -> Vec { + vec![ + "z".into(), + "x".into(), + "c".into(), + "v".into(), + "b".into(), + "n".into(), + "m".into(), + ",".into(), + ".".into(), + "/".into(), + ] + } +} diff --git a/crates/egui-picking/Cargo.toml b/crates/egui-picking/Cargo.toml index 3c81f10..65e9fd8 100644 --- a/crates/egui-picking/Cargo.toml +++ b/crates/egui-picking/Cargo.toml @@ -12,6 +12,7 @@ bevy.workspace = true bevy_egui.workspace = true bevy_mod_picking.workspace = true + [dev-dependencies] bevy_mod_picking = { workspace = true, default-features = false, features = [ "backend_raycast", diff --git a/crates/egui-picking/src/lib.rs b/crates/egui-picking/src/lib.rs index 1e24e06..2a26b94 100644 --- a/crates/egui-picking/src/lib.rs +++ b/crates/egui-picking/src/lib.rs @@ -1,5 +1,7 @@ +use bevy::window::PrimaryWindow; use bevy::{ecs::schedule::Condition, prelude::*, utils::HashMap}; -use bevy_egui::{egui::PointerButton, EguiInput, EguiRenderToTexture}; + +use bevy_egui::{egui, egui::PointerButton, EguiInput, EguiRenderToTexture, EguiSet}; use bevy_mod_picking::{ events::{Down, Move, Out, Pointer, Up}, focus::PickingInteraction, @@ -7,7 +9,6 @@ use bevy_mod_picking::{ pointer::PointerId, prelude::{ListenerInput, On}, }; - #[derive(Clone, Copy, Component, Debug)] pub struct WorldUI { pub size_x: f32, @@ -148,6 +149,12 @@ impl Plugin for PickabelEguiPlugin { .or_else(on_event::()), ), ); + app.add_systems( + PreUpdate, + (forward_egui_events + .after(EguiSet::ProcessInput) + .before(EguiSet::BeginFrame),), + ); } } @@ -160,6 +167,44 @@ pub struct CurrentPointerInteraction { pub pointer: Option, } +pub fn forward_egui_events( + mut query: Query<&mut EguiInput, With>, + window_query: Query<&EguiInput, (With, Without)>, +) { + let Ok(primary_input) = window_query.get_single() else { + warn!("Unable to find one Primary Window!"); + return; + }; + + let events = primary_input.events.iter().filter_map(|e| { + match e { + egui::Event::Copy => Some(e.clone()), + egui::Event::Cut => Some(e.clone()), + egui::Event::Paste(_) => Some(e.clone()), + egui::Event::Text(_) => Some(e.clone()), + egui::Event::Key { + key: _, + physical_key: _, + pressed: _, + repeat: _, + modifiers: _, + } => Some(e.clone()), + // egui::Event::Scroll(_) => Some(e.clone()), + // egui::Event::Zoom(_) => Some(e.clone()), + // egui::Event::MouseWheel { + // unit:_, + // delta:_, + // modifiers:_, + // } => Some(e.clone()), + _ => None, + } + }); + + for mut egui_input in query.iter_mut() { + egui_input.events.extend(events.clone()); + } +} + pub fn ui_interactions( mut inputs: Query<( &mut EguiInput,