From 4611d22653f36a4fc811569aa231eb476a426e49 Mon Sep 17 00:00:00 2001 From: Patrick Dobbs Date: Sat, 21 Dec 2024 16:00:06 +0000 Subject: [PATCH 1/2] Add support for handling `bevy_egui` focus interaction. Fixes #17 --- Cargo.toml | 16 ++++++++-- README.md | 14 +++++++++ examples/egui.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ src/egui.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 14 +++++++++ 5 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 examples/egui.rs create mode 100644 src/egui.rs diff --git a/Cargo.toml b/Cargo.toml index 2dc977a..bd4c60a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bevy_spectator" description = "A spectator camera plugin for Bevy" -version = "0.7.0" +version = "0.7.1" edition = "2021" authors = ["JonahPlusPlus <33059163+JonahPlusPlus@users.noreply.github.com>"] license = "MIT OR Apache-2.0" @@ -14,6 +14,7 @@ include = ["/src", "/examples", "/LICENSE*"] bevy = { version = "0.15", default-features = false, features = [ "bevy_window", ] } +bevy_egui = { version = "0.31", optional = true, default-features = false } [dev-dependencies] bevy = { version = "0.15", default-features = false, features = [ @@ -28,7 +29,18 @@ bevy = { version = "0.15", default-features = false, features = [ "zstd", "tonemapping_luts", ] } +bevy_egui = { version = "0.31", default-features = false, features = [ + "render", + "default_fonts", +] } [features] default = ["init"] -init = [] # Enables automatically choosing a camera +init = [] # Enables automatically choosing a camera +bevy_egui = ["dep:bevy_egui"] # Enables skipping enabling spectator if egui wants focus + + +[[example]] +name = "egui" +path = "examples/egui.rs" +required-features = ["bevy_egui"] \ No newline at end of file diff --git a/README.md b/README.md index 6bae850..c836eb0 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,20 @@ fn setup(mut commands: Commands) { } ``` +## Features + +### `init` + +Handles automatically setting `active_spectator` when there is exactly one camera with the `Spectator` component present. + +Enabled by default. + +### `bevy_egui` + +Handles selectively disabling spectator mode entry when [bevy_egui](https://docs.rs/bevy_egui/latest/bevy_egui/) wants focus. + +See [egui](crate::egui) for details. + ## Bevy compatibility | bevy | bevy_spectator | diff --git a/examples/egui.rs b/examples/egui.rs new file mode 100644 index 0000000..a7d0394 --- /dev/null +++ b/examples/egui.rs @@ -0,0 +1,76 @@ +//! The same as 3d_scene but showing the `bevy_egui` integration. + +use bevy::prelude::*; +use bevy_egui::{egui, EguiContexts, EguiPlugin}; +use bevy_spectator::*; + +fn main() { + App::new() + .insert_resource(SpectatorSettings { + base_speed: 5.0, + alt_speed: 15.0, + sensitivity: 0.0015, + ..default() + }) + .add_plugins((DefaultPlugins, EguiPlugin, SpectatorPlugin)) + .add_systems(Startup, setup) + .add_systems(Update, ui_example) + .run(); +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-3.0, 1.5, 3.0).looking_at(Vec3::ZERO, Vec3::Y), + Spectator, + )); + commands.spawn(( + PointLight { + shadows_enabled: true, + ..default() + }, + Transform::from_xyz(4.0, 8.0, 4.0), + )); + + commands.spawn(( + Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), + MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), + )); + commands.spawn(( + Mesh3d(meshes.add(Cuboid::default())), + MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))), + Transform::from_xyz(0.0, 0.5, 0.0), + )); +} + +fn ui_example( + mut contexts: EguiContexts, +) { + egui::SidePanel::left("left") + .resizable(false) + .show(contexts.ctx_mut(), |ui| { + ui.label("Left fixed panel"); + }); + + egui::SidePanel::right("right") + .resizable(true) + .show(contexts.ctx_mut(), |ui| { + ui.label("Right resizeable panel"); + + ui.allocate_rect(ui.available_rect_before_wrap(), egui::Sense::hover()); + }); + + egui::Window::new("Movable Window").show(contexts.ctx_mut(), |ui| { + ui.label("Move me!"); + }); + + egui::Window::new("Immovable Window") + .movable(false) + .show(contexts.ctx_mut(), |ui| { + ui.label("I can't be moved :("); + }); +} \ No newline at end of file diff --git a/src/egui.rs b/src/egui.rs new file mode 100644 index 0000000..da506f2 --- /dev/null +++ b/src/egui.rs @@ -0,0 +1,70 @@ +//! Handles focus / input conflicts between `bevy_egui` and this crate. +//! +//! ## Usage +//! +//! Enable the `bevy_egui` feature of this crate. +//! +//! Ensure [`bevy_egui::EguiPlugin`] is added to your app. +//! +//! *Note: this may be automatically done by `bevy_inspector_egui` plugins for example.* + +use bevy::prelude::*; +use bevy_egui::{EguiContexts, EguiPlugin, EguiSet}; + +/// A `Resource` for determining whether [`crate::Spectator`]s should handle input. +/// +/// To check focus state it is recommended to call +/// +/// [`EguiFocusState::wants_focus`] +/// +/// which only returns true if egui wanted focus in both this frame and the previous frame. +/// +#[derive(Resource, PartialEq, Eq, Default)] +pub struct EguiFocusState { + /// Whether egui wants focus this frame + pub current_frame_wants_focus: bool, + /// Whether egui wanted focus in the previous frame + pub previous_frame_wanted_focus: bool, +} + +impl EguiFocusState { + /// The default method for checking focus. + pub fn wants_focus(&self) -> bool { + self.previous_frame_wanted_focus && self.current_frame_wants_focus + } +} + +pub(crate) struct EguiFocusPlugin; + +impl Plugin for EguiFocusPlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + } + + fn finish(&self, app: &mut App) { + if app.is_plugin_added::() { + app.add_systems( + PostUpdate, + check_egui_wants_focus.after(EguiSet::InitContexts), + ); + } else { + warn!("EguiPlugin not added, no focus checking will occur."); + } + } +} + +fn check_egui_wants_focus( + mut contexts: EguiContexts, + mut focus_state: ResMut, + windows: Query>, +) { + let egui_wants_focus_this_frame = windows.iter().any(|window| { + let ctx = contexts.ctx_for_entity_mut(window); + ctx.wants_pointer_input() || ctx.wants_keyboard_input() || ctx.is_pointer_over_area() + }); + + focus_state.set_if_neq(EguiFocusState { + previous_frame_wanted_focus: focus_state.current_frame_wants_focus, + current_frame_wants_focus: egui_wants_focus_this_frame, + }); +} diff --git a/src/lib.rs b/src/lib.rs index 3ca4c38..1a2c1df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,12 @@ use bevy::{ window::{CursorGrabMode, PrimaryWindow}, }; +#[cfg(feature = "bevy_egui")] +use crate::egui::{EguiFocusPlugin, EguiFocusState}; + +#[cfg(feature = "bevy_egui")] +pub mod egui; + /// A marker `Component` for spectating cameras. /// /// ## Usage @@ -32,6 +38,9 @@ impl Plugin for SpectatorPlugin { app.add_systems(PostStartup, spectator_init); app.add_systems(Update, spectator_update); + + #[cfg(feature = "bevy_egui")] + app.add_plugins(EguiFocusPlugin); } } @@ -67,6 +76,7 @@ fn spectator_update( mut windows: Query<(&mut Window, Option<&PrimaryWindow>)>, mut camera_transforms: Query<&mut Transform, With>, mut focus: Local, + #[cfg(feature = "bevy_egui")] egui_focus: Res, ) { let Some(camera_id) = settings.active_spectator else { motion.clear(); @@ -116,6 +126,10 @@ fn spectator_update( if keys.just_pressed(KeyCode::Escape) { set_focus(false); } else if buttons.just_pressed(MouseButton::Left) { + #[cfg(feature = "bevy_egui")] + set_focus(!egui_focus.wants_focus()); + + #[cfg(not(feature = "bevy_egui"))] set_focus(true); } From 38f9bbce666337597e2946f670596554f9ef3ea9 Mon Sep 17 00:00:00 2001 From: Patrick Dobbs Date: Sun, 22 Dec 2024 03:48:14 +0000 Subject: [PATCH 2/2] fmt and doc fixes. --- README.md | 2 -- examples/egui.rs | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c836eb0..a8d9c6e 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,6 @@ Enabled by default. Handles selectively disabling spectator mode entry when [bevy_egui](https://docs.rs/bevy_egui/latest/bevy_egui/) wants focus. -See [egui](crate::egui) for details. - ## Bevy compatibility | bevy | bevy_spectator | diff --git a/examples/egui.rs b/examples/egui.rs index a7d0394..9206757 100644 --- a/examples/egui.rs +++ b/examples/egui.rs @@ -47,9 +47,7 @@ fn setup( )); } -fn ui_example( - mut contexts: EguiContexts, -) { +fn ui_example(mut contexts: EguiContexts) { egui::SidePanel::left("left") .resizable(false) .show(contexts.ctx_mut(), |ui| { @@ -73,4 +71,4 @@ fn ui_example( .show(contexts.ctx_mut(), |ui| { ui.label("I can't be moved :("); }); -} \ No newline at end of file +}