Skip to content

Commit 4611d22

Browse files
committed
Add support for handling bevy_egui focus interaction. Fixes #17
1 parent b9c9e8e commit 4611d22

File tree

5 files changed

+188
-2
lines changed

5 files changed

+188
-2
lines changed

Cargo.toml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "bevy_spectator"
33
description = "A spectator camera plugin for Bevy"
4-
version = "0.7.0"
4+
version = "0.7.1"
55
edition = "2021"
66
authors = ["JonahPlusPlus <[email protected]>"]
77
license = "MIT OR Apache-2.0"
@@ -14,6 +14,7 @@ include = ["/src", "/examples", "/LICENSE*"]
1414
bevy = { version = "0.15", default-features = false, features = [
1515
"bevy_window",
1616
] }
17+
bevy_egui = { version = "0.31", optional = true, default-features = false }
1718

1819
[dev-dependencies]
1920
bevy = { version = "0.15", default-features = false, features = [
@@ -28,7 +29,18 @@ bevy = { version = "0.15", default-features = false, features = [
2829
"zstd",
2930
"tonemapping_luts",
3031
] }
32+
bevy_egui = { version = "0.31", default-features = false, features = [
33+
"render",
34+
"default_fonts",
35+
] }
3136

3237
[features]
3338
default = ["init"]
34-
init = [] # Enables automatically choosing a camera
39+
init = [] # Enables automatically choosing a camera
40+
bevy_egui = ["dep:bevy_egui"] # Enables skipping enabling spectator if egui wants focus
41+
42+
43+
[[example]]
44+
name = "egui"
45+
path = "examples/egui.rs"
46+
required-features = ["bevy_egui"]

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ fn setup(mut commands: Commands) {
4343
}
4444
```
4545

46+
## Features
47+
48+
### `init`
49+
50+
Handles automatically setting `active_spectator` when there is exactly one camera with the `Spectator` component present.
51+
52+
Enabled by default.
53+
54+
### `bevy_egui`
55+
56+
Handles selectively disabling spectator mode entry when [bevy_egui](https://docs.rs/bevy_egui/latest/bevy_egui/) wants focus.
57+
58+
See [egui](crate::egui) for details.
59+
4660
## Bevy compatibility
4761

4862
| bevy | bevy_spectator |

examples/egui.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//! The same as 3d_scene but showing the `bevy_egui` integration.
2+
3+
use bevy::prelude::*;
4+
use bevy_egui::{egui, EguiContexts, EguiPlugin};
5+
use bevy_spectator::*;
6+
7+
fn main() {
8+
App::new()
9+
.insert_resource(SpectatorSettings {
10+
base_speed: 5.0,
11+
alt_speed: 15.0,
12+
sensitivity: 0.0015,
13+
..default()
14+
})
15+
.add_plugins((DefaultPlugins, EguiPlugin, SpectatorPlugin))
16+
.add_systems(Startup, setup)
17+
.add_systems(Update, ui_example)
18+
.run();
19+
}
20+
21+
fn setup(
22+
mut commands: Commands,
23+
mut meshes: ResMut<Assets<Mesh>>,
24+
mut materials: ResMut<Assets<StandardMaterial>>,
25+
) {
26+
commands.spawn((
27+
Camera3d::default(),
28+
Transform::from_xyz(-3.0, 1.5, 3.0).looking_at(Vec3::ZERO, Vec3::Y),
29+
Spectator,
30+
));
31+
commands.spawn((
32+
PointLight {
33+
shadows_enabled: true,
34+
..default()
35+
},
36+
Transform::from_xyz(4.0, 8.0, 4.0),
37+
));
38+
39+
commands.spawn((
40+
Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))),
41+
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
42+
));
43+
commands.spawn((
44+
Mesh3d(meshes.add(Cuboid::default())),
45+
MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
46+
Transform::from_xyz(0.0, 0.5, 0.0),
47+
));
48+
}
49+
50+
fn ui_example(
51+
mut contexts: EguiContexts,
52+
) {
53+
egui::SidePanel::left("left")
54+
.resizable(false)
55+
.show(contexts.ctx_mut(), |ui| {
56+
ui.label("Left fixed panel");
57+
});
58+
59+
egui::SidePanel::right("right")
60+
.resizable(true)
61+
.show(contexts.ctx_mut(), |ui| {
62+
ui.label("Right resizeable panel");
63+
64+
ui.allocate_rect(ui.available_rect_before_wrap(), egui::Sense::hover());
65+
});
66+
67+
egui::Window::new("Movable Window").show(contexts.ctx_mut(), |ui| {
68+
ui.label("Move me!");
69+
});
70+
71+
egui::Window::new("Immovable Window")
72+
.movable(false)
73+
.show(contexts.ctx_mut(), |ui| {
74+
ui.label("I can't be moved :(");
75+
});
76+
}

src/egui.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//! Handles focus / input conflicts between `bevy_egui` and this crate.
2+
//!
3+
//! ## Usage
4+
//!
5+
//! Enable the `bevy_egui` feature of this crate.
6+
//!
7+
//! Ensure [`bevy_egui::EguiPlugin`] is added to your app.
8+
//!
9+
//! *Note: this may be automatically done by `bevy_inspector_egui` plugins for example.*
10+
11+
use bevy::prelude::*;
12+
use bevy_egui::{EguiContexts, EguiPlugin, EguiSet};
13+
14+
/// A `Resource` for determining whether [`crate::Spectator`]s should handle input.
15+
///
16+
/// To check focus state it is recommended to call
17+
///
18+
/// [`EguiFocusState::wants_focus`]
19+
///
20+
/// which only returns true if egui wanted focus in both this frame and the previous frame.
21+
///
22+
#[derive(Resource, PartialEq, Eq, Default)]
23+
pub struct EguiFocusState {
24+
/// Whether egui wants focus this frame
25+
pub current_frame_wants_focus: bool,
26+
/// Whether egui wanted focus in the previous frame
27+
pub previous_frame_wanted_focus: bool,
28+
}
29+
30+
impl EguiFocusState {
31+
/// The default method for checking focus.
32+
pub fn wants_focus(&self) -> bool {
33+
self.previous_frame_wanted_focus && self.current_frame_wants_focus
34+
}
35+
}
36+
37+
pub(crate) struct EguiFocusPlugin;
38+
39+
impl Plugin for EguiFocusPlugin {
40+
fn build(&self, app: &mut App) {
41+
app.init_resource::<EguiFocusState>();
42+
}
43+
44+
fn finish(&self, app: &mut App) {
45+
if app.is_plugin_added::<EguiPlugin>() {
46+
app.add_systems(
47+
PostUpdate,
48+
check_egui_wants_focus.after(EguiSet::InitContexts),
49+
);
50+
} else {
51+
warn!("EguiPlugin not added, no focus checking will occur.");
52+
}
53+
}
54+
}
55+
56+
fn check_egui_wants_focus(
57+
mut contexts: EguiContexts,
58+
mut focus_state: ResMut<EguiFocusState>,
59+
windows: Query<Entity, With<Window>>,
60+
) {
61+
let egui_wants_focus_this_frame = windows.iter().any(|window| {
62+
let ctx = contexts.ctx_for_entity_mut(window);
63+
ctx.wants_pointer_input() || ctx.wants_keyboard_input() || ctx.is_pointer_over_area()
64+
});
65+
66+
focus_state.set_if_neq(EguiFocusState {
67+
previous_frame_wanted_focus: focus_state.current_frame_wants_focus,
68+
current_frame_wants_focus: egui_wants_focus_this_frame,
69+
});
70+
}

src/lib.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ use bevy::{
66
window::{CursorGrabMode, PrimaryWindow},
77
};
88

9+
#[cfg(feature = "bevy_egui")]
10+
use crate::egui::{EguiFocusPlugin, EguiFocusState};
11+
12+
#[cfg(feature = "bevy_egui")]
13+
pub mod egui;
14+
915
/// A marker `Component` for spectating cameras.
1016
///
1117
/// ## Usage
@@ -32,6 +38,9 @@ impl Plugin for SpectatorPlugin {
3238
app.add_systems(PostStartup, spectator_init);
3339

3440
app.add_systems(Update, spectator_update);
41+
42+
#[cfg(feature = "bevy_egui")]
43+
app.add_plugins(EguiFocusPlugin);
3544
}
3645
}
3746

@@ -67,6 +76,7 @@ fn spectator_update(
6776
mut windows: Query<(&mut Window, Option<&PrimaryWindow>)>,
6877
mut camera_transforms: Query<&mut Transform, With<Spectator>>,
6978
mut focus: Local<bool>,
79+
#[cfg(feature = "bevy_egui")] egui_focus: Res<EguiFocusState>,
7080
) {
7181
let Some(camera_id) = settings.active_spectator else {
7282
motion.clear();
@@ -116,6 +126,10 @@ fn spectator_update(
116126
if keys.just_pressed(KeyCode::Escape) {
117127
set_focus(false);
118128
} else if buttons.just_pressed(MouseButton::Left) {
129+
#[cfg(feature = "bevy_egui")]
130+
set_focus(!egui_focus.wants_focus());
131+
132+
#[cfg(not(feature = "bevy_egui"))]
119133
set_focus(true);
120134
}
121135

0 commit comments

Comments
 (0)