Skip to content

Add support for handling bevy_egui focus interaction. #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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 = [
Expand All @@ -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"]
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ 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.

## Bevy compatibility

| bevy | bevy_spectator |
Expand Down
74 changes: 74 additions & 0 deletions examples/egui.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! 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<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
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 :(");
});
}
70 changes: 70 additions & 0 deletions src/egui.rs
Original file line number Diff line number Diff line change
@@ -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::<EguiFocusState>();
}

fn finish(&self, app: &mut App) {
if app.is_plugin_added::<EguiPlugin>() {
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<EguiFocusState>,
windows: Query<Entity, With<Window>>,
) {
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,
});
}
14 changes: 14 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -67,6 +76,7 @@ fn spectator_update(
mut windows: Query<(&mut Window, Option<&PrimaryWindow>)>,
mut camera_transforms: Query<&mut Transform, With<Spectator>>,
mut focus: Local<bool>,
#[cfg(feature = "bevy_egui")] egui_focus: Res<EguiFocusState>,
) {
let Some(camera_id) = settings.active_spectator else {
motion.clear();
Expand Down Expand Up @@ -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);
}

Expand Down
Loading