diff --git a/Cargo.toml b/Cargo.toml index fae29567b7..ae5f431f3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -359,6 +359,10 @@ argh = "0.1.12" thiserror = "1.0" event-listener = "5.3.0" +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +wasm-bindgen = { version = "0.2" } +web-sys = { version = "0.3", features = ["Window"] } + [[example]] name = "hello_world" path = "examples/hello_world.rs" @@ -2836,6 +2840,17 @@ description = "Creates a solid color window" category = "Window" wasm = true +[[example]] +name = "custom_user_event" +path = "examples/window/custom_user_event.rs" +doc-scrape-examples = true + +[package.metadata.example.custom_user_event] +name = "Custom User Event" +description = "Handles custom user events within the event loop" +category = "Window" +wasm = true + [[example]] name = "low_power" path = "examples/window/low_power.rs" diff --git a/crates/bevy_a11y/Cargo.toml b/crates/bevy_a11y/Cargo.toml index 4ee262fa22..e45b112bdc 100644 --- a/crates/bevy_a11y/Cargo.toml +++ b/crates/bevy_a11y/Cargo.toml @@ -14,7 +14,7 @@ bevy_app = { path = "../bevy_app", version = "0.14.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" } -accesskit = "0.12" +accesskit = "0.14" [lints] workspace = true diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 4c1ecb3e09..026d41cc09 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -69,7 +69,7 @@ impl PluginGroup for DefaultPlugins { #[cfg(feature = "bevy_winit")] { - group = group.add(bevy_winit::WinitPlugin::default()); + group = group.add::(bevy_winit::WinitPlugin::default()); } #[cfg(feature = "bevy_render")] diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 841ed5956c..49f25530b1 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -58,7 +58,7 @@ use bevy_utils::prelude::default; pub use extract_param::Extract; use bevy_hierarchy::ValidParentCheckPlugin; -use bevy_window::{PrimaryWindow, RawHandleWrapper}; +use bevy_window::{PrimaryWindow, RawHandleWrapperHolder}; use extract_resource::ExtractResourcePlugin; use globals::GlobalsPlugin; use render_asset::RenderAssetBytesPerFrame; @@ -268,10 +268,9 @@ impl Plugin for RenderPlugin { )); let mut system_state: SystemState< - Query<&RawHandleWrapper, With>, + Query<&RawHandleWrapperHolder, With>, > = SystemState::new(app.world_mut()); let primary_window = system_state.get(app.world()).get_single().ok().cloned(); - let settings = render_creation.clone(); let async_renderer = async move { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { @@ -282,11 +281,20 @@ impl Plugin for RenderPlugin { }); // SAFETY: Plugins should be set up on the main thread. - let surface = primary_window.map(|wrapper| unsafe { - let handle = wrapper.get_handle(); - instance - .create_surface(handle) - .expect("Failed to create wgpu surface") + let surface = primary_window.and_then(|wrapper| unsafe { + let maybe_handle = wrapper.0.lock().expect( + "Couldn't get the window handle in time for renderer initialization", + ); + if let Some(wrapper) = maybe_handle.as_ref() { + let handle = wrapper.get_handle(); + Some( + instance + .create_surface(handle) + .expect("Failed to create wgpu surface"), + ) + } else { + None + } }); let request_adapter_options = wgpu::RequestAdapterOptions { diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index c7db7eeae7..f2c59e073b 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -22,7 +22,7 @@ concurrent-queue = { version = "2.0.0", optional = true } wasm-bindgen-futures = "0.4" [dev-dependencies] -web-time = { version = "0.2" } +web-time = { version = "1.1" } [lints] workspace = true diff --git a/crates/bevy_utils/Cargo.toml b/crates/bevy_utils/Cargo.toml index 8b6ac50a76..54e7bf938d 100644 --- a/crates/bevy_utils/Cargo.toml +++ b/crates/bevy_utils/Cargo.toml @@ -14,7 +14,7 @@ detailed_trace = [] [dependencies] ahash = "0.8.7" tracing = { version = "0.1", default-features = false, features = ["std"] } -web-time = { version = "0.2" } +web-time = { version = "1.1" } hashbrown = { version = "0.14", features = ["serde"] } bevy_utils_proc_macros = { version = "0.14.0-dev", path = "macros" } thread_local = "1.0" diff --git a/crates/bevy_window/src/event.rs b/crates/bevy_window/src/event.rs index 9bc698acae..682ccde3bd 100644 --- a/crates/bevy_window/src/event.rs +++ b/crates/bevy_window/src/event.rs @@ -388,13 +388,28 @@ pub struct WindowThemeChanged { derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -pub enum ApplicationLifetime { - /// The application just started. - Started, +pub enum AppLifecycle { + /// The application is not started yet. + Idle, + /// The application is running. + Running, + /// The application is going to be suspended. + /// Applications have one frame to react to this event before being paused in the background. + WillSuspend, /// The application was suspended. - /// - /// On Android, applications have one frame to react to this event before being paused in the background. Suspended, - /// The application was resumed. - Resumed, + /// The application is going to be resumed. + /// Applications have one extra frame to react to this event before being fully resumed. + WillResume, +} + +impl AppLifecycle { + /// Return `true` if the app can be updated. + #[inline] + pub fn is_active(&self) -> bool { + match self { + Self::Idle | Self::Suspended => false, + Self::Running | Self::WillSuspend | Self::WillResume => true, + } + } } diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index 7ecf8d21c0..9dd91bd862 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -11,6 +11,8 @@ //! The [`WindowPlugin`] sets up some global window-related parameters and //! is part of the [`DefaultPlugins`](https://docs.rs/bevy/latest/bevy/struct.DefaultPlugins.html). +use std::sync::{Arc, Mutex}; + use bevy_a11y::Focus; mod cursor; @@ -106,13 +108,16 @@ impl Plugin for WindowPlugin { .add_event::() .add_event::() .add_event::() - .add_event::(); + .add_event::(); if let Some(primary_window) = &self.primary_window { let initial_focus = app .world_mut() .spawn(primary_window.clone()) - .insert(PrimaryWindow) + .insert(( + PrimaryWindow, + RawHandleWrapperHolder(Arc::new(Mutex::new(None))), + )) .id(); if let Some(mut focus) = app.world_mut().get_resource_mut::() { **focus = Some(initial_focus); @@ -153,7 +158,7 @@ impl Plugin for WindowPlugin { .register_type::() .register_type::() .register_type::() - .register_type::(); + .register_type::(); // Register window descriptor and related types app.register_type::() diff --git a/crates/bevy_window/src/raw_handle.rs b/crates/bevy_window/src/raw_handle.rs index 0b799a1142..81b9a37096 100644 --- a/crates/bevy_window/src/raw_handle.rs +++ b/crates/bevy_window/src/raw_handle.rs @@ -5,7 +5,12 @@ use raw_window_handle::{ DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle, WindowHandle, }; -use std::{any::Any, marker::PhantomData, ops::Deref, sync::Arc}; +use std::{ + any::Any, + marker::PhantomData, + ops::Deref, + sync::{Arc, Mutex}, +}; /// A wrapper over a window. /// @@ -116,3 +121,7 @@ impl HasDisplayHandle for ThreadLockedRawWindowHandleWrapper { Ok(unsafe { DisplayHandle::borrow_raw(self.0.display_handle) }) } } + +/// Holder of the [`RawHandleWrapper`] with wrappers, to allow use in asynchronous context +#[derive(Debug, Clone, Component)] +pub struct RawHandleWrapperHolder(pub Arc>>); diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 453c9245b5..fdb8ae5c3d 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -687,10 +687,10 @@ impl Default for WindowResolution { impl WindowResolution { /// Creates a new [`WindowResolution`]. - pub fn new(logical_width: f32, logical_height: f32) -> Self { + pub fn new(physical_width: f32, physical_height: f32) -> Self { Self { - physical_width: logical_width as u32, - physical_height: logical_height as u32, + physical_width: physical_width as u32, + physical_height: physical_height as u32, ..Default::default() } } @@ -783,9 +783,7 @@ impl WindowResolution { /// Set the window's scale factor, this may get overridden by the backend. #[inline] pub fn set_scale_factor(&mut self, scale_factor: f32) { - let (width, height) = (self.width(), self.height()); self.scale_factor = scale_factor; - self.set(width, height); } /// Set the window's scale factor, this will be used over what the backend decides. @@ -794,9 +792,7 @@ impl WindowResolution { /// size is not within the limits. #[inline] pub fn set_scale_factor_override(&mut self, scale_factor_override: Option) { - let (width, height) = (self.width(), self.height()); self.scale_factor_override = scale_factor_override; - self.set(width, height); } } diff --git a/crates/bevy_winit/Cargo.toml b/crates/bevy_winit/Cargo.toml index 064a3afcdc..8a3db084d9 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -32,8 +32,8 @@ bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" } # other # feature rwh_06 refers to window_raw_handle@v0.6 -winit = { version = "0.29", default-features = false, features = ["rwh_06"] } -accesskit_winit = { version = "0.17", default-features = false, features = [ +winit = { version = "0.30", default-features = false, features = ["rwh_06"] } +accesskit_winit = { version = "0.20", default-features = false, features = [ "rwh_06", ] } approx = { version = "0.5", default-features = false } @@ -42,7 +42,7 @@ raw-window-handle = "0.6" serde = { version = "1.0", features = ["derive"], optional = true } [target.'cfg(target_os = "android")'.dependencies] -winit = { version = "0.29", default-features = false, features = [ +winit = { version = "0.30", default-features = false, features = [ "android-native-activity", "rwh_06", ] } diff --git a/crates/bevy_winit/src/accessibility.rs b/crates/bevy_winit/src/accessibility.rs index 1764f9cbaa..e72c6c8cfe 100644 --- a/crates/bevy_winit/src/accessibility.rs +++ b/crates/bevy_winit/src/accessibility.rs @@ -6,10 +6,9 @@ use std::{ }; use accesskit_winit::Adapter; +use bevy_a11y::accesskit::{ActivationHandler, DeactivationHandler, Node}; use bevy_a11y::{ - accesskit::{ - ActionHandler, ActionRequest, NodeBuilder, NodeClassSet, NodeId, Role, Tree, TreeUpdate, - }, + accesskit::{ActionHandler, ActionRequest, NodeBuilder, NodeId, Role, Tree, TreeUpdate}, AccessibilityNode, AccessibilityRequested, AccessibilitySystem, Focus, }; use bevy_a11y::{ActionRequest as ActionRequestWrapper, ManageAccessibilityUpdates}; @@ -20,7 +19,7 @@ use bevy_ecs::{ prelude::{DetectChanges, Entity, EventReader, EventWriter}, query::With, schedule::IntoSystemConfigs, - system::{NonSend, NonSendMut, Query, Res, ResMut, Resource}, + system::{NonSendMut, Query, Res, ResMut, Resource}, }; use bevy_hierarchy::{Children, Parent}; use bevy_window::{PrimaryWindow, Window, WindowClosed}; @@ -29,13 +28,78 @@ use bevy_window::{PrimaryWindow, Window, WindowClosed}; #[derive(Default, Deref, DerefMut)] pub struct AccessKitAdapters(pub EntityHashMap); -/// Maps window entities to their respective [`WinitActionHandler`]s. +/// Maps window entities to their respective [`WinitActionRequests`]s. #[derive(Resource, Default, Deref, DerefMut)] -pub struct WinitActionHandlers(pub EntityHashMap); +pub struct WinitActionRequestHandlers(pub EntityHashMap>>); /// Forwards `AccessKit` [`ActionRequest`]s from winit to an event channel. #[derive(Clone, Default, Deref, DerefMut)] -pub struct WinitActionHandler(pub Arc>>); +pub struct WinitActionRequestHandler(pub VecDeque); + +impl WinitActionRequestHandler { + fn new() -> Arc> { + Arc::new(Mutex::new(Self(VecDeque::new()))) + } +} + +struct AccessKitState { + name: String, + entity: Entity, + requested: AccessibilityRequested, +} + +impl AccessKitState { + fn new( + name: impl Into, + entity: Entity, + requested: AccessibilityRequested, + ) -> Arc> { + let name = name.into(); + + Arc::new(Mutex::new(Self { + name, + entity, + requested, + })) + } + + fn build_root(&mut self) -> Node { + let mut builder = NodeBuilder::new(Role::Window); + builder.set_name(self.name.clone()); + builder.build() + } + + fn build_initial_tree(&mut self) -> TreeUpdate { + let root = self.build_root(); + let accesskit_window_id = NodeId(self.entity.to_bits()); + let mut tree = Tree::new(accesskit_window_id); + tree.app_name = Some(self.name.clone()); + self.requested.set(true); + + TreeUpdate { + nodes: vec![(accesskit_window_id, root)], + tree: Some(tree), + focus: accesskit_window_id, + } + } +} + +struct WinitActivationHandler(Arc>); + +impl ActivationHandler for WinitActivationHandler { + fn request_initial_tree(&mut self) -> Option { + Some(self.0.lock().unwrap().build_initial_tree()) + } +} + +impl WinitActivationHandler { + pub fn new(state: Arc>) -> Self { + Self(state) + } +} + +#[derive(Clone, Default)] +struct WinitActionHandler(Arc>); impl ActionHandler for WinitActionHandler { fn do_action(&mut self, request: ActionRequest) { @@ -44,6 +108,18 @@ impl ActionHandler for WinitActionHandler { } } +impl WinitActionHandler { + pub fn new(handler: Arc>) -> Self { + Self(handler) + } +} + +struct WinitDeactivationHandler; + +impl DeactivationHandler for WinitDeactivationHandler { + fn deactivate_accessibility(&mut self) {} +} + /// Prepares accessibility for a winit window. pub(crate) fn prepare_accessibility_for_window( winit_window: &winit::window::Window, @@ -51,43 +127,39 @@ pub(crate) fn prepare_accessibility_for_window( name: String, accessibility_requested: AccessibilityRequested, adapters: &mut AccessKitAdapters, - handlers: &mut WinitActionHandlers, + handlers: &mut WinitActionRequestHandlers, ) { - let mut root_builder = NodeBuilder::new(Role::Window); - root_builder.set_name(name.into_boxed_str()); - let root = root_builder.build(&mut NodeClassSet::lock_global()); + let state = AccessKitState::new(name, entity, accessibility_requested); + let activation_handler = WinitActivationHandler::new(Arc::clone(&state)); - let accesskit_window_id = NodeId(entity.to_bits()); - let handler = WinitActionHandler::default(); - let adapter = Adapter::with_action_handler( + let action_request_handler = WinitActionRequestHandler::new(); + let action_handler = WinitActionHandler::new(Arc::clone(&action_request_handler)); + let deactivation_handler = WinitDeactivationHandler; + + let adapter = Adapter::with_direct_handlers( winit_window, - move || { - accessibility_requested.set(true); - TreeUpdate { - nodes: vec![(accesskit_window_id, root)], - tree: Some(Tree::new(accesskit_window_id)), - focus: accesskit_window_id, - } - }, - Box::new(handler.clone()), + activation_handler, + action_handler, + deactivation_handler, ); + adapters.insert(entity, adapter); - handlers.insert(entity, handler); + handlers.insert(entity, action_request_handler); } fn window_closed( mut adapters: NonSendMut, - mut receivers: ResMut, + mut handlers: ResMut, mut events: EventReader, ) { for WindowClosed { window, .. } in events.read() { adapters.remove(window); - receivers.remove(window); + handlers.remove(window); } } fn poll_receivers( - handlers: Res, + handlers: Res, mut actions: EventWriter, ) { for (_id, handler) in handlers.iter() { @@ -106,7 +178,7 @@ fn should_update_accessibility_nodes( } fn update_accessibility_nodes( - adapters: NonSend, + mut adapters: NonSendMut, focus: Res, primary_window: Query<(Entity, &Window), With>, nodes: Query<( @@ -120,7 +192,7 @@ fn update_accessibility_nodes( let Ok((primary_window_id, primary_window)) = primary_window.get_single() else { return; }; - let Some(adapter) = adapters.get(&primary_window_id) else { + let Some(adapter) = adapters.get_mut(&primary_window_id) else { return; }; if focus.is_changed() || !nodes.is_empty() { @@ -155,7 +227,7 @@ fn update_adapter( queue_node_for_update(entity, parent, &node_entities, &mut window_children); add_children_nodes(children, &node_entities, &mut node); let node_id = NodeId(entity.to_bits()); - let node = node.build(&mut NodeClassSet::lock_global()); + let node = node.build(); to_update.push((node_id, node)); } let mut window_node = NodeBuilder::new(Role::Window); @@ -164,7 +236,7 @@ fn update_adapter( window_node.set_name(title.into_boxed_str()); } window_node.set_children(window_children); - let window_node = window_node.build(&mut NodeClassSet::lock_global()); + let window_node = window_node.build(); let node_id = NodeId(primary_window_id.to_bits()); let window_update = (node_id, window_node); to_update.insert(0, window_update); @@ -214,7 +286,7 @@ pub struct AccessKitPlugin; impl Plugin for AccessKitPlugin { fn build(&self, app: &mut App) { app.init_non_send_resource::() - .init_resource::() + .init_resource::() .add_event::() .add_systems( PostUpdate, diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 60794135e3..8be97fc808 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -12,60 +12,33 @@ //! The app's [runner](bevy_app::App::runner) is set by `WinitPlugin` and handles the `winit` [`EventLoop`]. //! See `winit_runner` for details. -pub mod accessibility; -mod converters; -mod system; -mod winit_config; -pub mod winit_event; -mod winit_windows; +use bevy_window::RawHandleWrapperHolder; +use std::marker::PhantomData; +use winit::event_loop::EventLoop; +#[cfg(target_os = "android")] +pub use winit::platform::android::activity as android_activity; -use std::sync::mpsc::{sync_channel, SyncSender}; - -use approx::relative_eq; use bevy_a11y::AccessibilityRequested; -use bevy_utils::Instant; +use bevy_app::{App, Last, Plugin}; +use bevy_ecs::prelude::*; +#[allow(deprecated)] +use bevy_window::{exit_on_all_closed, Window, WindowCreated}; pub use system::create_windows; -use system::{changed_windows, despawn_windows, CachedWindow}; -use winit::dpi::{LogicalSize, PhysicalSize}; +use system::{changed_windows, despawn_windows}; pub use winit_config::*; pub use winit_event::*; pub use winit_windows::*; -use bevy_app::{App, AppExit, Last, Plugin, PluginsState}; -use bevy_ecs::event::ManualEventReader; -use bevy_ecs::prelude::*; -use bevy_ecs::system::SystemState; -use bevy_input::{ - mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, - touchpad::{TouchpadMagnify, TouchpadRotate}, -}; -use bevy_math::{ivec2, DVec2, Vec2}; -#[cfg(not(target_arch = "wasm32"))] -use bevy_tasks::tick_global_task_pools_on_main_thread; -use bevy_utils::tracing::{error, trace, warn}; -#[allow(deprecated)] -use bevy_window::{ - exit_on_all_closed, ApplicationLifetime, CursorEntered, CursorLeft, CursorMoved, - FileDragAndDrop, Ime, ReceivedCharacter, RequestRedraw, Window, - WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, WindowDestroyed, - WindowFocused, WindowMoved, WindowOccluded, WindowResized, WindowScaleFactorChanged, - WindowThemeChanged, -}; -#[cfg(target_os = "android")] -use bevy_window::{PrimaryWindow, RawHandleWrapper}; +use crate::accessibility::{AccessKitAdapters, AccessKitPlugin, WinitActionRequestHandlers}; +use crate::state::winit_runner; -#[cfg(target_os = "android")] -pub use winit::platform::android::activity as android_activity; - -use winit::event::StartCause; -use winit::{ - event::{self, DeviceEvent, Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopWindowTarget}, -}; - -use crate::accessibility::{AccessKitAdapters, AccessKitPlugin, WinitActionHandlers}; - -use crate::converters::convert_winit_theme; +pub mod accessibility; +mod converters; +mod state; +mod system; +mod winit_config; +pub mod winit_event; +mod winit_windows; /// [`AndroidApp`] provides an interface to query the application state as well as monitor events /// (for example lifecycle and input events). @@ -79,8 +52,11 @@ pub static ANDROID_APP: std::sync::OnceLock = /// This plugin will add systems and resources that sync with the `winit` backend and also /// replace the existing [`App`] runner with one that constructs an [event loop](EventLoop) to /// receive window and input events from the OS. +/// +/// The `T` event type can be used to pass custom events to the `winit`'s loop, and handled as events +/// in systems. #[derive(Default)] -pub struct WinitPlugin { +pub struct WinitPlugin { /// Allows the window (and the event loop) to be created on any thread /// instead of only the main thread. /// @@ -91,18 +67,23 @@ pub struct WinitPlugin { /// Only works on Linux (X11/Wayland) and Windows. /// This field is ignored on other platforms. pub run_on_any_thread: bool, + marker: PhantomData, } -impl Plugin for WinitPlugin { +impl Plugin for WinitPlugin { + fn name(&self) -> &str { + "bevy_winit::WinitPlugin" + } + fn build(&self, app: &mut App) { - let mut event_loop_builder = EventLoopBuilder::::with_user_event(); + let mut event_loop_builder = EventLoop::::with_user_event(); // linux check is needed because x11 might be enabled on other platforms. #[cfg(all(target_os = "linux", feature = "x11"))] { use winit::platform::x11::EventLoopBuilderExtX11; - // This allows a Bevy app to be started and ran outside of the main thread. + // This allows a Bevy app to be started and ran outside the main thread. // A use case for this is to allow external applications to spawn a thread // which runs a Bevy app without requiring the Bevy app to need to reside on // the main thread, which can be problematic. @@ -132,7 +113,7 @@ impl Plugin for WinitPlugin { app.init_non_send_resource::() .init_resource::() .add_event::() - .set_runner(winit_runner) + .set_runner(winit_runner::) .add_systems( Last, ( @@ -150,691 +131,50 @@ impl Plugin for WinitPlugin { .build() .expect("Failed to build event loop"); - // iOS, macOS, and Android don't like it if you create windows before the event loop is - // initialized. - // - // See: - // - https://github.com/rust-windowing/winit/blob/master/README.md#macos - // - https://github.com/rust-windowing/winit/blob/master/README.md#ios - #[cfg(not(any(target_os = "android", target_os = "ios", target_os = "macos")))] - { - // Otherwise, we want to create a window before `bevy_render` initializes the renderer - // so that we have a surface to use as a hint. This improves compatibility with `wgpu` - // backends, especially WASM/WebGL2. - let mut create_window = SystemState::::from_world(app.world_mut()); - create_windows(&event_loop, create_window.get_mut(app.world_mut())); - create_window.apply(app.world_mut()); - } - // `winit`'s windows are bound to the event loop that created them, so the event loop must // be inserted as a resource here to pass it onto the runner. app.insert_non_send_resource(event_loop); } } -trait AppSendEvent { - fn send(&mut self, event: impl Into); -} -impl AppSendEvent for Vec { - fn send(&mut self, event: impl Into) { - self.push(Into::::into(event)); - } -} - -/// Persistent state that is used to run the [`App`] according to the current -/// [`UpdateMode`]. -struct WinitAppRunnerState { - /// Current activity state of the app. - activity_state: UpdateState, - /// Current update mode of the app. - update_mode: UpdateMode, - /// Is `true` if a new [`WindowEvent`] has been received since the last update. - window_event_received: bool, - /// Is `true` if a new [`DeviceEvent`] has been received since the last update. - device_event_received: bool, - /// Is `true` if the app has requested a redraw since the last update. - redraw_requested: bool, - /// Is `true` if enough time has elapsed since `last_update` to run another update. - wait_elapsed: bool, - /// Number of "forced" updates to trigger on application start - startup_forced_updates: u32, -} - -impl WinitAppRunnerState { - fn reset_on_update(&mut self) { - self.window_event_received = false; - self.device_event_received = false; - } -} - -impl Default for WinitAppRunnerState { - fn default() -> Self { - Self { - activity_state: UpdateState::NotYetStarted, - update_mode: UpdateMode::Continuous, - window_event_received: false, - device_event_received: false, - redraw_requested: false, - wait_elapsed: false, - // 3 seems to be enough, 5 is a safe margin - startup_forced_updates: 5, - } - } -} - -#[derive(PartialEq, Eq, Debug)] -enum UpdateState { - NotYetStarted, - Active, - Suspended, - WillSuspend, - WillResume, -} - -impl UpdateState { - #[inline] - fn is_active(&self) -> bool { - match self { - Self::NotYetStarted | Self::Suspended => false, - Self::Active | Self::WillSuspend | Self::WillResume => true, - } - } -} - -/// The parameters of the [`create_windows`] system. -pub type CreateWindowParams<'w, 's, F = ()> = ( - Commands<'w, 's>, - Query<'w, 's, (Entity, &'static mut Window), F>, - EventWriter<'w, WindowCreated>, - NonSendMut<'w, WinitWindows>, - NonSendMut<'w, AccessKitAdapters>, - ResMut<'w, WinitActionHandlers>, - Res<'w, AccessibilityRequested>, -); +/// The default event that can be used to wake the window loop +/// Wakes up the loop if in wait state +#[derive(Debug, Default, Clone, Copy, Event)] +pub struct WakeUp; /// The [`winit::event_loop::EventLoopProxy`] with the specific [`winit::event::Event::UserEvent`] used in the [`winit_runner`]. /// /// The `EventLoopProxy` can be used to request a redraw from outside bevy. /// /// Use `NonSend` to receive this resource. -pub type EventLoopProxy = winit::event_loop::EventLoopProxy; +pub type EventLoopProxy = winit::event_loop::EventLoopProxy; -type UserEvent = RequestRedraw; - -/// The default [`App::runner`] for the [`WinitPlugin`] plugin. -/// -/// Overriding the app's [runner](bevy_app::App::runner) while using `WinitPlugin` will bypass the -/// `EventLoop`. -pub fn winit_runner(mut app: App) -> AppExit { - if app.plugins_state() == PluginsState::Ready { - app.finish(); - app.cleanup(); - } - - let event_loop = app - .world_mut() - .remove_non_send_resource::>() - .unwrap(); - - app.world_mut() - .insert_non_send_resource(event_loop.create_proxy()); - - let mut runner_state = WinitAppRunnerState::default(); - - // Create a channel with a size of 1, since ideally only one exit code will be sent before exiting the app. - let (exit_sender, exit_receiver) = sync_channel(1); - - // prepare structures to access data in the world - let mut redraw_event_reader = ManualEventReader::::default(); - - let mut focused_windows_state: SystemState<(Res, Query<(Entity, &Window)>)> = - SystemState::new(app.world_mut()); - - let mut event_writer_system_state: SystemState<( - EventWriter, - NonSend, - Query<(&mut Window, &mut CachedWindow)>, - NonSend, - )> = SystemState::new(app.world_mut()); - - let mut create_window = - SystemState::>>::from_world(app.world_mut()); - let mut winit_events = Vec::default(); - // set up the event loop - let event_handler = move |event, event_loop: &EventLoopWindowTarget| { - // The event loop is in the process of exiting, so don't deliver any new events - if event_loop.exiting() { - return; - } - - handle_winit_event( - &mut app, - &mut runner_state, - &mut create_window, - &mut event_writer_system_state, - &mut focused_windows_state, - &mut redraw_event_reader, - &mut winit_events, - &exit_sender, - event, - event_loop, - ); - }; - - trace!("starting winit event loop"); - // TODO(clean): the winit docs mention using `spawn` instead of `run` on WASM. - if let Err(err) = event_loop.run(event_handler) { - error!("winit event loop returned an error: {err}"); - } - - // If everything is working correctly then the event loop only exits after it's sent a exit code. - exit_receiver - .try_recv() - .map_err(|err| error!("Failed to receive a app exit code! This is a bug. Reason: {err}")) - .unwrap_or(AppExit::error()) +trait AppSendEvent { + fn send(&mut self, event: impl Into); } -#[allow(clippy::too_many_arguments /* TODO: probs can reduce # of args */)] -fn handle_winit_event( - app: &mut App, - runner_state: &mut WinitAppRunnerState, - create_window: &mut SystemState>>, - event_writer_system_state: &mut SystemState<( - EventWriter, - NonSend, - Query<(&mut Window, &mut CachedWindow)>, - NonSend, - )>, - focused_windows_state: &mut SystemState<(Res, Query<(Entity, &Window)>)>, - redraw_event_reader: &mut ManualEventReader, - winit_events: &mut Vec, - exit_notify: &SyncSender, - event: Event, - event_loop: &EventLoopWindowTarget, -) { - #[cfg(feature = "trace")] - let _span = bevy_utils::tracing::info_span!("winit event_handler").entered(); - - if app.plugins_state() != PluginsState::Cleaned { - if app.plugins_state() != PluginsState::Ready { - #[cfg(not(target_arch = "wasm32"))] - tick_global_task_pools_on_main_thread(); - } else { - app.finish(); - app.cleanup(); - } - runner_state.redraw_requested = true; - } - - // create any new windows - // (even if app did not update, some may have been created by plugin setup) - create_windows(event_loop, create_window.get_mut(app.world_mut())); - create_window.apply(app.world_mut()); - - match event { - Event::AboutToWait => { - if let Some(app_redraw_events) = app.world().get_resource::>() { - if redraw_event_reader.read(app_redraw_events).last().is_some() { - runner_state.redraw_requested = true; - } - } - - let (config, windows) = focused_windows_state.get(app.world()); - let focused = windows.iter().any(|(_, window)| window.focused); - - let mut update_mode = config.update_mode(focused); - let mut should_update = should_update(runner_state, update_mode); - - if runner_state.startup_forced_updates > 0 { - runner_state.startup_forced_updates -= 1; - // Ensure that an update is triggered on the first iterations for app initialization - should_update = true; - } - - if runner_state.activity_state == UpdateState::WillSuspend { - runner_state.activity_state = UpdateState::Suspended; - // Trigger one last update to enter the suspended state - should_update = true; - - #[cfg(target_os = "android")] - { - // Remove the `RawHandleWrapper` from the primary window. - // This will trigger the surface destruction. - let mut query = app - .world_mut() - .query_filtered::>(); - let entity = query.single(&app.world()); - app.world_mut() - .entity_mut(entity) - .remove::(); - } - } - - if runner_state.activity_state == UpdateState::WillResume { - runner_state.activity_state = UpdateState::Active; - // Trigger the update to enter the active state - should_update = true; - // Trigger the next redraw ro refresh the screen immediately - runner_state.redraw_requested = true; - - #[cfg(target_os = "android")] - { - // 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 = app - .world_mut() - .query_filtered::<(Entity, &Window), (With, Without)>(); - if let Ok((entity, window)) = query.get_single(&app.world()) { - let window = window.clone(); - - let ( - .., - mut winit_windows, - mut adapters, - mut handlers, - accessibility_requested, - ) = create_window.get_mut(app.world_mut()); - - let winit_window = winit_windows.create_window( - event_loop, - entity, - &window, - &mut adapters, - &mut handlers, - &accessibility_requested, - ); - - let wrapper = RawHandleWrapper::new(winit_window).unwrap(); - - app.world_mut().entity_mut(entity).insert(wrapper); - } - } - } - - // This is recorded before running app.update(), to run the next cycle after a correct timeout. - // If the cycle takes more than the wait timeout, it will be re-executed immediately. - let begin_frame_time = Instant::now(); - - if should_update { - // Not redrawing, but the timeout elapsed. - run_app_update(runner_state, app, winit_events); - - // Running the app may have changed the WinitSettings resource, so we have to re-extract it. - let (config, windows) = focused_windows_state.get(app.world()); - let focused = windows.iter().any(|(_, window)| window.focused); - - update_mode = config.update_mode(focused); - } - - match update_mode { - UpdateMode::Continuous => { - // per winit's docs on [Window::is_visible](https://docs.rs/winit/latest/winit/window/struct.Window.html#method.is_visible), - // we cannot use the visibility to drive rendering on these platforms - // so we cannot discern whether to beneficially use `Poll` or not? - cfg_if::cfg_if! { - if #[cfg(not(any( - target_arch = "wasm32", - target_os = "android", - target_os = "ios", - all(target_os = "linux", any(feature = "x11", feature = "wayland")) - )))] - { - let winit_windows = app.world().non_send_resource::(); - let visible = winit_windows.windows.iter().any(|(_, w)| { - w.is_visible().unwrap_or(false) - }); - - event_loop.set_control_flow(if visible { - ControlFlow::Wait - } else { - ControlFlow::Poll - }); - } - else { - event_loop.set_control_flow(ControlFlow::Wait); - } - } - - // Trigger the next redraw to refresh the screen immediately if waiting - if let ControlFlow::Wait = event_loop.control_flow() { - runner_state.redraw_requested = true; - } - } - UpdateMode::Reactive { wait } | UpdateMode::ReactiveLowPower { wait } => { - // Set the next timeout, starting from the instant before running app.update() to avoid frame delays - if let Some(next) = begin_frame_time.checked_add(wait) { - if runner_state.wait_elapsed { - event_loop.set_control_flow(ControlFlow::WaitUntil(next)); - } - } - } - } - - if update_mode != runner_state.update_mode { - // Trigger the next redraw since we're changing the update mode - runner_state.redraw_requested = true; - runner_state.update_mode = update_mode; - } - - if runner_state.redraw_requested - && runner_state.activity_state != UpdateState::Suspended - { - let winit_windows = app.world().non_send_resource::(); - for window in winit_windows.windows.values() { - window.request_redraw(); - } - runner_state.redraw_requested = false; - } - } - Event::NewEvents(cause) => { - runner_state.wait_elapsed = match cause { - StartCause::WaitCancelled { - requested_resume: Some(resume), - .. - } => { - // If the resume time is not after now, it means that at least the wait timeout - // has elapsed. - resume <= Instant::now() - } - _ => true, - }; - } - Event::WindowEvent { - event, window_id, .. - } => { - let (mut window_resized, winit_windows, mut windows, access_kit_adapters) = - event_writer_system_state.get_mut(app.world_mut()); - - let Some(window) = winit_windows.get_window_entity(window_id) else { - warn!("Skipped event {event:?} for unknown winit Window Id {window_id:?}"); - return; - }; - - let Ok((mut win, _)) = windows.get_mut(window) else { - warn!("Window {window:?} is missing `Window` component, skipping event {event:?}"); - return; - }; - - // Allow AccessKit to respond to `WindowEvent`s before they reach - // the engine. - if let Some(adapter) = access_kit_adapters.get(&window) { - if let Some(winit_window) = winit_windows.get_window(window) { - adapter.process_event(winit_window, &event); - } - } - - runner_state.window_event_received = true; - - match event { - WindowEvent::Resized(size) => { - react_to_resize(&mut win, size, &mut window_resized, window); - } - WindowEvent::CloseRequested => winit_events.send(WindowCloseRequested { window }), - WindowEvent::KeyboardInput { ref event, .. } => { - if event.state.is_pressed() { - if let Some(char) = &event.text { - let char = char.clone(); - #[allow(deprecated)] - winit_events.send(ReceivedCharacter { window, char }); - } - } - winit_events.send(converters::convert_keyboard_input(event, window)); - } - WindowEvent::CursorMoved { position, .. } => { - let physical_position = DVec2::new(position.x, position.y); - - let last_position = win.physical_cursor_position(); - let delta = last_position.map(|last_pos| { - (physical_position.as_vec2() - last_pos) / win.resolution.scale_factor() - }); - - win.set_physical_cursor_position(Some(physical_position)); - let position = - (physical_position / win.resolution.scale_factor() as f64).as_vec2(); - winit_events.send(CursorMoved { - window, - position, - delta, - }); - } - WindowEvent::CursorEntered { .. } => { - winit_events.send(CursorEntered { window }); - } - WindowEvent::CursorLeft { .. } => { - win.set_physical_cursor_position(None); - winit_events.send(CursorLeft { window }); - } - WindowEvent::MouseInput { state, button, .. } => { - winit_events.send(MouseButtonInput { - button: converters::convert_mouse_button(button), - state: converters::convert_element_state(state), - window, - }); - } - WindowEvent::TouchpadMagnify { delta, .. } => { - winit_events.send(TouchpadMagnify(delta as f32)); - } - WindowEvent::TouchpadRotate { delta, .. } => { - winit_events.send(TouchpadRotate(delta)); - } - WindowEvent::MouseWheel { delta, .. } => match delta { - event::MouseScrollDelta::LineDelta(x, y) => { - winit_events.send(MouseWheel { - unit: MouseScrollUnit::Line, - x, - y, - window, - }); - } - event::MouseScrollDelta::PixelDelta(p) => { - winit_events.send(MouseWheel { - unit: MouseScrollUnit::Pixel, - x: p.x as f32, - y: p.y as f32, - window, - }); - } - }, - WindowEvent::Touch(touch) => { - let location = touch - .location - .to_logical(win.resolution.scale_factor() as f64); - winit_events.send(converters::convert_touch_input(touch, location, window)); - } - WindowEvent::ScaleFactorChanged { - scale_factor, - mut inner_size_writer, - } => { - let prior_factor = win.resolution.scale_factor(); - win.resolution.set_scale_factor(scale_factor as f32); - // Note: this may be different from new_scale_factor if - // `scale_factor_override` is set to Some(thing) - let new_factor = win.resolution.scale_factor(); - - let mut new_inner_size = - PhysicalSize::new(win.physical_width(), win.physical_height()); - let scale_factor_override = win.resolution.scale_factor_override(); - if let Some(forced_factor) = scale_factor_override { - // This window is overriding the OS-suggested DPI, so its physical size - // should be set based on the overriding value. Its logical size already - // incorporates any resize constraints. - let maybe_new_inner_size = LogicalSize::new(win.width(), win.height()) - .to_physical::(forced_factor as f64); - if let Err(err) = inner_size_writer.request_inner_size(new_inner_size) { - warn!("Winit Failed to resize the window: {err}"); - } else { - new_inner_size = maybe_new_inner_size; - } - } - let new_logical_width = new_inner_size.width as f32 / new_factor; - let new_logical_height = new_inner_size.height as f32 / new_factor; - - let width_equal = relative_eq!(win.width(), new_logical_width); - let height_equal = relative_eq!(win.height(), new_logical_height); - win.resolution - .set_physical_resolution(new_inner_size.width, new_inner_size.height); - - winit_events.send(WindowBackendScaleFactorChanged { - window, - scale_factor, - }); - if scale_factor_override.is_none() && !relative_eq!(new_factor, prior_factor) { - winit_events.send(WindowScaleFactorChanged { - window, - scale_factor, - }); - } - - if !width_equal || !height_equal { - winit_events.send(WindowResized { - window, - width: new_logical_width, - height: new_logical_height, - }); - } - } - WindowEvent::Focused(focused) => { - win.focused = focused; - winit_events.send(WindowFocused { window, focused }); - } - WindowEvent::Occluded(occluded) => { - winit_events.send(WindowOccluded { window, occluded }); - } - WindowEvent::DroppedFile(path_buf) => { - winit_events.send(FileDragAndDrop::DroppedFile { window, path_buf }); - } - WindowEvent::HoveredFile(path_buf) => { - winit_events.send(FileDragAndDrop::HoveredFile { window, path_buf }); - } - WindowEvent::HoveredFileCancelled => { - winit_events.send(FileDragAndDrop::HoveredFileCanceled { window }); - } - WindowEvent::Moved(position) => { - let position = ivec2(position.x, position.y); - win.position.set(position); - winit_events.send(WindowMoved { window, position }); - } - WindowEvent::Ime(event) => match event { - event::Ime::Preedit(value, cursor) => { - winit_events.send(Ime::Preedit { - window, - value, - cursor, - }); - } - event::Ime::Commit(value) => { - winit_events.send(Ime::Commit { window, value }); - } - event::Ime::Enabled => { - winit_events.send(Ime::Enabled { window }); - } - event::Ime::Disabled => { - winit_events.send(Ime::Disabled { window }); - } - }, - WindowEvent::ThemeChanged(theme) => { - winit_events.send(WindowThemeChanged { - window, - theme: convert_winit_theme(theme), - }); - } - WindowEvent::Destroyed => { - winit_events.send(WindowDestroyed { window }); - } - WindowEvent::RedrawRequested => { - run_app_update(runner_state, app, winit_events); - } - _ => {} - } - - let mut windows = app.world_mut().query::<(&mut Window, &mut CachedWindow)>(); - if let Ok((window_component, mut cache)) = windows.get_mut(app.world_mut(), window) { - if window_component.is_changed() { - cache.window = window_component.clone(); - } - } - } - Event::DeviceEvent { event, .. } => { - runner_state.device_event_received = true; - if let DeviceEvent::MouseMotion { delta: (x, y) } = event { - let delta = Vec2::new(x as f32, y as f32); - winit_events.send(MouseMotion { delta }); - } - } - Event::Suspended => { - winit_events.send(ApplicationLifetime::Suspended); - // Mark the state as `WillSuspend`. This will let the schedule run one last time - // before actually suspending to let the application react - runner_state.activity_state = UpdateState::WillSuspend; - } - Event::Resumed => { - match runner_state.activity_state { - UpdateState::NotYetStarted => winit_events.send(ApplicationLifetime::Started), - _ => winit_events.send(ApplicationLifetime::Resumed), - } - runner_state.activity_state = UpdateState::WillResume; - } - Event::UserEvent(RequestRedraw) => { - runner_state.redraw_requested = true; - } - _ => (), - } - - if let Some(app_exit) = app.should_exit() { - if let Err(err) = exit_notify.try_send(app_exit) { - error!("Failed to send a app exit notification! This is a bug. Reason: {err}"); - }; - event_loop.exit(); - return; - } - - // We drain events after every received winit event in addition to on app update to ensure - // the work of pushing events into event queues is spread out over time in case the app becomes - // dormant for a long stretch. - forward_winit_events(winit_events, app); -} - -fn should_update(runner_state: &WinitAppRunnerState, update_mode: UpdateMode) -> bool { - let handle_event = match update_mode { - UpdateMode::Continuous | UpdateMode::Reactive { .. } => { - runner_state.wait_elapsed - || runner_state.window_event_received - || runner_state.device_event_received - } - UpdateMode::ReactiveLowPower { .. } => { - runner_state.wait_elapsed || runner_state.window_event_received - } - }; - - handle_event && runner_state.activity_state.is_active() -} - -fn run_app_update( - runner_state: &mut WinitAppRunnerState, - app: &mut App, - winit_events: &mut Vec, -) { - runner_state.reset_on_update(); - - forward_winit_events(winit_events, app); - - if app.plugins_state() == PluginsState::Cleaned { - app.update(); +impl AppSendEvent for Vec { + fn send(&mut self, event: impl Into) { + self.push(Into::::into(event)); } } -fn react_to_resize( - win: &mut Mut<'_, Window>, - size: winit::dpi::PhysicalSize, - window_resized: &mut EventWriter, - window: Entity, -) { - win.resolution - .set_physical_resolution(size.width, size.height); - - window_resized.send(WindowResized { - window, - width: win.width(), - height: win.height(), - }); -} +/// The parameters of the [`create_windows`] system. +pub type CreateWindowParams<'w, 's, F = ()> = ( + Commands<'w, 's>, + Query< + 'w, + 's, + ( + Entity, + &'static mut Window, + Option<&'static RawHandleWrapperHolder>, + ), + F, + >, + EventWriter<'w, WindowCreated>, + NonSendMut<'w, WinitWindows>, + NonSendMut<'w, AccessKitAdapters>, + ResMut<'w, WinitActionRequestHandlers>, + Res<'w, AccessibilityRequested>, +); diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs new file mode 100644 index 0000000000..df0aab42de --- /dev/null +++ b/crates/bevy_winit/src/state.rs @@ -0,0 +1,771 @@ +use approx::relative_eq; +use bevy_app::{App, AppExit, PluginsState}; +use bevy_ecs::change_detection::{DetectChanges, NonSendMut, Res}; +use bevy_ecs::entity::Entity; +use bevy_ecs::event::{EventWriter, ManualEventReader}; +use bevy_ecs::prelude::*; +use bevy_ecs::system::SystemState; +use bevy_ecs::world::FromWorld; +use bevy_input::{ + mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, + touchpad::{TouchpadMagnify, TouchpadRotate}, +}; +use bevy_log::{error, trace, warn}; +use bevy_math::{ivec2, DVec2, Vec2}; +#[cfg(not(target_arch = "wasm32"))] +use bevy_tasks::tick_global_task_pools_on_main_thread; +use bevy_utils::Instant; +use std::marker::PhantomData; +use winit::application::ApplicationHandler; +use winit::dpi::PhysicalSize; +use winit::event; +use winit::event::{DeviceEvent, DeviceId, StartCause, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +use winit::window::WindowId; + +#[allow(deprecated)] +use bevy_window::{ + AppLifecycle, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, ReceivedCharacter, + RequestRedraw, Window, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowDestroyed, + WindowFocused, WindowMoved, WindowOccluded, WindowResized, WindowScaleFactorChanged, + WindowThemeChanged, +}; +#[cfg(target_os = "android")] +use bevy_window::{PrimaryWindow, RawHandleWrapper}; + +use crate::accessibility::AccessKitAdapters; +use crate::system::CachedWindow; +use crate::{ + converters, create_windows, AppSendEvent, CreateWindowParams, UpdateMode, WinitEvent, + WinitSettings, WinitWindows, +}; + +/// Persistent state that is used to run the [`App`] according to the current +/// [`UpdateMode`]. +struct WinitAppRunnerState { + /// The running app. + app: App, + /// Exit value once the loop is finished. + app_exit: Option, + /// Current update mode of the app. + update_mode: UpdateMode, + /// Is `true` if a new [`WindowEvent`] event has been received since the last update. + window_event_received: bool, + /// Is `true` if a new [`DeviceEvent`] event has been received since the last update. + device_event_received: bool, + /// Is `true` if a new [`T`] event has been received since the last update. + user_event_received: bool, + /// Is `true` if the app has requested a redraw since the last update. + redraw_requested: bool, + /// Is `true` if enough time has elapsed since `last_update` to run another update. + wait_elapsed: bool, + /// Number of "forced" updates to trigger on application start + startup_forced_updates: u32, + + /// Current app lifecycle state. + lifecycle: AppLifecycle, + /// The previous app lifecycle state. + previous_lifecycle: AppLifecycle, + /// Winit events to send + winit_events: Vec, + _marker: PhantomData, + + event_writer_system_state: SystemState<( + EventWriter<'static, WindowResized>, + EventWriter<'static, WindowBackendScaleFactorChanged>, + EventWriter<'static, WindowScaleFactorChanged>, + NonSend<'static, WinitWindows>, + Query<'static, 'static, (&'static mut Window, &'static mut CachedWindow)>, + NonSendMut<'static, AccessKitAdapters>, + )>, +} + +impl WinitAppRunnerState { + fn new(mut app: App) -> Self { + app.add_event::(); + + let event_writer_system_state: SystemState<( + EventWriter, + EventWriter, + EventWriter, + NonSend, + Query<(&mut Window, &mut CachedWindow)>, + NonSendMut, + )> = SystemState::new(app.world_mut()); + + Self { + app, + lifecycle: AppLifecycle::Idle, + previous_lifecycle: AppLifecycle::Idle, + app_exit: None, + update_mode: UpdateMode::Continuous, + window_event_received: false, + device_event_received: false, + user_event_received: false, + redraw_requested: false, + wait_elapsed: false, + // 3 seems to be enough, 5 is a safe margin + startup_forced_updates: 5, + winit_events: Vec::new(), + _marker: PhantomData, + event_writer_system_state, + } + } + + fn reset_on_update(&mut self) { + self.window_event_received = false; + self.device_event_received = false; + self.user_event_received = false; + } + + fn world(&self) -> &World { + self.app.world() + } + + fn world_mut(&mut self) -> &mut World { + self.app.world_mut() + } +} + +impl ApplicationHandler for WinitAppRunnerState { + fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { + if event_loop.exiting() { + return; + } + + #[cfg(feature = "trace")] + let _span = bevy_utils::tracing::info_span!("winit event_handler").entered(); + + if self.app.plugins_state() != PluginsState::Cleaned { + if self.app.plugins_state() != PluginsState::Ready { + #[cfg(not(target_arch = "wasm32"))] + tick_global_task_pools_on_main_thread(); + } else { + self.app.finish(); + self.app.cleanup(); + } + self.redraw_requested = true; + } + + self.wait_elapsed = match cause { + StartCause::WaitCancelled { + requested_resume: Some(resume), + .. + } => { + // If the resume time is not after now, it means that at least the wait timeout + // has elapsed. + resume <= Instant::now() + } + _ => true, + }; + } + + fn resumed(&mut self, _event_loop: &ActiveEventLoop) { + // Mark the state as `WillResume`. This will let the schedule run one extra time + // when actually resuming the app + self.lifecycle = AppLifecycle::WillResume; + } + + fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: T) { + self.user_event_received = true; + + self.world_mut().send_event(event); + self.redraw_requested = true; + } + + fn window_event( + &mut self, + _event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + self.window_event_received = true; + + let ( + mut window_resized, + mut window_backend_scale_factor_changed, + mut window_scale_factor_changed, + winit_windows, + mut windows, + mut access_kit_adapters, + ) = self.event_writer_system_state.get_mut(self.app.world_mut()); + + let Some(window) = winit_windows.get_window_entity(window_id) else { + warn!("Skipped event {event:?} for unknown winit Window Id {window_id:?}"); + return; + }; + + let Ok((mut win, _)) = windows.get_mut(window) else { + warn!("Window {window:?} is missing `Window` component, skipping event {event:?}"); + return; + }; + + // Allow AccessKit to respond to `WindowEvent`s before they reach + // the engine. + if let Some(adapter) = access_kit_adapters.get_mut(&window) { + if let Some(winit_window) = winit_windows.get_window(window) { + adapter.process_event(winit_window, &event); + } + } + + match event { + WindowEvent::Resized(size) => { + react_to_resize(window, &mut win, size, &mut window_resized); + } + WindowEvent::ScaleFactorChanged { scale_factor, .. } => { + react_to_scale_factor_change( + window, + &mut win, + scale_factor, + &mut window_backend_scale_factor_changed, + &mut window_scale_factor_changed, + ); + } + WindowEvent::CloseRequested => self.winit_events.send(WindowCloseRequested { window }), + WindowEvent::KeyboardInput { ref event, .. } => { + if event.state.is_pressed() { + if let Some(char) = &event.text { + let char = char.clone(); + #[allow(deprecated)] + self.winit_events.send(ReceivedCharacter { window, char }); + } + } + self.winit_events + .send(converters::convert_keyboard_input(event, window)); + } + WindowEvent::CursorMoved { position, .. } => { + let physical_position = DVec2::new(position.x, position.y); + + let last_position = win.physical_cursor_position(); + let delta = last_position.map(|last_pos| { + (physical_position.as_vec2() - last_pos) / win.resolution.scale_factor() + }); + + win.set_physical_cursor_position(Some(physical_position)); + let position = (physical_position / win.resolution.scale_factor() as f64).as_vec2(); + self.winit_events.send(CursorMoved { + window, + position, + delta, + }); + } + WindowEvent::CursorEntered { .. } => { + self.winit_events.send(CursorEntered { window }); + } + WindowEvent::CursorLeft { .. } => { + win.set_physical_cursor_position(None); + self.winit_events.send(CursorLeft { window }); + } + WindowEvent::MouseInput { state, button, .. } => { + self.winit_events.send(MouseButtonInput { + button: converters::convert_mouse_button(button), + state: converters::convert_element_state(state), + window, + }); + } + WindowEvent::PinchGesture { delta, .. } => { + self.winit_events.send(TouchpadMagnify(delta as f32)); + } + WindowEvent::RotationGesture { delta, .. } => { + self.winit_events.send(TouchpadRotate(delta)); + } + WindowEvent::MouseWheel { delta, .. } => match delta { + event::MouseScrollDelta::LineDelta(x, y) => { + self.winit_events.send(MouseWheel { + unit: MouseScrollUnit::Line, + x, + y, + window, + }); + } + event::MouseScrollDelta::PixelDelta(p) => { + self.winit_events.send(MouseWheel { + unit: MouseScrollUnit::Pixel, + x: p.x as f32, + y: p.y as f32, + window, + }); + } + }, + WindowEvent::Touch(touch) => { + let location = touch + .location + .to_logical(win.resolution.scale_factor() as f64); + self.winit_events + .send(converters::convert_touch_input(touch, location, window)); + } + WindowEvent::Focused(focused) => { + win.focused = focused; + self.winit_events.send(WindowFocused { window, focused }); + } + WindowEvent::Occluded(occluded) => { + self.winit_events.send(WindowOccluded { window, occluded }); + } + WindowEvent::DroppedFile(path_buf) => { + self.winit_events + .send(FileDragAndDrop::DroppedFile { window, path_buf }); + } + WindowEvent::HoveredFile(path_buf) => { + self.winit_events + .send(FileDragAndDrop::HoveredFile { window, path_buf }); + } + WindowEvent::HoveredFileCancelled => { + self.winit_events + .send(FileDragAndDrop::HoveredFileCanceled { window }); + } + WindowEvent::Moved(position) => { + let position = ivec2(position.x, position.y); + win.position.set(position); + self.winit_events.send(WindowMoved { window, position }); + } + WindowEvent::Ime(event) => match event { + event::Ime::Preedit(value, cursor) => { + self.winit_events.send(Ime::Preedit { + window, + value, + cursor, + }); + } + event::Ime::Commit(value) => { + self.winit_events.send(Ime::Commit { window, value }); + } + event::Ime::Enabled => { + self.winit_events.send(Ime::Enabled { window }); + } + event::Ime::Disabled => { + self.winit_events.send(Ime::Disabled { window }); + } + }, + WindowEvent::ThemeChanged(theme) => { + self.winit_events.send(WindowThemeChanged { + window, + theme: converters::convert_winit_theme(theme), + }); + } + WindowEvent::Destroyed => { + self.winit_events.send(WindowDestroyed { window }); + } + _ => {} + } + + let mut windows = self.world_mut().query::<(&mut Window, &mut CachedWindow)>(); + if let Ok((window_component, mut cache)) = windows.get_mut(self.world_mut(), window) { + if window_component.is_changed() { + cache.window = window_component.clone(); + } + } + } + + fn device_event( + &mut self, + _event_loop: &ActiveEventLoop, + _device_id: DeviceId, + event: DeviceEvent, + ) { + self.device_event_received = true; + + if let DeviceEvent::MouseMotion { delta: (x, y) } = event { + let delta = Vec2::new(x as f32, y as f32); + self.winit_events.send(MouseMotion { delta }); + } + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + // create any new windows + // (even if app did not update, some may have been created by plugin setup) + let mut create_window = + SystemState::>>::from_world(self.world_mut()); + create_windows(event_loop, create_window.get_mut(self.world_mut())); + create_window.apply(self.world_mut()); + + let mut redraw_event_reader = ManualEventReader::::default(); + + let mut focused_windows_state: SystemState<(Res, Query<(Entity, &Window)>)> = + SystemState::new(self.world_mut()); + + if let Some(app_redraw_events) = self.world().get_resource::>() { + if redraw_event_reader.read(app_redraw_events).last().is_some() { + self.redraw_requested = true; + } + } + + let (config, windows) = focused_windows_state.get(self.world()); + let focused = windows.iter().any(|(_, window)| window.focused); + + let mut update_mode = config.update_mode(focused); + let mut should_update = self.should_update(update_mode); + + if self.startup_forced_updates > 0 { + self.startup_forced_updates -= 1; + // Ensure that an update is triggered on the first iterations for app initialization + should_update = true; + } + + if self.lifecycle == AppLifecycle::WillSuspend { + self.lifecycle = AppLifecycle::Suspended; + // Trigger one last update to enter the suspended state + should_update = true; + + #[cfg(target_os = "android")] + { + // Remove the `RawHandleWrapper` from the primary window. + // This will trigger the surface destruction. + let mut query = self + .world_mut() + .query_filtered::>(); + let entity = query.single(&self.world()); + self.world_mut() + .entity_mut(entity) + .remove::(); + } + } + + if self.lifecycle == AppLifecycle::WillResume { + self.lifecycle = AppLifecycle::Running; + // Trigger the update to enter the running state + should_update = true; + // Trigger the next redraw to refresh the screen immediately + self.redraw_requested = true; + + #[cfg(target_os = "android")] + { + // 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, Without)>(); + if let Ok((entity, window)) = query.get_single(&self.world()) { + let window = window.clone(); + + let mut create_window = + SystemState::::from_world(self.world_mut()); + + let ( + .., + mut winit_windows, + mut adapters, + mut handlers, + accessibility_requested, + ) = create_window.get_mut(self.world_mut()); + + let winit_window = winit_windows.create_window( + event_loop, + entity, + &window, + &mut adapters, + &mut handlers, + &accessibility_requested, + ); + + let wrapper = RawHandleWrapper::new(winit_window).unwrap(); + + self.world_mut().entity_mut(entity).insert(wrapper); + } + } + } + + // Notifies a lifecycle change + if self.lifecycle != self.previous_lifecycle { + self.previous_lifecycle = self.lifecycle; + self.winit_events.send(self.lifecycle); + } + + // This is recorded before running app.update(), to run the next cycle after a correct timeout. + // If the cycle takes more than the wait timeout, it will be re-executed immediately. + let begin_frame_time = Instant::now(); + + if should_update { + // Not redrawing, but the timeout elapsed. + self.run_app_update(); + + // Running the app may have changed the WinitSettings resource, so we have to re-extract it. + let (config, windows) = focused_windows_state.get(self.world()); + let focused = windows.iter().any(|(_, window)| window.focused); + + update_mode = config.update_mode(focused); + } + + // The update mode could have been changed, so we need to redraw and force an update + if update_mode != self.update_mode { + // Trigger the next redraw since we're changing the update mode + self.redraw_requested = true; + // Consider the wait as elapsed since it could have been cancelled by a user event + self.wait_elapsed = true; + + self.update_mode = update_mode; + } + + match update_mode { + UpdateMode::Continuous => { + // per winit's docs on [Window::is_visible](https://docs.rs/winit/latest/winit/window/struct.Window.html#method.is_visible), + // we cannot use the visibility to drive rendering on these platforms + // so we cannot discern whether to beneficially use `Poll` or not? + cfg_if::cfg_if! { + if #[cfg(not(any( + target_arch = "wasm32", + target_os = "android", + target_os = "ios", + all(target_os = "linux", any(feature = "x11", feature = "wayland")) + )))] + { + let winit_windows = self.world().non_send_resource::(); + let visible = winit_windows.windows.iter().any(|(_, w)| { + w.is_visible().unwrap_or(false) + }); + + event_loop.set_control_flow(if visible { + ControlFlow::Wait + } else { + ControlFlow::Poll + }); + } + else { + event_loop.set_control_flow(ControlFlow::Wait); + } + } + + // Trigger the next redraw to refresh the screen immediately if waiting + if let ControlFlow::Wait = event_loop.control_flow() { + self.redraw_requested = true; + } + } + UpdateMode::Reactive { wait, .. } => { + // Set the next timeout, starting from the instant before running app.update() to avoid frame delays + if let Some(next) = begin_frame_time.checked_add(wait) { + if self.wait_elapsed { + event_loop.set_control_flow(ControlFlow::WaitUntil(next)); + } + } + } + } + + if self.redraw_requested && self.lifecycle != AppLifecycle::Suspended { + let winit_windows = self.world().non_send_resource::(); + for window in winit_windows.windows.values() { + window.request_redraw(); + } + self.redraw_requested = false; + } + + if let Some(app_exit) = self.app.should_exit() { + self.app_exit = Some(app_exit); + + event_loop.exit(); + } + } + + fn suspended(&mut self, _event_loop: &ActiveEventLoop) { + // Mark the state as `WillSuspend`. This will let the schedule run one last time + // before actually suspending to let the application react + self.lifecycle = AppLifecycle::WillSuspend; + } + + fn exiting(&mut self, _event_loop: &ActiveEventLoop) { + let world = self.world_mut(); + world.clear_all(); + } +} + +impl WinitAppRunnerState { + fn should_update(&self, update_mode: UpdateMode) -> bool { + let handle_event = match update_mode { + UpdateMode::Continuous => { + self.wait_elapsed + || self.user_event_received + || self.window_event_received + || self.device_event_received + } + UpdateMode::Reactive { + react_to_device_events, + react_to_user_events, + react_to_window_events, + .. + } => { + self.wait_elapsed + || (react_to_device_events && self.device_event_received) + || (react_to_user_events && self.user_event_received) + || (react_to_window_events && self.window_event_received) + } + }; + + handle_event && self.lifecycle.is_active() + } + + fn run_app_update(&mut self) { + self.reset_on_update(); + + self.forward_winit_events(); + + if self.app.plugins_state() == PluginsState::Cleaned { + self.app.update(); + } + } + + fn forward_winit_events(&mut self) { + let buffered_events = self.winit_events.drain(..).collect::>(); + + if buffered_events.is_empty() { + return; + } + + let world = self.world_mut(); + + for winit_event in buffered_events.iter() { + match winit_event.clone() { + WinitEvent::AppLifecycle(e) => { + world.send_event(e); + } + WinitEvent::CursorEntered(e) => { + world.send_event(e); + } + WinitEvent::CursorLeft(e) => { + world.send_event(e); + } + WinitEvent::CursorMoved(e) => { + world.send_event(e); + } + WinitEvent::FileDragAndDrop(e) => { + world.send_event(e); + } + WinitEvent::Ime(e) => { + world.send_event(e); + } + WinitEvent::ReceivedCharacter(e) => { + world.send_event(e); + } + WinitEvent::RequestRedraw(e) => { + world.send_event(e); + } + WinitEvent::WindowBackendScaleFactorChanged(e) => { + world.send_event(e); + } + WinitEvent::WindowCloseRequested(e) => { + world.send_event(e); + } + WinitEvent::WindowCreated(e) => { + world.send_event(e); + } + WinitEvent::WindowDestroyed(e) => { + world.send_event(e); + } + WinitEvent::WindowFocused(e) => { + world.send_event(e); + } + WinitEvent::WindowMoved(e) => { + world.send_event(e); + } + WinitEvent::WindowOccluded(e) => { + world.send_event(e); + } + WinitEvent::WindowResized(e) => { + world.send_event(e); + } + WinitEvent::WindowScaleFactorChanged(e) => { + world.send_event(e); + } + WinitEvent::WindowThemeChanged(e) => { + world.send_event(e); + } + WinitEvent::MouseButtonInput(e) => { + world.send_event(e); + } + WinitEvent::MouseMotion(e) => { + world.send_event(e); + } + WinitEvent::MouseWheel(e) => { + world.send_event(e); + } + WinitEvent::TouchpadMagnify(e) => { + world.send_event(e); + } + WinitEvent::TouchpadRotate(e) => { + world.send_event(e); + } + WinitEvent::TouchInput(e) => { + world.send_event(e); + } + WinitEvent::KeyboardInput(e) => { + world.send_event(e); + } + } + } + + world + .resource_mut::>() + .send_batch(buffered_events); + } +} + +/// The default [`App::runner`] for the [`WinitPlugin`] plugin. +/// +/// Overriding the app's [runner](bevy_app::App::runner) while using `WinitPlugin` will bypass the +/// `EventLoop`. +pub fn winit_runner(mut app: App) -> AppExit { + if app.plugins_state() == PluginsState::Ready { + app.finish(); + app.cleanup(); + } + + let event_loop = app + .world_mut() + .remove_non_send_resource::>() + .unwrap(); + + app.world_mut() + .insert_non_send_resource(event_loop.create_proxy()); + + let mut runner_state = WinitAppRunnerState::new(app); + + trace!("starting winit event loop"); + // TODO(clean): the winit docs mention using `spawn` instead of `run` on WASM. + if let Err(err) = event_loop.run_app(&mut runner_state) { + error!("winit event loop returned an error: {err}"); + } + + // If everything is working correctly then the event loop only exits after it's sent an exit code. + runner_state.app_exit.unwrap_or_else(|| { + error!("Failed to receive a app exit code! This is a bug"); + AppExit::error() + }) +} + +pub(crate) fn react_to_resize( + window_entity: Entity, + window: &mut Mut<'_, Window>, + size: PhysicalSize, + window_resized: &mut EventWriter, +) { + window + .resolution + .set_physical_resolution(size.width, size.height); + + window_resized.send(WindowResized { + window: window_entity, + width: window.width(), + height: window.height(), + }); +} + +pub(crate) fn react_to_scale_factor_change( + window_entity: Entity, + window: &mut Mut<'_, Window>, + scale_factor: f64, + window_backend_scale_factor_changed: &mut EventWriter, + window_scale_factor_changed: &mut EventWriter, +) { + window.resolution.set_scale_factor(scale_factor as f32); + + window_backend_scale_factor_changed.send(WindowBackendScaleFactorChanged { + window: window_entity, + scale_factor, + }); + + let prior_factor = window.resolution.scale_factor(); + let scale_factor_override = window.resolution.scale_factor_override(); + + if scale_factor_override.is_none() && !relative_eq!(scale_factor as f32, prior_factor) { + window_scale_factor_changed.send(WindowScaleFactorChanged { + window: window_entity, + scale_factor, + }); + } +} diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index d871c0fb6e..57a9de8978 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -12,15 +12,14 @@ use bevy_window::{ WindowMode, WindowResized, }; -use winit::{ - dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, - event_loop::EventLoopWindowTarget, -}; +use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; +use winit::event_loop::ActiveEventLoop; use bevy_ecs::query::With; #[cfg(target_arch = "wasm32")] use winit::platform::web::WindowExtWebSys; +use crate::state::react_to_resize; use crate::{ converters::{ self, convert_enabled_buttons, convert_window_level, convert_window_theme, @@ -36,7 +35,7 @@ use crate::{ /// default values. #[allow(clippy::too_many_arguments)] pub fn create_windows( - event_loop: &EventLoopWindowTarget, + event_loop: &ActiveEventLoop, ( mut commands, mut created_windows, @@ -47,7 +46,7 @@ pub fn create_windows( accessibility_requested, ): SystemParamItem>, ) { - for (entity, mut window) in &mut created_windows { + for (entity, mut window, handle_holder) in &mut created_windows { if winit_windows.get_window(entity).is_some() { continue; } @@ -80,7 +79,11 @@ pub fn create_windows( }); if let Ok(handle_wrapper) = RawHandleWrapper::new(winit_window) { - commands.entity(entity).insert(handle_wrapper); + let mut entity = commands.entity(entity); + entity.insert(handle_wrapper.clone()); + if let Some(handle_holder) = handle_holder { + *handle_holder.0.lock().unwrap() = Some(handle_wrapper); + } } #[cfg(target_arch = "wasm32")] @@ -181,13 +184,46 @@ pub(crate) fn changed_windows( } } } + if window.resolution != cache.window.resolution { - let physical_size = PhysicalSize::new( - window.resolution.physical_width(), - window.resolution.physical_height(), + let mut physical_size = winit_window.inner_size(); + + let cached_physical_size = PhysicalSize::new( + cache.window.physical_width(), + cache.window.physical_height(), ); - if let Some(size_now) = winit_window.request_inner_size(physical_size) { - crate::react_to_resize(&mut window, size_now, &mut window_resized, entity); + + let base_scale_factor = window.resolution.base_scale_factor(); + + // 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(); + + // 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() { + physical_size.to_logical::(cached_factor as f64) + } else { + physical_size.to_logical::(base_scale_factor as f64) + }; + + // Scale factor changed, updating physical and logical size + if let Some(forced_factor) = window.resolution.scale_factor_override() { + // This window is overriding the OS-suggested DPI, so its physical size + // should be set based on the overriding value. Its logical size already + // incorporates any resize constraints. + physical_size = logical_size.to_physical::(forced_factor as f64); + } else { + physical_size = logical_size.to_physical::(base_scale_factor as f64); + } + } + + if physical_size != cached_physical_size { + if let Some(new_physical_size) = winit_window.request_inner_size(physical_size) { + react_to_resize(entity, &mut window, new_physical_size, &mut window_resized); + } } } @@ -202,7 +238,7 @@ pub(crate) fn changed_windows( } if window.cursor.icon != cache.window.cursor.icon { - winit_window.set_cursor_icon(converters::convert_cursor_icon(window.cursor.icon)); + winit_window.set_cursor(converters::convert_cursor_icon(window.cursor.icon)); } if window.cursor.grab_mode != cache.window.cursor.grab_mode { diff --git a/crates/bevy_winit/src/winit_config.rs b/crates/bevy_winit/src/winit_config.rs index 2e77de82aa..1043840866 100644 --- a/crates/bevy_winit/src/winit_config.rs +++ b/crates/bevy_winit/src/winit_config.rs @@ -18,9 +18,7 @@ impl WinitSettings { pub fn game() -> Self { WinitSettings { focused_mode: UpdateMode::Continuous, - unfocused_mode: UpdateMode::ReactiveLowPower { - wait: Duration::from_secs_f64(1.0 / 60.0), // 60Hz - }, + unfocused_mode: UpdateMode::reactive_low_power(Duration::from_secs_f64(1.0 / 60.0)), // 60Hz, } } @@ -32,12 +30,8 @@ impl WinitSettings { /// Use the [`EventLoopProxy`](crate::EventLoopProxy) to request a redraw from outside bevy. pub fn desktop_app() -> Self { WinitSettings { - focused_mode: UpdateMode::Reactive { - wait: Duration::from_secs(5), - }, - unfocused_mode: UpdateMode::ReactiveLowPower { - wait: Duration::from_secs(60), - }, + focused_mode: UpdateMode::reactive(Duration::from_secs(5)), + unfocused_mode: UpdateMode::reactive_low_power(Duration::from_secs(60)), } } @@ -72,7 +66,7 @@ pub enum UpdateMode { /// [`AppExit`](bevy_app::AppExit) event appears: /// - `wait` time has elapsed since the previous update /// - a redraw has been requested by [`RequestRedraw`](bevy_window::RequestRedraw) - /// - new [window](`winit::event::WindowEvent`) or [raw input](`winit::event::DeviceEvent`) + /// - new [window](`winit::event::WindowEvent`), [raw input](`winit::event::DeviceEvent`), or custom /// events have appeared /// - a redraw has been requested with the [`EventLoopProxy`](crate::EventLoopProxy) Reactive { @@ -81,23 +75,38 @@ pub enum UpdateMode { /// **Note:** This has no upper limit. /// The [`App`](bevy_app::App) will wait indefinitely if you set this to [`Duration::MAX`]. wait: Duration, - }, - /// The [`App`](bevy_app::App) will update in response to the following, until an - /// [`AppExit`](bevy_app::AppExit) event appears: - /// - `wait` time has elapsed since the previous update - /// - a redraw has been requested by [`RequestRedraw`](bevy_window::RequestRedraw) - /// - new [window events](`winit::event::WindowEvent`) have appeared - /// - a redraw has been requested with the [`EventLoopProxy`](crate::EventLoopProxy) - /// - /// **Note:** Unlike [`Reactive`](`UpdateMode::Reactive`), this mode will ignore events that - /// don't come from interacting with a window, like [`MouseMotion`](winit::event::DeviceEvent::MouseMotion). - /// Use this mode if, for example, you only want your app to update when the mouse cursor is - /// moving over a window, not just moving in general. This can greatly reduce power consumption. - ReactiveLowPower { - /// The approximate time from the start of one update to the next. - /// - /// **Note:** This has no upper limit. - /// The [`App`](bevy_app::App) will wait indefinitely if you set this to [`Duration::MAX`]. - wait: Duration, + /// Reacts to device events, that will wake up the loop if it's in a wait wtate + react_to_device_events: bool, + /// Reacts to user events, that will wake up the loop if it's in a wait wtate + react_to_user_events: bool, + /// Reacts to window events, that will wake up the loop if it's in a wait wtate + react_to_window_events: bool, }, } + +impl UpdateMode { + /// Reactive mode, will update the app for any kind of event + pub fn reactive(wait: Duration) -> Self { + Self::Reactive { + wait, + react_to_device_events: true, + react_to_user_events: true, + react_to_window_events: true, + } + } + + /// Low power mode + /// + /// Unlike [`Reactive`](`UpdateMode::reactive()`), this will ignore events that + /// don't come from interacting with a window, like [`MouseMotion`](winit::event::DeviceEvent::MouseMotion). + /// Use this if, for example, you only want your app to update when the mouse cursor is + /// moving over a window, not just moving in general. This can greatly reduce power consumption. + pub fn reactive_low_power(wait: Duration) -> Self { + Self::Reactive { + wait, + react_to_device_events: false, + react_to_user_events: true, + react_to_window_events: true, + } + } +} diff --git a/crates/bevy_winit/src/winit_event.rs b/crates/bevy_winit/src/winit_event.rs index a314ea2896..92d3c775a8 100644 --- a/crates/bevy_winit/src/winit_event.rs +++ b/crates/bevy_winit/src/winit_event.rs @@ -1,7 +1,6 @@ #![allow(deprecated)] #![allow(missing_docs)] -use bevy_app::App; use bevy_ecs::prelude::*; use bevy_input::keyboard::KeyboardInput; use bevy_input::touch::TouchInput; @@ -13,9 +12,9 @@ use bevy_reflect::Reflect; #[cfg(feature = "serialize")] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use bevy_window::{ - ApplicationLifetime, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, - ReceivedCharacter, RequestRedraw, WindowBackendScaleFactorChanged, WindowCloseRequested, - WindowCreated, WindowDestroyed, WindowFocused, WindowMoved, WindowOccluded, WindowResized, + AppLifecycle, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, ReceivedCharacter, + RequestRedraw, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, + WindowDestroyed, WindowFocused, WindowMoved, WindowOccluded, WindowResized, WindowScaleFactorChanged, WindowThemeChanged, }; @@ -33,7 +32,7 @@ use bevy_window::{ reflect(Serialize, Deserialize) )] pub enum WinitEvent { - ApplicationLifetime(ApplicationLifetime), + AppLifecycle(AppLifecycle), CursorEntered(CursorEntered), CursorLeft(CursorLeft), CursorMoved(CursorMoved), @@ -64,9 +63,9 @@ pub enum WinitEvent { KeyboardInput(KeyboardInput), } -impl From for WinitEvent { - fn from(e: ApplicationLifetime) -> Self { - Self::ApplicationLifetime(e) +impl From for WinitEvent { + fn from(e: AppLifecycle) -> Self { + Self::AppLifecycle(e) } } impl From for WinitEvent { @@ -189,92 +188,3 @@ impl From for WinitEvent { Self::KeyboardInput(e) } } - -/// Forwards buffered [`WinitEvent`] events to the app. -pub(crate) fn forward_winit_events(buffered_events: &mut Vec, app: &mut App) { - if buffered_events.is_empty() { - return; - } - for winit_event in buffered_events.iter() { - match winit_event.clone() { - WinitEvent::ApplicationLifetime(e) => { - app.world_mut().send_event(e); - } - WinitEvent::CursorEntered(e) => { - app.world_mut().send_event(e); - } - WinitEvent::CursorLeft(e) => { - app.world_mut().send_event(e); - } - WinitEvent::CursorMoved(e) => { - app.world_mut().send_event(e); - } - WinitEvent::FileDragAndDrop(e) => { - app.world_mut().send_event(e); - } - WinitEvent::Ime(e) => { - app.world_mut().send_event(e); - } - WinitEvent::ReceivedCharacter(e) => { - app.world_mut().send_event(e); - } - WinitEvent::RequestRedraw(e) => { - app.world_mut().send_event(e); - } - WinitEvent::WindowBackendScaleFactorChanged(e) => { - app.world_mut().send_event(e); - } - WinitEvent::WindowCloseRequested(e) => { - app.world_mut().send_event(e); - } - WinitEvent::WindowCreated(e) => { - app.world_mut().send_event(e); - } - WinitEvent::WindowDestroyed(e) => { - app.world_mut().send_event(e); - } - WinitEvent::WindowFocused(e) => { - app.world_mut().send_event(e); - } - WinitEvent::WindowMoved(e) => { - app.world_mut().send_event(e); - } - WinitEvent::WindowOccluded(e) => { - app.world_mut().send_event(e); - } - WinitEvent::WindowResized(e) => { - app.world_mut().send_event(e); - } - WinitEvent::WindowScaleFactorChanged(e) => { - app.world_mut().send_event(e); - } - WinitEvent::WindowThemeChanged(e) => { - app.world_mut().send_event(e); - } - WinitEvent::MouseButtonInput(e) => { - app.world_mut().send_event(e); - } - WinitEvent::MouseMotion(e) => { - app.world_mut().send_event(e); - } - WinitEvent::MouseWheel(e) => { - app.world_mut().send_event(e); - } - WinitEvent::TouchpadMagnify(e) => { - app.world_mut().send_event(e); - } - WinitEvent::TouchpadRotate(e) => { - app.world_mut().send_event(e); - } - WinitEvent::TouchInput(e) => { - app.world_mut().send_event(e); - } - WinitEvent::KeyboardInput(e) => { - app.world_mut().send_event(e); - } - } - } - app.world_mut() - .resource_mut::>() - .send_batch(buffered_events.drain(..)); -} diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index ecea140f73..582c2a1efb 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -9,11 +9,15 @@ use bevy_window::{ use winit::{ dpi::{LogicalSize, PhysicalPosition}, - monitor::MonitorHandle, + event_loop::ActiveEventLoop, + monitor::{MonitorHandle, VideoModeHandle}, + window::{CursorGrabMode as WinitCursorGrabMode, Fullscreen, Window as WinitWindow, WindowId}, }; use crate::{ - accessibility::{prepare_accessibility_for_window, AccessKitAdapters, WinitActionHandlers}, + accessibility::{ + prepare_accessibility_for_window, AccessKitAdapters, WinitActionRequestHandlers, + }, converters::{convert_enabled_buttons, convert_window_level, convert_window_theme}, }; @@ -22,11 +26,11 @@ use crate::{ #[derive(Debug, Default)] pub struct WinitWindows { /// Stores [`winit`] windows by window identifier. - pub windows: HashMap>, + pub windows: HashMap>, /// Maps entities to `winit` window identifiers. - pub entity_to_winit: EntityHashMap, + pub entity_to_winit: EntityHashMap, /// Maps `winit` window identifiers to entities. - pub winit_to_entity: HashMap, + pub winit_to_entity: HashMap, // Many `winit` window functions (e.g. `set_window_icon`) can only be called on the main thread. // If they're called on other threads, the program might hang. This marker indicates that this // type is not thread-safe and will be `!Send` and `!Sync`. @@ -37,23 +41,22 @@ impl WinitWindows { /// Creates a `winit` window and associates it with our entity. pub fn create_window( &mut self, - event_loop: &winit::event_loop::EventLoopWindowTarget, + event_loop: &ActiveEventLoop, entity: Entity, window: &Window, adapters: &mut AccessKitAdapters, - handlers: &mut WinitActionHandlers, + handlers: &mut WinitActionRequestHandlers, accessibility_requested: &AccessibilityRequested, - ) -> &WindowWrapper { - let mut winit_window_builder = winit::window::WindowBuilder::new(); + ) -> &WindowWrapper { + let mut winit_window_attributes = WinitWindow::default_attributes(); // Due to a UIA limitation, winit windows need to be invisible for the // AccessKit adapter is initialized. - winit_window_builder = winit_window_builder.with_visible(false); + winit_window_attributes = winit_window_attributes.with_visible(false); - winit_window_builder = match window.mode { - WindowMode::BorderlessFullscreen => winit_window_builder.with_fullscreen(Some( - winit::window::Fullscreen::Borderless(event_loop.primary_monitor()), - )), + winit_window_attributes = match window.mode { + WindowMode::BorderlessFullscreen => winit_window_attributes + .with_fullscreen(Some(Fullscreen::Borderless(event_loop.primary_monitor()))), mode @ (WindowMode::Fullscreen | WindowMode::SizedFullscreen) => { if let Some(primary_monitor) = event_loop.primary_monitor() { let videomode = match mode { @@ -66,11 +69,10 @@ impl WinitWindows { _ => unreachable!(), }; - winit_window_builder - .with_fullscreen(Some(winit::window::Fullscreen::Exclusive(videomode))) + winit_window_attributes.with_fullscreen(Some(Fullscreen::Exclusive(videomode))) } else { warn!("Could not determine primary monitor, ignoring exclusive fullscreen request for window {:?}", window.title); - winit_window_builder + winit_window_attributes } } WindowMode::Windowed => { @@ -81,19 +83,20 @@ impl WinitWindows { event_loop.primary_monitor(), None, ) { - winit_window_builder = winit_window_builder.with_position(position); + winit_window_attributes = winit_window_attributes.with_position(position); } let logical_size = LogicalSize::new(window.width(), window.height()); if let Some(sf) = window.resolution.scale_factor_override() { - winit_window_builder.with_inner_size(logical_size.to_physical::(sf.into())) + winit_window_attributes + .with_inner_size(logical_size.to_physical::(sf.into())) } else { - winit_window_builder.with_inner_size(logical_size) + winit_window_attributes.with_inner_size(logical_size) } } }; - winit_window_builder = winit_window_builder + winit_window_attributes = winit_window_attributes .with_window_level(convert_window_level(window.window_level)) .with_theme(window.window_theme.map(convert_window_theme)) .with_resizable(window.resizable) @@ -104,8 +107,9 @@ impl WinitWindows { #[cfg(target_os = "windows")] { - use winit::platform::windows::WindowBuilderExtWindows; - winit_window_builder = winit_window_builder.with_skip_taskbar(window.skip_taskbar); + use winit::platform::windows::WindowAttributesExtWindows; + winit_window_attributes = + winit_window_attributes.with_skip_taskbar(window.skip_taskbar); } #[cfg(any( @@ -128,11 +132,12 @@ impl WinitWindows { ) ))] { - winit_window_builder = winit::platform::wayland::WindowBuilderExtWayland::with_name( - winit_window_builder, - name.clone(), - "", - ); + winit_window_attributes = + winit::platform::wayland::WindowAttributesExtWayland::with_name( + winit_window_attributes, + name.clone(), + "", + ); } #[cfg(all( @@ -146,17 +151,17 @@ impl WinitWindows { ) ))] { - winit_window_builder = winit::platform::x11::WindowBuilderExtX11::with_name( - winit_window_builder, + winit_window_attributes = winit::platform::x11::WindowAttributesExtX11::with_name( + winit_window_attributes, name.clone(), "", ); } #[cfg(target_os = "windows")] { - winit_window_builder = - winit::platform::windows::WindowBuilderExtWindows::with_class_name( - winit_window_builder, + winit_window_attributes = + winit::platform::windows::WindowAttributesExtWindows::with_class_name( + winit_window_attributes, name.clone(), ); } @@ -172,22 +177,22 @@ impl WinitWindows { height: constraints.max_height, }; - let winit_window_builder = + let winit_window_attributes = if constraints.max_width.is_finite() && constraints.max_height.is_finite() { - winit_window_builder + winit_window_attributes .with_min_inner_size(min_inner_size) .with_max_inner_size(max_inner_size) } else { - winit_window_builder.with_min_inner_size(min_inner_size) + winit_window_attributes.with_min_inner_size(min_inner_size) }; #[allow(unused_mut)] - let mut winit_window_builder = winit_window_builder.with_title(window.title.as_str()); + let mut winit_window_attributes = winit_window_attributes.with_title(window.title.as_str()); #[cfg(target_arch = "wasm32")] { use wasm_bindgen::JsCast; - use winit::platform::web::WindowBuilderExtWebSys; + use winit::platform::web::WindowAttributesExtWebSys; if let Some(selector) = &window.canvas { let window = web_sys::window().unwrap(); @@ -197,18 +202,18 @@ impl WinitWindows { .expect("Cannot query for canvas element."); if let Some(canvas) = canvas { let canvas = canvas.dyn_into::().ok(); - winit_window_builder = winit_window_builder.with_canvas(canvas); + winit_window_attributes = winit_window_attributes.with_canvas(canvas); } else { panic!("Cannot find element: {}.", selector); } } - winit_window_builder = - winit_window_builder.with_prevent_default(window.prevent_default_event_handling); - winit_window_builder = winit_window_builder.with_append(true); + winit_window_attributes = + winit_window_attributes.with_prevent_default(window.prevent_default_event_handling); + winit_window_attributes = winit_window_attributes.with_append(true); } - let winit_window = winit_window_builder.build(event_loop).unwrap(); + let winit_window = event_loop.create_window(winit_window_attributes).unwrap(); let name = window.title.clone(); prepare_accessibility_for_window( &winit_window, @@ -247,7 +252,7 @@ impl WinitWindows { } /// Get the winit window that is associated with our entity. - pub fn get_window(&self, entity: Entity) -> Option<&WindowWrapper> { + pub fn get_window(&self, entity: Entity) -> Option<&WindowWrapper> { self.entity_to_winit .get(&entity) .and_then(|winit_id| self.windows.get(winit_id)) @@ -256,17 +261,14 @@ impl WinitWindows { /// Get the entity associated with the winit window id. /// /// This is mostly just an intermediary step between us and winit. - pub fn get_window_entity(&self, winit_id: winit::window::WindowId) -> Option { + pub fn get_window_entity(&self, winit_id: WindowId) -> Option { self.winit_to_entity.get(&winit_id).cloned() } /// Remove a window from winit. /// /// This should mostly just be called when the window is closing. - pub fn remove_window( - &mut self, - entity: Entity, - ) -> Option> { + pub fn remove_window(&mut self, entity: Entity) -> Option> { let winit_id = self.entity_to_winit.remove(&entity)?; self.winit_to_entity.remove(&winit_id); self.windows.remove(&winit_id) @@ -276,11 +278,7 @@ impl WinitWindows { /// Gets the "best" video mode which fits the given dimensions. /// /// The heuristic for "best" prioritizes width, height, and refresh rate in that order. -pub fn get_fitting_videomode( - monitor: &MonitorHandle, - width: u32, - height: u32, -) -> winit::monitor::VideoMode { +pub fn get_fitting_videomode(monitor: &MonitorHandle, width: u32, height: u32) -> VideoModeHandle { let mut modes = monitor.video_modes().collect::>(); fn abs_diff(a: u32, b: u32) -> u32 { @@ -308,10 +306,10 @@ pub fn get_fitting_videomode( modes.first().unwrap().clone() } -/// Gets the "best" videomode from a monitor. +/// Gets the "best" video-mode handle from a monitor. /// /// The heuristic for "best" prioritizes width, height, and refresh rate in that order. -pub fn get_best_videomode(monitor: &MonitorHandle) -> winit::monitor::VideoMode { +pub fn get_best_videomode(monitor: &MonitorHandle) -> VideoModeHandle { let mut modes = monitor.video_modes().collect::>(); modes.sort_by(|a, b| { use std::cmp::Ordering::*; @@ -329,15 +327,15 @@ pub fn get_best_videomode(monitor: &MonitorHandle) -> winit::monitor::VideoMode modes.first().unwrap().clone() } -pub(crate) fn attempt_grab(winit_window: &winit::window::Window, grab_mode: CursorGrabMode) { +pub(crate) fn attempt_grab(winit_window: &WinitWindow, grab_mode: CursorGrabMode) { let grab_result = match grab_mode { - CursorGrabMode::None => winit_window.set_cursor_grab(winit::window::CursorGrabMode::None), + CursorGrabMode::None => winit_window.set_cursor_grab(WinitCursorGrabMode::None), CursorGrabMode::Confined => winit_window - .set_cursor_grab(winit::window::CursorGrabMode::Confined) - .or_else(|_e| winit_window.set_cursor_grab(winit::window::CursorGrabMode::Locked)), + .set_cursor_grab(WinitCursorGrabMode::Confined) + .or_else(|_e| winit_window.set_cursor_grab(WinitCursorGrabMode::Locked)), CursorGrabMode::Locked => winit_window - .set_cursor_grab(winit::window::CursorGrabMode::Locked) - .or_else(|_e| winit_window.set_cursor_grab(winit::window::CursorGrabMode::Confined)), + .set_cursor_grab(WinitCursorGrabMode::Locked) + .or_else(|_e| winit_window.set_cursor_grab(WinitCursorGrabMode::Confined)), }; if let Err(err) = grab_result { diff --git a/examples/README.md b/examples/README.md index 661c9c6a78..ddead2b7d4 100644 --- a/examples/README.md +++ b/examples/README.md @@ -459,6 +459,7 @@ Example | Description Example | Description --- | --- [Clear Color](../examples/window/clear_color.rs) | Creates a solid color window +[Custom User Event](../examples/window/custom_user_event.rs) | Handles custom user events within the event loop [Low Power](../examples/window/low_power.rs) | Demonstrates settings to reduce power use for bevy applications [Multiple Windows](../examples/window/multiple_windows.rs) | Demonstrates creating multiple windows, and rendering to them [Scale Factor Override](../examples/window/scale_factor_override.rs) | Illustrates how to customize the default window settings diff --git a/examples/mobile/src/lib.rs b/examples/mobile/src/lib.rs index 60af2455d5..45a89a076c 100644 --- a/examples/mobile/src/lib.rs +++ b/examples/mobile/src/lib.rs @@ -4,7 +4,7 @@ use bevy::{ color::palettes::basic::*, input::touch::TouchPhase, prelude::*, - window::{ApplicationLifetime, WindowMode}, + window::{AppLifecycle, WindowMode}, }; // the `bevy_main` proc_macro generates the required boilerplate for iOS and Android @@ -166,14 +166,18 @@ fn setup_music(asset_server: Res, mut commands: Commands) { // Pause audio when app goes into background and resume when it returns. // This is handled by the OS on iOS, but not on Android. fn handle_lifetime( - mut lifetime_events: EventReader, + mut lifecycle_events: EventReader, music_controller: Query<&AudioSink>, ) { - for event in lifetime_events.read() { + let Ok(music_controller) = music_controller.get_single() else { + return; + }; + + for event in lifecycle_events.read() { match event { - ApplicationLifetime::Suspended => music_controller.single().pause(), - ApplicationLifetime::Resumed => music_controller.single().play(), - ApplicationLifetime::Started => (), + AppLifecycle::Idle | AppLifecycle::WillSuspend | AppLifecycle::WillResume => {} + AppLifecycle::Suspended => music_controller.pause(), + AppLifecycle::Running => music_controller.play(), } } } diff --git a/examples/window/custom_user_event.rs b/examples/window/custom_user_event.rs new file mode 100644 index 0000000000..6fe3c57d7a --- /dev/null +++ b/examples/window/custom_user_event.rs @@ -0,0 +1,122 @@ +//! Shows how to create a custom event that can be handled by `winit`'s event loop. + +use bevy::prelude::*; +use bevy::winit::{EventLoopProxy, WakeUp, WinitPlugin}; +use std::fmt::Formatter; +use std::sync::OnceLock; + +#[derive(Default, Debug, Event)] +enum CustomEvent { + #[default] + WakeUp, + Key(char), +} + +impl std::fmt::Display for CustomEvent { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::WakeUp => write!(f, "Wake up"), + Self::Key(ch) => write!(f, "Key: {ch}"), + } + } +} + +static EVENT_LOOP_PROXY: OnceLock> = OnceLock::new(); + +fn main() { + let winit_plugin = WinitPlugin::::default(); + + App::new() + .add_plugins( + DefaultPlugins + .build() + // Only one event type can be handled at once + // so we must disable the default event type + .disable::>() + .add(winit_plugin), + ) + .add_systems( + Startup, + ( + setup, + expose_event_loop_proxy, + #[cfg(target_arch = "wasm32")] + wasm::setup_js_closure, + ), + ) + .add_systems(Update, (send_event, handle_event)) + .run(); +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2dBundle::default()); +} + +fn send_event(input: Res>) { + let Some(event_loop_proxy) = EVENT_LOOP_PROXY.get() else { + return; + }; + + if input.just_pressed(KeyCode::Space) { + let _ = event_loop_proxy.send_event(CustomEvent::WakeUp); + } + + // This simulates sending a custom event through an external thread. + #[cfg(not(target_arch = "wasm32"))] + if input.just_pressed(KeyCode::KeyE) { + let handler = std::thread::spawn(|| { + let _ = event_loop_proxy.send_event(CustomEvent::Key('e')); + }); + + handler.join().unwrap(); + } +} + +fn expose_event_loop_proxy(event_loop_proxy: NonSend>) { + EVENT_LOOP_PROXY.set((*event_loop_proxy).clone()).unwrap(); +} + +fn handle_event(mut events: EventReader) { + for evt in events.read() { + info!("Received event: {evt:?}"); + } +} + +/// Since the [`EventLoopProxy`] can be exposed to the javascript environment, it can +/// be used to send events inside the loop, to be handled by a system or simply to wake up +/// the loop if that's currently waiting for a timeout or a user event. +#[cfg(target_arch = "wasm32")] +pub(crate) mod wasm { + use crate::{CustomEvent, EVENT_LOOP_PROXY}; + use wasm_bindgen::prelude::*; + use wasm_bindgen::JsCast; + use web_sys::KeyboardEvent; + + pub(crate) fn setup_js_closure() { + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + + let closure = Closure::wrap(Box::new(move |event: KeyboardEvent| { + let key = event.key(); + if key == "e" { + send_custom_event('e').unwrap(); + } + }) as Box); + + document + .add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref()) + .unwrap(); + + closure.forget(); + } + + fn send_custom_event(ch: char) -> Result<(), String> { + if let Some(proxy) = EVENT_LOOP_PROXY.get() { + proxy + .send_event(CustomEvent::Key(ch)) + .map_err(|_| "Failed to send event".to_string()) + } else { + Err("Event loop proxy not found".to_string()) + } + } +} diff --git a/examples/window/low_power.rs b/examples/window/low_power.rs index 9f00de2f84..795d064b35 100644 --- a/examples/window/low_power.rs +++ b/examples/window/low_power.rs @@ -3,10 +3,12 @@ //! This is useful for making desktop applications, or any other program that doesn't need to be //! running the event loop non-stop. +use bevy::window::WindowResolution; +use bevy::winit::WakeUp; use bevy::{ prelude::*, utils::Duration, - window::{PresentMode, RequestRedraw, WindowPlugin}, + window::{PresentMode, WindowPlugin}, winit::{EventLoopProxy, WinitSettings}, }; @@ -19,15 +21,14 @@ fn main() { // You can also customize update behavior with the fields of [`WinitSettings`] .insert_resource(WinitSettings { focused_mode: bevy::winit::UpdateMode::Continuous, - unfocused_mode: bevy::winit::UpdateMode::ReactiveLowPower { - wait: Duration::from_millis(10), - }, + unfocused_mode: bevy::winit::UpdateMode::reactive_low_power(Duration::from_millis(10)), }) .insert_resource(ExampleMode::Game) .add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { // Turn off vsync to maximize CPU/GPU usage present_mode: PresentMode::AutoNoVsync, + resolution: WindowResolution::new(800., 640.).with_scale_factor_override(1.), ..default() }), ..default() @@ -56,7 +57,7 @@ enum ExampleMode { fn update_winit( mode: Res, mut winit_config: ResMut, - event_loop_proxy: NonSend, + event_loop_proxy: NonSend>, ) { use ExampleMode::*; *winit_config = match *mode { @@ -78,7 +79,10 @@ fn update_winit( // (e.g. the mouse hovers over a visible part of the out of focus window), a // [`RequestRedraw`] event is received, or one minute has passed without the app // updating. - WinitSettings::desktop_app() + WinitSettings { + focused_mode: bevy::winit::UpdateMode::reactive(Duration::from_secs(1)), + unfocused_mode: bevy::winit::UpdateMode::reactive_low_power(Duration::from_secs(5)), + } } ApplicationWithRedraw => { // Sending a `RequestRedraw` event is useful when you want the app to update the next @@ -87,7 +91,7 @@ fn update_winit( // when there are no inputs, so you send redraw requests while the animation is playing. // Note that in this example the RequestRedraw winit event will make the app run in the same // way as continuous - let _ = event_loop_proxy.send_event(RequestRedraw); + let _ = event_loop_proxy.send_event(WakeUp); WinitSettings::desktop_app() } }; diff --git a/examples/window/scale_factor_override.rs b/examples/window/scale_factor_override.rs index 8ca92469c1..b814dac6f4 100644 --- a/examples/window/scale_factor_override.rs +++ b/examples/window/scale_factor_override.rs @@ -3,6 +3,9 @@ use bevy::{prelude::*, window::WindowResolution}; +#[derive(Component)] +struct CustomText; + fn main() { App::new() .add_plugins(DefaultPlugins.set(WindowPlugin { @@ -39,7 +42,7 @@ fn setup(mut commands: Commands) { parent .spawn(NodeBundle { style: Style { - width: Val::Px(200.0), + width: Val::Px(300.0), height: Val::Percent(100.0), border: UiRect::all(Val::Px(2.0)), ..default() @@ -48,7 +51,8 @@ fn setup(mut commands: Commands) { ..default() }) .with_children(|parent| { - parent.spawn( + parent.spawn(( + CustomText, TextBundle::from_section( "Example text", TextStyle { @@ -60,19 +64,32 @@ fn setup(mut commands: Commands) { align_self: AlignSelf::FlexEnd, ..default() }), - ); + )); }); }); } /// Set the title of the window to the current override -fn display_override(mut windows: Query<&mut Window>) { +fn display_override( + mut windows: Query<&mut Window>, + mut custom_text: Query<&mut Text, With>, +) { let mut window = windows.single_mut(); - window.title = format!( - "Scale override: {:?}", - window.resolution.scale_factor_override() + let text = format!( + "Scale factor: {:.1} {}", + window.scale_factor(), + if window.resolution.scale_factor_override().is_some() { + "(overridden)" + } else { + "(default)" + } ); + + window.title.clone_from(&text); + + let mut custom_text = custom_text.single_mut(); + custom_text.sections[0].value = text; } /// This system toggles scale factor overrides when enter is pressed diff --git a/tools/example-showcase/extra-window-resized-events.patch b/tools/example-showcase/extra-window-resized-events.patch index 345e2f6382..71fe860356 100644 --- a/tools/example-showcase/extra-window-resized-events.patch +++ b/tools/example-showcase/extra-window-resized-events.patch @@ -1,17 +1,17 @@ -diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs -index 63d79b1d2..83ed56293 100644 ---- a/crates/bevy_winit/src/lib.rs -+++ b/crates/bevy_winit/src/lib.rs -@@ -429,6 +429,12 @@ fn handle_winit_event( +diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs +index df0aab42d..6e28a6e9c 100644 +--- a/crates/bevy_winit/src/state.rs ++++ b/crates/bevy_winit/src/state.rs +@@ -208,6 +208,12 @@ impl ApplicationHandler for WinitAppRunnerState { + } + } - runner_state.window_event_received = true; - -+ window_resized.send(WindowResized { -+ window, -+ width: win.width(), -+ height: win.height(), -+ }); ++ window_resized.send(WindowResized { ++ window, ++ width: win.width(), ++ height: win.height(), ++ }); + - match event { - WindowEvent::Resized(size) => { - react_to_resize(&mut win, size, &mut window_resized, window); + match event { + WindowEvent::Resized(size) => { + react_to_resize(window, &mut win, size, &mut window_resized); diff --git a/tools/example-showcase/remove-desktop-app-mode.patch b/tools/example-showcase/remove-desktop-app-mode.patch index 7551ce3dc5..2d21e07902 100644 --- a/tools/example-showcase/remove-desktop-app-mode.patch +++ b/tools/example-showcase/remove-desktop-app-mode.patch @@ -1,18 +1,14 @@ diff --git a/crates/bevy_winit/src/winit_config.rs b/crates/bevy_winit/src/winit_config.rs -index f2cb424ec..e68e01de0 100644 +index 104384086..6e3c8dd83 100644 --- a/crates/bevy_winit/src/winit_config.rs +++ b/crates/bevy_winit/src/winit_config.rs -@@ -31,14 +31,7 @@ impl WinitSettings { +@@ -29,10 +29,7 @@ impl WinitSettings { /// /// Use the [`EventLoopProxy`](crate::EventLoopProxy) to request a redraw from outside bevy. pub fn desktop_app() -> Self { - WinitSettings { -- focused_mode: UpdateMode::Reactive { -- wait: Duration::from_secs(5), -- }, -- unfocused_mode: UpdateMode::ReactiveLowPower { -- wait: Duration::from_secs(60), -- }, +- focused_mode: UpdateMode::reactive(Duration::from_secs(5)), +- unfocused_mode: UpdateMode::reactive_low_power(Duration::from_secs(60)), - } + Self::default() }