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 {
WindowPlugin {
primary_window: Some(Window::default()),
primary_cursor_options: Some(CursorOptions::default()),
exit_condition: ExitCondition::OnAllClosed,
close_when_requested: true,
}
@ -76,6 +77,13 @@ pub struct WindowPlugin {
/// [`exit_on_all_closed`].
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.
///
/// If disabling this, ensure that you send the [`bevy_app::AppExit`]
@ -122,10 +130,14 @@ impl Plugin for WindowPlugin {
.add_event::<AppLifecycle>();
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,
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 {
@ -168,7 +180,8 @@ impl Plugin for WindowPlugin {
// Register window descriptor and related types
#[cfg(feature = "bevy_reflect")]
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"),
reflect(Serialize, Deserialize)
)]
#[require(CursorOptions)]
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.
pub present_mode: PresentMode,
/// Which fullscreen or windowing mode should be used.
@ -470,7 +468,6 @@ impl Default for Window {
Self {
title: DEFAULT_WINDOW_TITLE.to_owned(),
name: None,
cursor_options: Default::default(),
present_mode: Default::default(),
mode: Default::default(),
position: Default::default(),
@ -728,11 +725,11 @@ impl WindowResizeConstraints {
}
/// Cursor data for a [`Window`].
#[derive(Debug, Clone)]
#[derive(Component, Debug, Clone)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, Default, Clone)
reflect(Component, Debug, Default, Clone)
)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(

View File

@ -25,8 +25,8 @@ use winit::{event_loop::EventLoop, window::WindowId};
use bevy_a11y::AccessibilityRequested;
use bevy_app::{App, Last, Plugin};
use bevy_ecs::prelude::*;
use bevy_window::{exit_on_all_closed, Window, WindowCreated};
use system::{changed_windows, check_keyboard_focus_lost, despawn_windows};
use bevy_window::{exit_on_all_closed, CursorOptions, Window, WindowCreated};
use system::{changed_cursor_options, changed_windows, check_keyboard_focus_lost, despawn_windows};
pub use system::{create_monitors, create_windows};
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
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,
// so we don't need to care about its ordering relative to `changed_windows`
changed_windows.ambiguous_with(exit_on_all_closed),
changed_cursor_options,
despawn_windows,
check_keyboard_focus_lost,
)
@ -211,6 +212,7 @@ pub type CreateWindowParams<'w, 's, F = ()> = (
(
Entity,
&'static mut Window,
&'static CursorOptions,
Option<&'static RawHandleWrapperHolder>,
),
F,

View File

@ -46,7 +46,7 @@ use bevy_window::{
WindowScaleFactorChanged, WindowThemeChanged,
};
#[cfg(target_os = "android")]
use bevy_window::{PrimaryWindow, RawHandleWrapper};
use bevy_window::{CursorOptions, PrimaryWindow, RawHandleWrapper};
use crate::{
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 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
// handle wrapper removed when the app was suspended.
let mut query = self.world_mut()
.query_filtered::<(Entity, &Window), (With<CachedWindow>, Without<RawHandleWrapper>)>();
if let Ok((entity, window)) = query.single(&self.world()) {
.query_filtered::<(Entity, &Window, &CursorOptions), (With<CachedWindow>, Without<RawHandleWrapper>)>();
if let Ok((entity, window, cursor_options)) = query.single(&self.world()) {
let window = window.clone();
let cursor_options = cursor_options.clone();
WINIT_WINDOWS.with_borrow_mut(|winit_windows| {
ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| {
@ -622,6 +624,7 @@ impl<T: BufferedEvent> WinitAppRunnerState<T> {
event_loop,
entity,
&window,
&cursor_options,
adapters,
&mut handlers,
&accessibility_requested,

View File

@ -1,6 +1,8 @@
use std::collections::HashMap;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
change_detection::DetectChangesMut,
entity::Entity,
event::EventWriter,
lifecycle::RemovedComponents,
@ -10,9 +12,9 @@ use bevy_ecs::{
};
use bevy_input::keyboard::{Key, KeyCode, KeyboardFocusLost, KeyboardInput};
use bevy_window::{
ClosingWindow, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window, WindowClosed,
WindowClosing, WindowCreated, WindowEvent, WindowFocused, WindowMode, WindowResized,
WindowWrapper,
ClosingWindow, CursorOptions, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window,
WindowClosed, WindowClosing, WindowCreated, WindowEvent, WindowFocused, WindowMode,
WindowResized, WindowWrapper,
};
use tracing::{error, info, warn};
@ -59,7 +61,7 @@ pub fn create_windows<F: QueryFilter + 'static>(
) {
WINIT_WINDOWS.with_borrow_mut(|winit_windows| {
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() {
continue;
}
@ -70,6 +72,7 @@ pub fn create_windows<F: QueryFilter + 'static>(
event_loop,
entity,
&window,
cursor_options,
adapters,
&mut handlers,
&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);
commands.entity(entity).insert((
CachedWindow {
window: window.clone(),
},
CachedWindow(window.clone()),
CachedCursorOptions(cursor_options.clone()),
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.
#[derive(Debug, Clone, Component)]
pub struct CachedWindow {
pub window: Window,
}
#[derive(Debug, Clone, Component, Deref, DerefMut)]
pub(crate) struct CachedWindow(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.
///
@ -306,11 +310,11 @@ pub(crate) fn changed_windows(
continue;
};
if window.title != cache.window.title {
if window.title != cache.title {
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 {
WindowMode::BorderlessFullscreen(monitor_selection) => {
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(
window.resolution.physical_width(),
window.resolution.physical_height(),
);
let cached_physical_size = PhysicalSize::new(
cache.window.physical_width(),
cache.window.physical_height(),
cache.physical_width(),
cache.physical_height(),
);
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
// `scale_factor_override` is set to Some(f32)
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
if scale_factor != cached_scale_factor && !winit_window.is_maximized() {
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)
} else {
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() {
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
&& 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
if window.decorations != cache.decorations
&& window.decorations != winit_window.is_decorated()
{
winit_window.set_decorations(window.decorations);
}
if window.resizable != cache.window.resizable
if window.resizable != cache.resizable
&& window.resizable != winit_window.is_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));
}
if window.resize_constraints != cache.window.resize_constraints {
if window.resize_constraints != cache.resize_constraints {
let constraints = window.resize_constraints.check_constraints();
let min_inner_size = LogicalSize {
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(
&window.position,
&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();
}
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));
}
// Currently unsupported changes
if window.transparent != cache.window.transparent {
window.transparent = cache.window.transparent;
if window.transparent != cache.transparent {
window.transparent = cache.transparent;
warn!("Winit does not currently support updating transparency after window creation.");
}
#[cfg(target_arch = "wasm32")]
if window.canvas != cache.window.canvas {
window.canvas.clone_from(&cache.window.canvas);
if window.canvas != cache.canvas {
window.canvas.clone_from(&cache.canvas);
warn!(
"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);
}
if window.ime_position != cache.window.ime_position {
if window.ime_position != cache.ime_position {
winit_window.set_ime_cursor_area(
LogicalPosition::new(window.ime_position.x, window.ime_position.y),
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));
}
if window.visible != cache.window.visible {
if window.visible != cache.visible {
winit_window.set_visible(window.visible);
}
#[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);
}
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);
}
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);
}
if window.recognize_pan_gesture != cache.window.recognize_pan_gesture {
if window.recognize_pan_gesture != cache.recognize_pan_gesture {
match (
window.recognize_pan_gesture,
cache.window.recognize_pan_gesture,
cache.recognize_pan_gesture,
) {
(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");
@ -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
.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);
}
if window.preferred_screen_edges_deferring_system_gestures
!= cache
.window
.preferred_screen_edges_deferring_system_gestures
{
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);
}
}
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_platform::collections::HashMap;
use bevy_window::{
CursorGrabMode, MonitorSelection, VideoModeSelection, Window, WindowMode, WindowPosition,
WindowResolution, WindowWrapper,
CursorGrabMode, CursorOptions, MonitorSelection, VideoModeSelection, Window, WindowMode,
WindowPosition, WindowResolution, WindowWrapper,
};
use tracing::warn;
@ -58,6 +58,7 @@ impl WinitWindows {
event_loop: &ActiveEventLoop,
entity: Entity,
window: &Window,
cursor_options: &CursorOptions,
adapters: &mut AccessKitAdapters,
handlers: &mut WinitActionRequestHandlers,
accessibility_requested: &AccessibilityRequested,
@ -310,16 +311,16 @@ impl WinitWindows {
winit_window.set_visible(window.visible);
// 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 {
let _ = attempt_grab(&winit_window, window.cursor_options.grab_mode);
if cursor_options.grab_mode != CursorGrabMode::None {
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
// some platforms and log an unfixable warning.
if !window.cursor_options.hit_test {
if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) {
if !cursor_options.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

View File

@ -10,7 +10,7 @@ use bevy::{
app::AppExit,
input::common_conditions::{input_just_pressed, input_just_released},
prelude::*,
window::{PrimaryWindow, WindowLevel},
window::{CursorOptions, PrimaryWindow, WindowLevel},
};
#[cfg(target_os = "macos")]
@ -219,12 +219,13 @@ fn get_cursor_world_pos(
/// Update whether the window is clickable or not
fn update_cursor_hit_test(
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>>,
) {
let (window, mut cursor_options) = primary_window.into_inner();
// If the window has decorations (e.g. a border) then it should be clickable
if primary_window.decorations {
primary_window.cursor_options.hit_test = true;
if window.decorations {
cursor_options.hit_test = true;
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
primary_window.cursor_options.hit_test = bevy_logo_transform
cursor_options.hit_test = bevy_logo_transform
.translation
.truncate()
.distance(cursor_world_pos)

View File

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

View File

@ -1,6 +1,9 @@
//! Demonstrates how to grab and hide the mouse cursor.
use bevy::{prelude::*, window::CursorGrabMode};
use bevy::{
prelude::*,
window::{CursorGrabMode, CursorOptions},
};
fn main() {
App::new()
@ -12,17 +15,17 @@ fn main() {
// This system grabs the mouse when the left mouse button is pressed
// and releases it when the escape key is pressed
fn grab_mouse(
mut window: Single<&mut Window>,
mut cursor_options: Single<&mut CursorOptions>,
mouse: Res<ButtonInput<MouseButton>>,
key: Res<ButtonInput<KeyCode>>,
) {
if mouse.just_pressed(MouseButton::Left) {
window.cursor_options.visible = false;
window.cursor_options.grab_mode = CursorGrabMode::Locked;
cursor_options.visible = false;
cursor_options.grab_mode = CursorGrabMode::Locked;
}
if key.just_pressed(KeyCode::Escape) {
window.cursor_options.visible = true;
window.cursor_options.grab_mode = CursorGrabMode::None;
cursor_options.visible = true;
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.
//! 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() {
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.
fn toggle_mouse_passthrough(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut window: Single<&mut Window>,
mut cursor_options: Single<&mut CursorOptions>,
) {
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::{
diagnostic::{FrameCount, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
window::{CursorGrabMode, PresentMode, SystemCursorIcon, WindowLevel, WindowTheme},
window::{
CursorGrabMode, CursorOptions, PresentMode, SystemCursorIcon, WindowLevel, WindowTheme,
},
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) {
window.cursor_options.visible = !window.cursor_options.visible;
window.cursor_options.grab_mode = match window.cursor_options.grab_mode {
cursor_options.visible = !cursor_options.visible;
cursor_options.grab_mode = match cursor_options.grab_mode {
CursorGrabMode::None => CursorGrabMode::Locked,
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
+++ b/crates/bevy_window/src/window.rs
@@ -318,7 +318,7 @@ impl Default for Window {
cursor_options: Default::default(),
name: None,
present_mode: Default::default(),
mode: Default::default(),
- position: Default::default(),