Split CursorOptions off of Window (#19668)

# Objective

- Fixes #19627 
- Tackles part of #19644 
- Supersedes #19629
- `Window` has become a very very very big component
- As such, our change detection does not *really* work on it, as e.g.
moving the mouse will cause a change for the entire window
- We circumvented this with a cache
- But, some things *shouldn't* be cached as they can be changed from
outside the user's control, notably the cursor grab mode on web
- So, we need to disable the cache for that
- But because change detection is broken, that would result in the
cursor grab mode being set every frame the mouse is moved
- That is usually *not* what a dev wants, as it forces the cursor to be
locked even when the end-user is trying to free the cursor on the
browser
  - the cache in this situation is invalid due to #8949

## Solution

- Split `Window` into multiple components, each with working change
detection
- Disable caching of the cursor grab mode
- This will only attempt to force the grab mode when the `CursorOptions`
were touched by the user, which is *much* rarer than simply moving the
mouse.
- If this PR is merged, I'll do the exact same for the other
constituents of `Window` as a follow-up

## Testing

- Ran all the changed examples
This commit is contained in:
Jan Hohenheim 2025-06-17 22:20:13 +02:00 committed by GitHub
parent d1c6fbea57
commit a750cfe4a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 212 additions and 112 deletions

View File

@ -57,6 +57,7 @@ impl Default for WindowPlugin {
fn default() -> Self { fn default() -> Self {
WindowPlugin { WindowPlugin {
primary_window: Some(Window::default()), primary_window: Some(Window::default()),
primary_cursor_options: Some(CursorOptions::default()),
exit_condition: ExitCondition::OnAllClosed, exit_condition: ExitCondition::OnAllClosed,
close_when_requested: true, close_when_requested: true,
} }
@ -76,6 +77,13 @@ pub struct WindowPlugin {
/// [`exit_on_all_closed`]. /// [`exit_on_all_closed`].
pub primary_window: Option<Window>, pub primary_window: Option<Window>,
/// Settings for the cursor on the primary window.
///
/// Defaults to `Some(CursorOptions::default())`.
///
/// Has no effect if [`WindowPlugin::primary_window`] is `None`.
pub primary_cursor_options: Option<CursorOptions>,
/// Whether to exit the app when there are no open windows. /// Whether to exit the app when there are no open windows.
/// ///
/// If disabling this, ensure that you send the [`bevy_app::AppExit`] /// If disabling this, ensure that you send the [`bevy_app::AppExit`]
@ -122,10 +130,14 @@ impl Plugin for WindowPlugin {
.add_event::<AppLifecycle>(); .add_event::<AppLifecycle>();
if let Some(primary_window) = &self.primary_window { if let Some(primary_window) = &self.primary_window {
app.world_mut().spawn(primary_window.clone()).insert(( let mut entity_commands = app.world_mut().spawn(primary_window.clone());
entity_commands.insert((
PrimaryWindow, PrimaryWindow,
RawHandleWrapperHolder(Arc::new(Mutex::new(None))), RawHandleWrapperHolder(Arc::new(Mutex::new(None))),
)); ));
if let Some(primary_cursor_options) = &self.primary_cursor_options {
entity_commands.insert(primary_cursor_options.clone());
}
} }
match self.exit_condition { match self.exit_condition {
@ -168,7 +180,8 @@ impl Plugin for WindowPlugin {
// Register window descriptor and related types // Register window descriptor and related types
#[cfg(feature = "bevy_reflect")] #[cfg(feature = "bevy_reflect")]
app.register_type::<Window>() app.register_type::<Window>()
.register_type::<PrimaryWindow>(); .register_type::<PrimaryWindow>()
.register_type::<CursorOptions>();
} }
} }

View File

@ -158,10 +158,8 @@ impl ContainsEntity for NormalizedWindowRef {
all(feature = "serialize", feature = "bevy_reflect"), all(feature = "serialize", feature = "bevy_reflect"),
reflect(Serialize, Deserialize) reflect(Serialize, Deserialize)
)] )]
#[require(CursorOptions)]
pub struct Window { pub struct Window {
/// The cursor options of this window. Cursor icons are set with the `Cursor` component on the
/// window entity.
pub cursor_options: CursorOptions,
/// What presentation mode to give the window. /// What presentation mode to give the window.
pub present_mode: PresentMode, pub present_mode: PresentMode,
/// Which fullscreen or windowing mode should be used. /// Which fullscreen or windowing mode should be used.
@ -470,7 +468,6 @@ impl Default for Window {
Self { Self {
title: DEFAULT_WINDOW_TITLE.to_owned(), title: DEFAULT_WINDOW_TITLE.to_owned(),
name: None, name: None,
cursor_options: Default::default(),
present_mode: Default::default(), present_mode: Default::default(),
mode: Default::default(), mode: Default::default(),
position: Default::default(), position: Default::default(),
@ -728,11 +725,11 @@ impl WindowResizeConstraints {
} }
/// Cursor data for a [`Window`]. /// Cursor data for a [`Window`].
#[derive(Debug, Clone)] #[derive(Component, Debug, Clone)]
#[cfg_attr( #[cfg_attr(
feature = "bevy_reflect", feature = "bevy_reflect",
derive(Reflect), derive(Reflect),
reflect(Debug, Default, Clone) reflect(Component, Debug, Default, Clone)
)] )]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr( #[cfg_attr(

View File

@ -25,8 +25,8 @@ use winit::{event_loop::EventLoop, window::WindowId};
use bevy_a11y::AccessibilityRequested; use bevy_a11y::AccessibilityRequested;
use bevy_app::{App, Last, Plugin}; use bevy_app::{App, Last, Plugin};
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_window::{exit_on_all_closed, Window, WindowCreated}; use bevy_window::{exit_on_all_closed, CursorOptions, Window, WindowCreated};
use system::{changed_windows, check_keyboard_focus_lost, despawn_windows}; use system::{changed_cursor_options, changed_windows, check_keyboard_focus_lost, despawn_windows};
pub use system::{create_monitors, create_windows}; pub use system::{create_monitors, create_windows};
#[cfg(all(target_family = "wasm", target_os = "unknown"))] #[cfg(all(target_family = "wasm", target_os = "unknown"))]
pub use winit::platform::web::CustomCursorExtWebSys; pub use winit::platform::web::CustomCursorExtWebSys;
@ -142,6 +142,7 @@ impl<T: BufferedEvent> Plugin for WinitPlugin<T> {
// `exit_on_all_closed` only checks if windows exist but doesn't access data, // `exit_on_all_closed` only checks if windows exist but doesn't access data,
// so we don't need to care about its ordering relative to `changed_windows` // so we don't need to care about its ordering relative to `changed_windows`
changed_windows.ambiguous_with(exit_on_all_closed), changed_windows.ambiguous_with(exit_on_all_closed),
changed_cursor_options,
despawn_windows, despawn_windows,
check_keyboard_focus_lost, check_keyboard_focus_lost,
) )
@ -211,6 +212,7 @@ pub type CreateWindowParams<'w, 's, F = ()> = (
( (
Entity, Entity,
&'static mut Window, &'static mut Window,
&'static CursorOptions,
Option<&'static RawHandleWrapperHolder>, Option<&'static RawHandleWrapperHolder>,
), ),
F, F,

View File

@ -46,7 +46,7 @@ use bevy_window::{
WindowScaleFactorChanged, WindowThemeChanged, WindowScaleFactorChanged, WindowThemeChanged,
}; };
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
use bevy_window::{PrimaryWindow, RawHandleWrapper}; use bevy_window::{CursorOptions, PrimaryWindow, RawHandleWrapper};
use crate::{ use crate::{
accessibility::ACCESS_KIT_ADAPTERS, accessibility::ACCESS_KIT_ADAPTERS,
@ -474,7 +474,7 @@ impl<T: BufferedEvent> ApplicationHandler<T> for WinitAppRunnerState<T> {
if let Ok((window_component, mut cache)) = windows.get_mut(self.world_mut(), window) if let Ok((window_component, mut cache)) = windows.get_mut(self.world_mut(), window)
{ {
if window_component.is_changed() { if window_component.is_changed() {
cache.window = window_component.clone(); **cache = window_component.clone();
} }
} }
}); });
@ -605,10 +605,12 @@ impl<T: BufferedEvent> WinitAppRunnerState<T> {
{ {
// Get windows that are cached but without raw handles. Those window were already created, but got their // Get windows that are cached but without raw handles. Those window were already created, but got their
// handle wrapper removed when the app was suspended. // handle wrapper removed when the app was suspended.
let mut query = self.world_mut() let mut query = self.world_mut()
.query_filtered::<(Entity, &Window), (With<CachedWindow>, Without<RawHandleWrapper>)>(); .query_filtered::<(Entity, &Window, &CursorOptions), (With<CachedWindow>, Without<RawHandleWrapper>)>();
if let Ok((entity, window)) = query.single(&self.world()) { if let Ok((entity, window, cursor_options)) = query.single(&self.world()) {
let window = window.clone(); let window = window.clone();
let cursor_options = cursor_options.clone();
WINIT_WINDOWS.with_borrow_mut(|winit_windows| { WINIT_WINDOWS.with_borrow_mut(|winit_windows| {
ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| { ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| {
@ -622,6 +624,7 @@ impl<T: BufferedEvent> WinitAppRunnerState<T> {
event_loop, event_loop,
entity, entity,
&window, &window,
&cursor_options,
adapters, adapters,
&mut handlers, &mut handlers,
&accessibility_requested, &accessibility_requested,

View File

@ -1,6 +1,8 @@
use std::collections::HashMap; use std::collections::HashMap;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{ use bevy_ecs::{
change_detection::DetectChangesMut,
entity::Entity, entity::Entity,
event::EventWriter, event::EventWriter,
lifecycle::RemovedComponents, lifecycle::RemovedComponents,
@ -10,9 +12,9 @@ use bevy_ecs::{
}; };
use bevy_input::keyboard::{Key, KeyCode, KeyboardFocusLost, KeyboardInput}; use bevy_input::keyboard::{Key, KeyCode, KeyboardFocusLost, KeyboardInput};
use bevy_window::{ use bevy_window::{
ClosingWindow, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window, WindowClosed, ClosingWindow, CursorOptions, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window,
WindowClosing, WindowCreated, WindowEvent, WindowFocused, WindowMode, WindowResized, WindowClosed, WindowClosing, WindowCreated, WindowEvent, WindowFocused, WindowMode,
WindowWrapper, WindowResized, WindowWrapper,
}; };
use tracing::{error, info, warn}; use tracing::{error, info, warn};
@ -59,7 +61,7 @@ pub fn create_windows<F: QueryFilter + 'static>(
) { ) {
WINIT_WINDOWS.with_borrow_mut(|winit_windows| { WINIT_WINDOWS.with_borrow_mut(|winit_windows| {
ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| { ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| {
for (entity, mut window, handle_holder) in &mut created_windows { for (entity, mut window, cursor_options, handle_holder) in &mut created_windows {
if winit_windows.get_window(entity).is_some() { if winit_windows.get_window(entity).is_some() {
continue; continue;
} }
@ -70,6 +72,7 @@ pub fn create_windows<F: QueryFilter + 'static>(
event_loop, event_loop,
entity, entity,
&window, &window,
cursor_options,
adapters, adapters,
&mut handlers, &mut handlers,
&accessibility_requested, &accessibility_requested,
@ -85,9 +88,8 @@ pub fn create_windows<F: QueryFilter + 'static>(
.set_scale_factor_and_apply_to_physical_size(winit_window.scale_factor() as f32); .set_scale_factor_and_apply_to_physical_size(winit_window.scale_factor() as f32);
commands.entity(entity).insert(( commands.entity(entity).insert((
CachedWindow { CachedWindow(window.clone()),
window: window.clone(), CachedCursorOptions(cursor_options.clone()),
},
WinitWindowPressedKeys::default(), WinitWindowPressedKeys::default(),
)); ));
@ -281,10 +283,12 @@ pub(crate) fn despawn_windows(
} }
/// The cached state of the window so we can check which properties were changed from within the app. /// The cached state of the window so we can check which properties were changed from within the app.
#[derive(Debug, Clone, Component)] #[derive(Debug, Clone, Component, Deref, DerefMut)]
pub struct CachedWindow { pub(crate) struct CachedWindow(Window);
pub window: Window,
} /// The cached state of the window so we can check which properties were changed from within the app.
#[derive(Debug, Clone, Component, Deref, DerefMut)]
pub(crate) struct CachedCursorOptions(CursorOptions);
/// Propagates changes from [`Window`] entities to the [`winit`] backend. /// Propagates changes from [`Window`] entities to the [`winit`] backend.
/// ///
@ -306,11 +310,11 @@ pub(crate) fn changed_windows(
continue; continue;
}; };
if window.title != cache.window.title { if window.title != cache.title {
winit_window.set_title(window.title.as_str()); winit_window.set_title(window.title.as_str());
} }
if window.mode != cache.window.mode { if window.mode != cache.mode {
let new_mode = match window.mode { let new_mode = match window.mode {
WindowMode::BorderlessFullscreen(monitor_selection) => { WindowMode::BorderlessFullscreen(monitor_selection) => {
Some(Some(winit::window::Fullscreen::Borderless(select_monitor( Some(Some(winit::window::Fullscreen::Borderless(select_monitor(
@ -352,15 +356,15 @@ pub(crate) fn changed_windows(
} }
} }
if window.resolution != cache.window.resolution { if window.resolution != cache.resolution {
let mut physical_size = PhysicalSize::new( let mut physical_size = PhysicalSize::new(
window.resolution.physical_width(), window.resolution.physical_width(),
window.resolution.physical_height(), window.resolution.physical_height(),
); );
let cached_physical_size = PhysicalSize::new( let cached_physical_size = PhysicalSize::new(
cache.window.physical_width(), cache.physical_width(),
cache.window.physical_height(), cache.physical_height(),
); );
let base_scale_factor = window.resolution.base_scale_factor(); let base_scale_factor = window.resolution.base_scale_factor();
@ -368,12 +372,12 @@ pub(crate) fn changed_windows(
// Note: this may be different from `winit`'s base scale factor if // Note: this may be different from `winit`'s base scale factor if
// `scale_factor_override` is set to Some(f32) // `scale_factor_override` is set to Some(f32)
let scale_factor = window.scale_factor(); let scale_factor = window.scale_factor();
let cached_scale_factor = cache.window.scale_factor(); let cached_scale_factor = cache.scale_factor();
// Check and update `winit`'s physical size only if the window is not maximized // Check and update `winit`'s physical size only if the window is not maximized
if scale_factor != cached_scale_factor && !winit_window.is_maximized() { if scale_factor != cached_scale_factor && !winit_window.is_maximized() {
let logical_size = let logical_size =
if let Some(cached_factor) = cache.window.resolution.scale_factor_override() { if let Some(cached_factor) = cache.resolution.scale_factor_override() {
physical_size.to_logical::<f32>(cached_factor as f64) physical_size.to_logical::<f32>(cached_factor as f64)
} else { } else {
physical_size.to_logical::<f32>(base_scale_factor as f64) physical_size.to_logical::<f32>(base_scale_factor as f64)
@ -397,7 +401,7 @@ pub(crate) fn changed_windows(
} }
} }
if window.physical_cursor_position() != cache.window.physical_cursor_position() { if window.physical_cursor_position() != cache.physical_cursor_position() {
if let Some(physical_position) = window.physical_cursor_position() { if let Some(physical_position) = window.physical_cursor_position() {
let position = PhysicalPosition::new(physical_position.x, physical_position.y); let position = PhysicalPosition::new(physical_position.x, physical_position.y);
@ -407,44 +411,23 @@ pub(crate) fn changed_windows(
} }
} }
if window.cursor_options.grab_mode != cache.window.cursor_options.grab_mode if window.decorations != cache.decorations
&& crate::winit_windows::attempt_grab(winit_window, window.cursor_options.grab_mode)
.is_err()
{
window.cursor_options.grab_mode = cache.window.cursor_options.grab_mode;
}
if window.cursor_options.visible != cache.window.cursor_options.visible {
winit_window.set_cursor_visible(window.cursor_options.visible);
}
if window.cursor_options.hit_test != cache.window.cursor_options.hit_test {
if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) {
window.cursor_options.hit_test = cache.window.cursor_options.hit_test;
warn!(
"Could not set cursor hit test for window {}: {}",
window.title, err
);
}
}
if window.decorations != cache.window.decorations
&& window.decorations != winit_window.is_decorated() && window.decorations != winit_window.is_decorated()
{ {
winit_window.set_decorations(window.decorations); winit_window.set_decorations(window.decorations);
} }
if window.resizable != cache.window.resizable if window.resizable != cache.resizable
&& window.resizable != winit_window.is_resizable() && window.resizable != winit_window.is_resizable()
{ {
winit_window.set_resizable(window.resizable); winit_window.set_resizable(window.resizable);
} }
if window.enabled_buttons != cache.window.enabled_buttons { if window.enabled_buttons != cache.enabled_buttons {
winit_window.set_enabled_buttons(convert_enabled_buttons(window.enabled_buttons)); winit_window.set_enabled_buttons(convert_enabled_buttons(window.enabled_buttons));
} }
if window.resize_constraints != cache.window.resize_constraints { if window.resize_constraints != cache.resize_constraints {
let constraints = window.resize_constraints.check_constraints(); let constraints = window.resize_constraints.check_constraints();
let min_inner_size = LogicalSize { let min_inner_size = LogicalSize {
width: constraints.min_width, width: constraints.min_width,
@ -461,7 +444,7 @@ pub(crate) fn changed_windows(
} }
} }
if window.position != cache.window.position { if window.position != cache.position {
if let Some(position) = crate::winit_window_position( if let Some(position) = crate::winit_window_position(
&window.position, &window.position,
&window.resolution, &window.resolution,
@ -502,62 +485,62 @@ pub(crate) fn changed_windows(
} }
} }
if window.focused != cache.window.focused && window.focused { if window.focused != cache.focused && window.focused {
winit_window.focus_window(); winit_window.focus_window();
} }
if window.window_level != cache.window.window_level { if window.window_level != cache.window_level {
winit_window.set_window_level(convert_window_level(window.window_level)); winit_window.set_window_level(convert_window_level(window.window_level));
} }
// Currently unsupported changes // Currently unsupported changes
if window.transparent != cache.window.transparent { if window.transparent != cache.transparent {
window.transparent = cache.window.transparent; window.transparent = cache.transparent;
warn!("Winit does not currently support updating transparency after window creation."); warn!("Winit does not currently support updating transparency after window creation.");
} }
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
if window.canvas != cache.window.canvas { if window.canvas != cache.canvas {
window.canvas.clone_from(&cache.window.canvas); window.canvas.clone_from(&cache.canvas);
warn!( warn!(
"Bevy currently doesn't support modifying the window canvas after initialization." "Bevy currently doesn't support modifying the window canvas after initialization."
); );
} }
if window.ime_enabled != cache.window.ime_enabled { if window.ime_enabled != cache.ime_enabled {
winit_window.set_ime_allowed(window.ime_enabled); winit_window.set_ime_allowed(window.ime_enabled);
} }
if window.ime_position != cache.window.ime_position { if window.ime_position != cache.ime_position {
winit_window.set_ime_cursor_area( winit_window.set_ime_cursor_area(
LogicalPosition::new(window.ime_position.x, window.ime_position.y), LogicalPosition::new(window.ime_position.x, window.ime_position.y),
PhysicalSize::new(10, 10), PhysicalSize::new(10, 10),
); );
} }
if window.window_theme != cache.window.window_theme { if window.window_theme != cache.window_theme {
winit_window.set_theme(window.window_theme.map(convert_window_theme)); winit_window.set_theme(window.window_theme.map(convert_window_theme));
} }
if window.visible != cache.window.visible { if window.visible != cache.visible {
winit_window.set_visible(window.visible); winit_window.set_visible(window.visible);
} }
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
{ {
if window.recognize_pinch_gesture != cache.window.recognize_pinch_gesture { if window.recognize_pinch_gesture != cache.recognize_pinch_gesture {
winit_window.recognize_pinch_gesture(window.recognize_pinch_gesture); winit_window.recognize_pinch_gesture(window.recognize_pinch_gesture);
} }
if window.recognize_rotation_gesture != cache.window.recognize_rotation_gesture { if window.recognize_rotation_gesture != cache.recognize_rotation_gesture {
winit_window.recognize_rotation_gesture(window.recognize_rotation_gesture); winit_window.recognize_rotation_gesture(window.recognize_rotation_gesture);
} }
if window.recognize_doubletap_gesture != cache.window.recognize_doubletap_gesture { if window.recognize_doubletap_gesture != cache.recognize_doubletap_gesture {
winit_window.recognize_doubletap_gesture(window.recognize_doubletap_gesture); winit_window.recognize_doubletap_gesture(window.recognize_doubletap_gesture);
} }
if window.recognize_pan_gesture != cache.window.recognize_pan_gesture { if window.recognize_pan_gesture != cache.recognize_pan_gesture {
match ( match (
window.recognize_pan_gesture, window.recognize_pan_gesture,
cache.window.recognize_pan_gesture, cache.recognize_pan_gesture,
) { ) {
(Some(_), Some(_)) => { (Some(_), Some(_)) => {
warn!("Bevy currently doesn't support modifying PanGesture number of fingers recognition. Please disable it before re-enabling it with the new number of fingers"); warn!("Bevy currently doesn't support modifying PanGesture number of fingers recognition. Please disable it before re-enabling it with the new number of fingers");
@ -567,16 +550,15 @@ pub(crate) fn changed_windows(
} }
} }
if window.prefers_home_indicator_hidden != cache.window.prefers_home_indicator_hidden { if window.prefers_home_indicator_hidden != cache.prefers_home_indicator_hidden {
winit_window winit_window
.set_prefers_home_indicator_hidden(window.prefers_home_indicator_hidden); .set_prefers_home_indicator_hidden(window.prefers_home_indicator_hidden);
} }
if window.prefers_status_bar_hidden != cache.window.prefers_status_bar_hidden { if window.prefers_status_bar_hidden != cache.prefers_status_bar_hidden {
winit_window.set_prefers_status_bar_hidden(window.prefers_status_bar_hidden); winit_window.set_prefers_status_bar_hidden(window.prefers_status_bar_hidden);
} }
if window.preferred_screen_edges_deferring_system_gestures if window.preferred_screen_edges_deferring_system_gestures
!= cache != cache
.window
.preferred_screen_edges_deferring_system_gestures .preferred_screen_edges_deferring_system_gestures
{ {
use crate::converters::convert_screen_edge; use crate::converters::convert_screen_edge;
@ -585,7 +567,59 @@ pub(crate) fn changed_windows(
winit_window.set_preferred_screen_edges_deferring_system_gestures(preferred_edge); winit_window.set_preferred_screen_edges_deferring_system_gestures(preferred_edge);
} }
} }
cache.window = window.clone(); **cache = window.clone();
}
});
}
pub(crate) fn changed_cursor_options(
mut changed_windows: Query<
(
Entity,
&Window,
&mut CursorOptions,
&mut CachedCursorOptions,
),
Changed<CursorOptions>,
>,
_non_send_marker: NonSendMarker,
) {
WINIT_WINDOWS.with_borrow(|winit_windows| {
for (entity, window, mut cursor_options, mut cache) in &mut changed_windows {
// This system already only runs when the cursor options change, so we need to bypass change detection or the next frame will also run this system
let cursor_options = cursor_options.bypass_change_detection();
let Some(winit_window) = winit_windows.get_window(entity) else {
continue;
};
// Don't check the cache for the grab mode. It can change through external means, leaving the cache outdated.
if let Err(err) =
crate::winit_windows::attempt_grab(winit_window, cursor_options.grab_mode)
{
warn!(
"Could not set cursor grab mode for window {}: {}",
window.title, err
);
cursor_options.grab_mode = cache.grab_mode;
} else {
cache.grab_mode = cursor_options.grab_mode;
}
if cursor_options.visible != cache.visible {
winit_window.set_cursor_visible(cursor_options.visible);
cache.visible = cursor_options.visible;
}
if cursor_options.hit_test != cache.hit_test {
if let Err(err) = winit_window.set_cursor_hittest(cursor_options.hit_test) {
warn!(
"Could not set cursor hit test for window {}: {}",
window.title, err
);
cursor_options.hit_test = cache.hit_test;
} else {
cache.hit_test = cursor_options.hit_test;
}
}
} }
}); });
} }

View File

@ -4,8 +4,8 @@ use bevy_ecs::entity::Entity;
use bevy_ecs::entity::EntityHashMap; use bevy_ecs::entity::EntityHashMap;
use bevy_platform::collections::HashMap; use bevy_platform::collections::HashMap;
use bevy_window::{ use bevy_window::{
CursorGrabMode, MonitorSelection, VideoModeSelection, Window, WindowMode, WindowPosition, CursorGrabMode, CursorOptions, MonitorSelection, VideoModeSelection, Window, WindowMode,
WindowResolution, WindowWrapper, WindowPosition, WindowResolution, WindowWrapper,
}; };
use tracing::warn; use tracing::warn;
@ -58,6 +58,7 @@ impl WinitWindows {
event_loop: &ActiveEventLoop, event_loop: &ActiveEventLoop,
entity: Entity, entity: Entity,
window: &Window, window: &Window,
cursor_options: &CursorOptions,
adapters: &mut AccessKitAdapters, adapters: &mut AccessKitAdapters,
handlers: &mut WinitActionRequestHandlers, handlers: &mut WinitActionRequestHandlers,
accessibility_requested: &AccessibilityRequested, accessibility_requested: &AccessibilityRequested,
@ -310,16 +311,16 @@ impl WinitWindows {
winit_window.set_visible(window.visible); winit_window.set_visible(window.visible);
// Do not set the grab mode on window creation if it's none. It can fail on mobile. // Do not set the grab mode on window creation if it's none. It can fail on mobile.
if window.cursor_options.grab_mode != CursorGrabMode::None { if cursor_options.grab_mode != CursorGrabMode::None {
let _ = attempt_grab(&winit_window, window.cursor_options.grab_mode); let _ = attempt_grab(&winit_window, cursor_options.grab_mode);
} }
winit_window.set_cursor_visible(window.cursor_options.visible); winit_window.set_cursor_visible(cursor_options.visible);
// Do not set the cursor hittest on window creation if it's false, as it will always fail on // Do not set the cursor hittest on window creation if it's false, as it will always fail on
// some platforms and log an unfixable warning. // some platforms and log an unfixable warning.
if !window.cursor_options.hit_test { if !cursor_options.hit_test {
if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) { if let Err(err) = winit_window.set_cursor_hittest(cursor_options.hit_test) {
warn!( warn!(
"Could not set cursor hit test for window {}: {}", "Could not set cursor hit test for window {}: {}",
window.title, err window.title, err

View File

@ -10,7 +10,7 @@ use bevy::{
app::AppExit, app::AppExit,
input::common_conditions::{input_just_pressed, input_just_released}, input::common_conditions::{input_just_pressed, input_just_released},
prelude::*, prelude::*,
window::{PrimaryWindow, WindowLevel}, window::{CursorOptions, PrimaryWindow, WindowLevel},
}; };
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@ -219,12 +219,13 @@ fn get_cursor_world_pos(
/// Update whether the window is clickable or not /// Update whether the window is clickable or not
fn update_cursor_hit_test( fn update_cursor_hit_test(
cursor_world_pos: Res<CursorWorldPos>, cursor_world_pos: Res<CursorWorldPos>,
mut primary_window: Single<&mut Window, With<PrimaryWindow>>, primary_window: Single<(&Window, &mut CursorOptions), With<PrimaryWindow>>,
bevy_logo_transform: Single<&Transform, With<BevyLogo>>, bevy_logo_transform: Single<&Transform, With<BevyLogo>>,
) { ) {
let (window, mut cursor_options) = primary_window.into_inner();
// If the window has decorations (e.g. a border) then it should be clickable // If the window has decorations (e.g. a border) then it should be clickable
if primary_window.decorations { if window.decorations {
primary_window.cursor_options.hit_test = true; cursor_options.hit_test = true;
return; return;
} }
@ -234,7 +235,7 @@ fn update_cursor_hit_test(
}; };
// If the cursor is within the radius of the Bevy logo make the window clickable otherwise the window is not clickable // If the cursor is within the radius of the Bevy logo make the window clickable otherwise the window is not clickable
primary_window.cursor_options.hit_test = bevy_logo_transform cursor_options.hit_test = bevy_logo_transform
.translation .translation
.truncate() .truncate()
.distance(cursor_world_pos) .distance(cursor_world_pos)

View File

@ -8,7 +8,7 @@
use bevy::{ use bevy::{
input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseScrollUnit}, input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseScrollUnit},
prelude::*, prelude::*,
window::CursorGrabMode, window::{CursorGrabMode, CursorOptions},
}; };
use std::{f32::consts::*, fmt}; use std::{f32::consts::*, fmt};
@ -126,7 +126,7 @@ Freecam Controls:
fn run_camera_controller( fn run_camera_controller(
time: Res<Time>, time: Res<Time>,
mut windows: Query<&mut Window>, mut windows: Query<(&Window, &mut CursorOptions)>,
accumulated_mouse_motion: Res<AccumulatedMouseMotion>, accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
accumulated_mouse_scroll: Res<AccumulatedMouseScroll>, accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,
mouse_button_input: Res<ButtonInput<MouseButton>>, mouse_button_input: Res<ButtonInput<MouseButton>>,
@ -226,18 +226,18 @@ fn run_camera_controller(
// Handle cursor grab // Handle cursor grab
if cursor_grab_change { if cursor_grab_change {
if cursor_grab { if cursor_grab {
for mut window in &mut windows { for (window, mut cursor_options) in &mut windows {
if !window.focused { if !window.focused {
continue; continue;
} }
window.cursor_options.grab_mode = CursorGrabMode::Locked; cursor_options.grab_mode = CursorGrabMode::Locked;
window.cursor_options.visible = false; cursor_options.visible = false;
} }
} else { } else {
for mut window in &mut windows { for (_, mut cursor_options) in &mut windows {
window.cursor_options.grab_mode = CursorGrabMode::None; cursor_options.grab_mode = CursorGrabMode::None;
window.cursor_options.visible = true; cursor_options.visible = true;
} }
} }
} }

View File

@ -1,6 +1,9 @@
//! Demonstrates how to grab and hide the mouse cursor. //! Demonstrates how to grab and hide the mouse cursor.
use bevy::{prelude::*, window::CursorGrabMode}; use bevy::{
prelude::*,
window::{CursorGrabMode, CursorOptions},
};
fn main() { fn main() {
App::new() App::new()
@ -12,17 +15,17 @@ fn main() {
// This system grabs the mouse when the left mouse button is pressed // This system grabs the mouse when the left mouse button is pressed
// and releases it when the escape key is pressed // and releases it when the escape key is pressed
fn grab_mouse( fn grab_mouse(
mut window: Single<&mut Window>, mut cursor_options: Single<&mut CursorOptions>,
mouse: Res<ButtonInput<MouseButton>>, mouse: Res<ButtonInput<MouseButton>>,
key: Res<ButtonInput<KeyCode>>, key: Res<ButtonInput<KeyCode>>,
) { ) {
if mouse.just_pressed(MouseButton::Left) { if mouse.just_pressed(MouseButton::Left) {
window.cursor_options.visible = false; cursor_options.visible = false;
window.cursor_options.grab_mode = CursorGrabMode::Locked; cursor_options.grab_mode = CursorGrabMode::Locked;
} }
if key.just_pressed(KeyCode::Escape) { if key.just_pressed(KeyCode::Escape) {
window.cursor_options.visible = true; cursor_options.visible = true;
window.cursor_options.grab_mode = CursorGrabMode::None; cursor_options.grab_mode = CursorGrabMode::None;
} }
} }

View File

@ -2,7 +2,7 @@
//! If you build this, and hit 'P' it should toggle on/off the mouse's passthrough. //! If you build this, and hit 'P' it should toggle on/off the mouse's passthrough.
//! Note: this example will not work on following platforms: iOS / Android / Web / X11. Window fall through is not supported there. //! Note: this example will not work on following platforms: iOS / Android / Web / X11. Window fall through is not supported there.
use bevy::prelude::*; use bevy::{prelude::*, window::CursorOptions};
fn main() { fn main() {
App::new() App::new()
@ -46,9 +46,9 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// A simple system to handle some keyboard input and toggle on/off the hit test. // A simple system to handle some keyboard input and toggle on/off the hit test.
fn toggle_mouse_passthrough( fn toggle_mouse_passthrough(
keyboard_input: Res<ButtonInput<KeyCode>>, keyboard_input: Res<ButtonInput<KeyCode>>,
mut window: Single<&mut Window>, mut cursor_options: Single<&mut CursorOptions>,
) { ) {
if keyboard_input.just_pressed(KeyCode::KeyP) { if keyboard_input.just_pressed(KeyCode::KeyP) {
window.cursor_options.hit_test = !window.cursor_options.hit_test; cursor_options.hit_test = !cursor_options.hit_test;
} }
} }

View File

@ -6,7 +6,9 @@ use bevy::winit::cursor::{CustomCursor, CustomCursorImage};
use bevy::{ use bevy::{
diagnostic::{FrameCount, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, diagnostic::{FrameCount, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*, prelude::*,
window::{CursorGrabMode, PresentMode, SystemCursorIcon, WindowLevel, WindowTheme}, window::{
CursorGrabMode, CursorOptions, PresentMode, SystemCursorIcon, WindowLevel, WindowTheme,
},
winit::cursor::CursorIcon, winit::cursor::CursorIcon,
}; };
@ -128,10 +130,10 @@ fn change_title(mut window: Single<&mut Window>, time: Res<Time>) {
); );
} }
fn toggle_cursor(mut window: Single<&mut Window>, input: Res<ButtonInput<KeyCode>>) { fn toggle_cursor(mut cursor_options: Single<&mut CursorOptions>, input: Res<ButtonInput<KeyCode>>) {
if input.just_pressed(KeyCode::Space) { if input.just_pressed(KeyCode::Space) {
window.cursor_options.visible = !window.cursor_options.visible; cursor_options.visible = !cursor_options.visible;
window.cursor_options.grab_mode = match window.cursor_options.grab_mode { cursor_options.grab_mode = match cursor_options.grab_mode {
CursorGrabMode::None => CursorGrabMode::Locked, CursorGrabMode::None => CursorGrabMode::Locked,
CursorGrabMode::Locked | CursorGrabMode::Confined => CursorGrabMode::None, CursorGrabMode::Locked | CursorGrabMode::Confined => CursorGrabMode::None,
}; };

View File

@ -0,0 +1,44 @@
---
title: Window is now split into multiple components
pull_requests: [19668]
---
`Window` has become a very large component over the last few releases. To improve our internal handling of it and to make it more approachable, we
have split it into multiple components, all on the same entity. So far, this affects `CursorOptions`:
```rust
// old
fn lock_cursor(primary_window: Single<&mut Window, With<PrimaryWindow>>) {
primary_window.cursor_options.grab_mode = CursorGrabMode::Locked;
}
// new
fn lock_cursor(primary_cursor_options: Single<&mut CursorOptions, With<PrimaryWindow>>) {
primary_cursor_options.grab_mode = CursorGrabMode::Locked;
}
```
This split also applies when specifying the initial settings for the primary window:
```rust
// old
app.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
cursor_options: CursorOptions {
grab_mode: CursorGrabMode::Locked,
..default()
},
..default()
}),
..default()
}));
// new
app.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_cursor_options: Some(CursorOptions {
grab_mode: CursorGrabMode::Locked,
..default()
}),
..default()
}));
```

View File

@ -3,7 +3,7 @@ index f578658cd..ffac22062 100644
--- a/crates/bevy_window/src/window.rs --- a/crates/bevy_window/src/window.rs
+++ b/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs
@@ -318,7 +318,7 @@ impl Default for Window { @@ -318,7 +318,7 @@ impl Default for Window {
cursor_options: Default::default(), name: None,
present_mode: Default::default(), present_mode: Default::default(),
mode: Default::default(), mode: Default::default(),
- position: Default::default(), - position: Default::default(),