diff --git a/crates/bevy_winit/src/accessibility.rs b/crates/bevy_winit/src/accessibility.rs index f22291af8d533a..54fb6c4ec80e60 100644 --- a/crates/bevy_winit/src/accessibility.rs +++ b/crates/bevy_winit/src/accessibility.rs @@ -14,16 +14,19 @@ use bevy_a11y::{ use bevy_app::{App, Plugin, PostUpdate}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - prelude::{DetectChanges, Entity, EventReader, EventWriter}, + prelude::{DetectChanges, Entity, EventReader, EventWriter, ThreadLocal, ThreadLocalResource}, query::With, - system::{NonSend, NonSendMut, Query, Res, ResMut, Resource}, + system::{Query, Res, ResMut, Resource}, }; use bevy_hierarchy::{Children, Parent}; use bevy_utils::{default, HashMap}; use bevy_window::{PrimaryWindow, Window, WindowClosed, WindowFocused}; -/// Maps window entities to their `AccessKit` [`Adapter`]s. -#[derive(Default, Deref, DerefMut)] +/// Maps each window entity to its `AccessKit` [`Adapter`]. +/// +/// **Note:** This is a [`ThreadLocalResource`] because the macOS implementation of [`Adapter`] +/// is not [`Send`](Send). +#[derive(ThreadLocalResource, Default, Deref, DerefMut)] pub struct AccessKitAdapters(pub HashMap); /// Maps window entities to their respective [`WinitActionHandler`]s. @@ -43,35 +46,41 @@ impl ActionHandler for WinitActionHandler { fn handle_window_focus( focus: Res, - adapters: NonSend, mut focused: EventReader, + mut main_thread: ThreadLocal, ) { - for event in focused.read() { - if let Some(adapter) = adapters.get(&event.window) { - adapter.update_if_active(|| { - let focus_id = (*focus).unwrap_or_else(|| event.window); - TreeUpdate { - focus: if event.focused { - Some(focus_id.to_node_id()) - } else { - None - }, - ..default() - } - }); + main_thread.run(|tls| { + let adapters = tls.resource::(); + for event in focused.read() { + if let Some(adapter) = adapters.get(&event.window) { + adapter.update_if_active(|| { + let focus_id = (*focus).unwrap_or_else(|| event.window); + TreeUpdate { + focus: if event.focused { + Some(focus_id.to_node_id()) + } else { + None + }, + ..default() + } + }); + } } - } + }); } fn window_closed( - mut adapters: NonSendMut, mut receivers: ResMut, mut events: EventReader, + mut main_thread: ThreadLocal, ) { - for WindowClosed { window, .. } in events.read() { - adapters.remove(window); - receivers.remove(window); - } + main_thread.run(|tls| { + let mut adapters = tls.resource_mut::(); + for WindowClosed { window, .. } in events.read() { + adapters.remove(window); + receivers.remove(window); + } + }); } fn poll_receivers( @@ -87,7 +96,6 @@ fn poll_receivers( } fn update_accessibility_nodes( - adapters: NonSend, focus: Res, accessibility_requested: Res, primary_window: Query<(Entity, &Window), With>, @@ -98,70 +106,75 @@ fn update_accessibility_nodes( Option<&Parent>, )>, node_entities: Query>, + mut main_thread: ThreadLocal, ) { if !accessibility_requested.load(Ordering::SeqCst) { return; } + if let Ok((primary_window_id, primary_window)) = primary_window.get_single() { - if let Some(adapter) = adapters.get(&primary_window_id) { - let should_run = focus.is_changed() || !nodes.is_empty(); - if should_run { - adapter.update_if_active(|| { - let mut to_update = vec![]; - let mut has_focus = false; - let mut name = None; - if primary_window.focused { - has_focus = true; - let title = primary_window.title.clone(); - name = Some(title.into_boxed_str()); - } - let focus_id = if has_focus { - (*focus).or_else(|| Some(primary_window_id)) - } else { - None - }; - let mut root_children = vec![]; - for (entity, node, children, parent) in &nodes { - let mut node = (**node).clone(); - if let Some(parent) = parent { - if node_entities.get(**parent).is_err() { + main_thread.run(|tls| { + let adapters = tls.resource::(); + if let Some(adapter) = adapters.get(&primary_window_id) { + let should_run = focus.is_changed() || !nodes.is_empty(); + if should_run { + adapter.update_if_active(|| { + let mut to_update = vec![]; + let mut has_focus = false; + let mut name = None; + if primary_window.focused { + has_focus = true; + let title = primary_window.title.clone(); + name = Some(title.into_boxed_str()); + } + let focus_id = if has_focus { + (*focus).or_else(|| Some(primary_window_id)) + } else { + None + }; + let mut root_children = vec![]; + for (entity, node, children, parent) in &nodes { + let mut node = (**node).clone(); + if let Some(parent) = parent { + if node_entities.get(**parent).is_err() { + root_children.push(entity.to_node_id()); + } + } else { root_children.push(entity.to_node_id()); } - } else { - root_children.push(entity.to_node_id()); - } - if let Some(children) = children { - for child in children { - if node_entities.get(*child).is_ok() { - node.push_child(child.to_node_id()); + if let Some(children) = children { + for child in children { + if node_entities.get(*child).is_ok() { + node.push_child(child.to_node_id()); + } } } + to_update.push(( + entity.to_node_id(), + node.build(&mut NodeClassSet::lock_global()), + )); } - to_update.push(( - entity.to_node_id(), - node.build(&mut NodeClassSet::lock_global()), - )); - } - let mut root = NodeBuilder::new(Role::Window); - if let Some(name) = name { - root.set_name(name); - } - root.set_children(root_children); - let root = root.build(&mut NodeClassSet::lock_global()); - let window_update = (primary_window_id.to_node_id(), root); - to_update.insert(0, window_update); - TreeUpdate { - nodes: to_update, - focus: focus_id.map(|v| v.to_node_id()), - ..default() - } - }); + let mut root = NodeBuilder::new(Role::Window); + if let Some(name) = name { + root.set_name(name); + } + root.set_children(root_children); + let root = root.build(&mut NodeClassSet::lock_global()); + let window_update = (primary_window_id.to_node_id(), root); + to_update.insert(0, window_update); + TreeUpdate { + nodes: to_update, + focus: focus_id.map(|v| v.to_node_id()), + ..default() + } + }); + } } - } + }); } } -/// Implements winit-specific `AccessKit` functionality. +/// Implements [`winit`]-specific `AccessKit` functionality. pub struct AccessibilityPlugin; impl Plugin for AccessibilityPlugin { diff --git a/crates/bevy_winit/src/converters.rs b/crates/bevy_winit/src/converters.rs index 85302ecb0d68cd..4e11811aa88502 100644 --- a/crates/bevy_winit/src/converters.rs +++ b/crates/bevy_winit/src/converters.rs @@ -307,3 +307,234 @@ pub fn convert_enabled_buttons(enabled_buttons: EnabledButtons) -> winit::window } window_buttons } + +use winit::dpi::{PhysicalPosition, PhysicalSize}; +use winit::event::{ + AxisId, DeviceEvent, DeviceId, ElementState, Ime, ModifiersState, StartCause, Touch, +}; +use winit::window::{Theme, WindowId}; + +use std::path::PathBuf; + +// TODO: can remove all these types when we upgrade to winit 0.29 +#[derive(Debug, PartialEq)] +#[allow(clippy::enum_variant_names)] +pub(crate) enum Event { + NewEvents(StartCause), + WindowEvent { + window_id: WindowId, + event: WindowEvent, + }, + DeviceEvent { + device_id: DeviceId, + event: DeviceEvent, + }, + UserEvent(T), + Suspended, + Resumed, + MainEventsCleared, + RedrawRequested(WindowId), + RedrawEventsCleared, + LoopDestroyed, +} + +#[derive(Debug, PartialEq)] +pub(crate) enum WindowEvent { + Resized(PhysicalSize), + Moved(PhysicalPosition), + CloseRequested, + Destroyed, + DroppedFile(PathBuf), + HoveredFile(PathBuf), + HoveredFileCancelled, + ReceivedCharacter(char), + Focused(bool), + KeyboardInput { + device_id: DeviceId, + input: winit::event::KeyboardInput, + is_synthetic: bool, + }, + ModifiersChanged(ModifiersState), + Ime(Ime), + CursorMoved { + device_id: DeviceId, + position: PhysicalPosition, + }, + + CursorEntered { + device_id: DeviceId, + }, + CursorLeft { + device_id: DeviceId, + }, + MouseWheel { + device_id: DeviceId, + delta: winit::event::MouseScrollDelta, + phase: winit::event::TouchPhase, + }, + MouseInput { + device_id: DeviceId, + state: ElementState, + button: winit::event::MouseButton, + }, + TouchpadMagnify { + device_id: DeviceId, + delta: f64, + phase: winit::event::TouchPhase, + }, + SmartMagnify { + device_id: DeviceId, + }, + TouchpadRotate { + device_id: DeviceId, + delta: f32, + phase: winit::event::TouchPhase, + }, + TouchpadPressure { + device_id: DeviceId, + pressure: f32, + stage: i64, + }, + AxisMotion { + device_id: DeviceId, + axis: AxisId, + value: f64, + }, + Touch(Touch), + ScaleFactorChanged { + scale_factor: f64, + new_inner_size: PhysicalSize, + }, + ThemeChanged(Theme), + Occluded(bool), +} + +pub(crate) fn convert_event(event: winit::event::Event<'_, T>) -> Event { + match event { + winit::event::Event::NewEvents(start_cause) => Event::NewEvents(start_cause), + winit::event::Event::WindowEvent { window_id, event } => Event::WindowEvent { + window_id, + event: convert_window_event(event), + }, + winit::event::Event::DeviceEvent { device_id, event } => { + Event::DeviceEvent { device_id, event } + } + winit::event::Event::UserEvent(value) => Event::UserEvent(value), + winit::event::Event::Suspended => Event::Suspended, + winit::event::Event::Resumed => Event::Resumed, + winit::event::Event::MainEventsCleared => Event::MainEventsCleared, + winit::event::Event::RedrawRequested(window_id) => Event::RedrawRequested(window_id), + winit::event::Event::RedrawEventsCleared => Event::RedrawEventsCleared, + winit::event::Event::LoopDestroyed => Event::LoopDestroyed, + } +} + +pub(crate) fn convert_window_event(event: winit::event::WindowEvent<'_>) -> WindowEvent { + match event { + winit::event::WindowEvent::AxisMotion { + device_id, + axis, + value, + } => WindowEvent::AxisMotion { + device_id, + axis, + value, + }, + winit::event::WindowEvent::CloseRequested => WindowEvent::CloseRequested, + winit::event::WindowEvent::CursorEntered { device_id } => { + WindowEvent::CursorEntered { device_id } + } + winit::event::WindowEvent::CursorLeft { device_id } => { + WindowEvent::CursorLeft { device_id } + } + winit::event::WindowEvent::CursorMoved { + device_id, + position, + .. + } => WindowEvent::CursorMoved { + device_id, + position, + }, + winit::event::WindowEvent::Destroyed => WindowEvent::Destroyed, + winit::event::WindowEvent::DroppedFile(path_buf) => WindowEvent::DroppedFile(path_buf), + winit::event::WindowEvent::Focused(b) => WindowEvent::Focused(b), + winit::event::WindowEvent::HoveredFile(path_buf) => WindowEvent::HoveredFile(path_buf), + winit::event::WindowEvent::HoveredFileCancelled => WindowEvent::HoveredFileCancelled, + winit::event::WindowEvent::Ime(ime) => WindowEvent::Ime(ime), + winit::event::WindowEvent::KeyboardInput { + device_id, + input, + is_synthetic, + } => WindowEvent::KeyboardInput { + device_id, + input, + is_synthetic, + }, + winit::event::WindowEvent::ModifiersChanged(modifiers_state) => { + WindowEvent::ModifiersChanged(modifiers_state) + } + winit::event::WindowEvent::MouseInput { + device_id, + state, + button, + .. + } => WindowEvent::MouseInput { + device_id, + state, + button, + }, + winit::event::WindowEvent::MouseWheel { + device_id, + delta, + phase, + .. + } => WindowEvent::MouseWheel { + device_id, + delta, + phase, + }, + winit::event::WindowEvent::Moved(new_position) => WindowEvent::Moved(new_position), + winit::event::WindowEvent::Occluded(b) => WindowEvent::Occluded(b), + winit::event::WindowEvent::ReceivedCharacter(char) => WindowEvent::ReceivedCharacter(char), + winit::event::WindowEvent::Resized(new_size) => WindowEvent::Resized(new_size), + winit::event::WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + } => WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size: *new_inner_size, + }, + winit::event::WindowEvent::SmartMagnify { device_id } => { + WindowEvent::SmartMagnify { device_id } + } + winit::event::WindowEvent::ThemeChanged(theme) => WindowEvent::ThemeChanged(theme), + winit::event::WindowEvent::Touch(touch) => WindowEvent::Touch(touch), + winit::event::WindowEvent::TouchpadMagnify { + device_id, + delta, + phase, + } => WindowEvent::TouchpadMagnify { + device_id, + delta, + phase, + }, + winit::event::WindowEvent::TouchpadPressure { + device_id, + pressure, + stage, + } => WindowEvent::TouchpadPressure { + device_id, + pressure, + stage, + }, + winit::event::WindowEvent::TouchpadRotate { + device_id, + delta, + phase, + } => WindowEvent::TouchpadRotate { + device_id, + delta, + phase, + }, + } +} diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 4c68a31de3ed5f..1a0b36bf15629e 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -1,11 +1,11 @@ #![allow(clippy::type_complexity)] #![warn(missing_docs)] -//! `bevy_winit` provides utilities to handle window creation and the eventloop through [`winit`] +//! `bevy_winit` provides utilities to create and manage windows through [`winit`] //! -//! Most commonly, the [`WinitPlugin`] is used as part of -//! [`DefaultPlugins`](https://docs.rs/bevy/latest/bevy/struct.DefaultPlugins.html). -//! The app's [runner](bevy_app::App::runner) is set by `WinitPlugin` and handles the `winit` [`EventLoop`](winit::event_loop::EventLoop). -//! See `winit_runner` for details. +//! The [`WinitPlugin`] is one of the [`DefaultPlugins`]. It registers an [`App`](bevy_app::App) +//! runner that manages the [`App`](bevy_app::App) using an [`EventLoop`](winit::event_loop::EventLoop). +//! +//! [`DefaultPlugins`]: https://docs.rs/bevy/latest/bevy/struct.DefaultPlugins.html pub mod accessibility; mod converters; @@ -15,49 +15,38 @@ mod web_resize; mod winit_config; mod winit_windows; -use bevy_a11y::AccessibilityRequested; -use system::{changed_windows, create_windows, despawn_windows, CachedWindow}; +use accessibility::AccessibilityPlugin; +pub use runner::*; +use system::{changed_windows, create_windows, despawn_windows}; +#[cfg(target_arch = "wasm32")] +use web_resize::{CanvasParentResizeEventChannel, CanvasParentResizePlugin}; pub use winit_config::*; pub use winit_windows::*; -use bevy_app::{App, AppExit, Last, Plugin}; -use bevy_ecs::event::{Events, ManualEventReader}; +use winit::event_loop::EventLoopBuilder; +#[cfg(target_os = "android")] +pub use winit::platform::android::activity::AndroidApp; + +#[cfg(not(target_arch = "wasm32"))] +use bevy_app::AppEvent; +use bevy_app::{App, First, Last, Plugin}; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; -use bevy_ecs::system::{SystemParam, SystemState}; +use bevy_ecs::storage::{ThreadLocalTask, ThreadLocalTaskSendError, ThreadLocalTaskSender}; +use bevy_ecs::system::SystemParam; use bevy_input::{ keyboard::KeyboardInput, - mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, + mouse::{MouseButtonInput, MouseMotion, MouseWheel}, touch::TouchInput, 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::{trace, warn}, - Duration, Instant, -}; +use bevy_utils::{synccell::SyncCell, tracing::warn, Instant}; use bevy_window::{ exit_on_all_closed, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, - ReceivedCharacter, RequestRedraw, Window, WindowBackendScaleFactorChanged, - WindowCloseRequested, WindowCreated, WindowDestroyed, WindowFocused, WindowMoved, - WindowResized, WindowScaleFactorChanged, WindowThemeChanged, + ReceivedCharacter, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowDestroyed, + WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged, WindowThemeChanged, }; -#[cfg(target_os = "android")] -pub use winit::platform::android::activity::AndroidApp; - -use winit::{ - event::{self, DeviceEvent, Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopWindowTarget}, -}; - -use crate::accessibility::{AccessKitAdapters, AccessibilityPlugin, WinitActionHandlers}; - -use crate::converters::convert_winit_theme; -#[cfg(target_arch = "wasm32")] -use crate::web_resize::{CanvasParentResizeEventChannel, CanvasParentResizePlugin}; - /// [`AndroidApp`] provides an interface to query the application state as well as monitor events /// (for example lifecycle and input events). #[cfg(target_os = "android")] @@ -74,7 +63,8 @@ pub struct WinitPlugin; impl Plugin for WinitPlugin { fn build(&self, app: &mut App) { - let mut event_loop_builder = EventLoopBuilder::<()>::with_user_event(); + // setup event loop + let mut event_loop_builder = EventLoopBuilder::::with_user_event(); #[cfg(target_os = "android")] { use winit::platform::android::EventLoopBuilderExtAndroid; @@ -86,109 +76,61 @@ impl Plugin for WinitPlugin { ); } + let event_loop = crate::EventLoop::new(event_loop_builder.build()); + app.insert_non_send_resource(event_loop); + + // setup app app.init_non_send_resource::() .init_resource::() .set_runner(winit_runner) .add_systems( Last, ( - // `exit_on_all_closed` only checks if windows exist but doesn't access data, - // so we don't need to care about its ordering relative to `changed_windows` + // `exit_on_all_closed` seemingly conflicts with `changed_windows` + // but does not actually access any data that would alias (only metadata) changed_windows.ambiguous_with(exit_on_all_closed), despawn_windows, + create_windows::, ) + // apply all changes before despawning windows for consistent event ordering .chain(), ); + // TODO: schedule after TimeSystem + #[cfg(not(target_arch = "wasm32"))] + app.add_systems(First, flush_winit_events::); + app.add_plugins(AccessibilityPlugin); #[cfg(target_arch = "wasm32")] app.add_plugins(CanvasParentResizePlugin); - let event_loop = event_loop_builder.build(); - - // iOS, macOS, and Android don't like it if you create windows before the event loop is - // initialized. + // 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. - #[cfg(not(target_arch = "wasm32"))] - let mut create_window_system_state: SystemState<( - Commands, - Query<(Entity, &mut Window)>, - EventWriter, - NonSendMut, - NonSendMut, - ResMut, - ResMut, - )> = SystemState::from_world(app.world_mut()); - - #[cfg(target_arch = "wasm32")] - let mut create_window_system_state: SystemState<( - Commands, - Query<(Entity, &mut Window)>, - EventWriter, - NonSendMut, - NonSendMut, - ResMut, - ResMut, - ResMut, - )> = SystemState::from_world(app.world_mut()); - - #[cfg(not(target_arch = "wasm32"))] - let ( - commands, - mut windows, - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - ) = create_window_system_state.get_mut(app.world_mut()); - - #[cfg(target_arch = "wasm32")] - let ( - commands, - mut windows, - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - event_channel, - ) = create_window_system_state.get_mut(app.world_mut()); - - create_windows( - &event_loop, - commands, - windows.iter_mut(), - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - #[cfg(target_arch = "wasm32")] - event_channel, - ); - - create_window_system_state.apply(app.world_mut()); + // Otherwise, 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_windows = IntoSystem::into_system(create_windows::); + create_windows.run((), app.world_mut()); + create_windows.apply_deferred(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); } } -fn run(event_loop: EventLoop, event_handler: F) -> ! +pub(crate) fn run(event_loop: winit::event_loop::EventLoop, event_handler: F) -> ! where - F: 'static + FnMut(Event<'_, T>, &EventLoopWindowTarget, &mut ControlFlow), + F: 'static + + FnMut( + winit::event::Event<'_, T>, + &winit::event_loop::EventLoopWindowTarget, + &mut winit::event_loop::ControlFlow, + ), { event_loop.run(event_handler) } @@ -202,9 +144,13 @@ where target_os = "netbsd", target_os = "openbsd" ))] -fn run_return(event_loop: &mut EventLoop, event_handler: F) +pub(crate) fn run_return(event_loop: &mut winit::event_loop::EventLoop, event_handler: F) where - F: FnMut(Event<'_, T>, &EventLoopWindowTarget, &mut ControlFlow), + F: FnMut( + winit::event::Event<'_, T>, + &winit::event_loop::EventLoopWindowTarget, + &mut winit::event_loop::ControlFlow, + ), { use winit::platform::run_return::EventLoopExtRunReturn; event_loop.run_return(event_handler); @@ -219,9 +165,13 @@ where target_os = "netbsd", target_os = "openbsd" )))] -fn run_return(_event_loop: &mut EventLoop, _event_handler: F) +pub(crate) fn run_return(_event_loop: &mut winit::event_loop::EventLoop, _event_handler: F) where - F: FnMut(Event<'_, T>, &EventLoopWindowTarget, &mut ControlFlow), + F: FnMut( + winit::event::Event<'_, T>, + &winit::event_loop::EventLoopWindowTarget, + &mut winit::event_loop::ControlFlow, + ), { panic!("Run return is not supported on this platform!") } @@ -253,18 +203,19 @@ struct WindowAndInputEventWriters<'w> { mouse_motion: EventWriter<'w, MouseMotion>, } -/// Persistent state that is used to run the [`App`] according to the current -/// [`UpdateMode`]. +/// Persistent state that is used to run the [`App`] according to the current [`UpdateMode`]. struct WinitAppRunnerState { /// Is `true` if the app is running and not suspended. is_active: bool, - /// Is `true` if a new [`WindowEvent`] has been received since the last update. + /// Is `true` if a new window event has been received. window_event_received: bool, - /// Is `true` if the app has requested a redraw since the last update. + /// Is `true` if a new device event has been received. + device_event_received: bool, + /// Is `true` if the app has requested a redraw. redraw_requested: bool, - /// Is `true` if enough time has elapsed since `last_update` to run another update. + /// Is `true` if enough time has elapsed since `last_update`. wait_elapsed: bool, - /// The time the last update started. + /// The time the most recent update started. last_update: Instant, /// The time the next update is scheduled to start. scheduled_update: Option, @@ -273,8 +224,9 @@ struct WinitAppRunnerState { impl Default for WinitAppRunnerState { fn default() -> Self { Self { - is_active: false, + is_active: true, window_event_received: false, + device_event_received: false, redraw_requested: false, wait_elapsed: false, last_update: Instant::now(), @@ -283,509 +235,1236 @@ impl Default for WinitAppRunnerState { } } -/// 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) { - let mut event_loop = app - .world_mut() - .remove_non_send_resource::>() - .unwrap(); - - let return_from_run = app.world().resource::().return_from_run; - - app.world_mut() - .insert_non_send_resource(event_loop.create_proxy()); - - let mut runner_state = WinitAppRunnerState::default(); - - // prepare structures to access data in the world - let mut app_exit_event_reader = ManualEventReader::::default(); - let mut redraw_event_reader = ManualEventReader::::default(); - - let mut focused_windows_state: SystemState<(Res, Query<&Window>)> = - SystemState::new(app.world_mut()); - - let mut event_writer_system_state: SystemState<( - WindowAndInputEventWriters, - NonSend, - Query<(&mut Window, &mut CachedWindow)>, - )> = SystemState::new(app.world_mut()); - - #[cfg(not(target_arch = "wasm32"))] - let mut create_window_system_state: SystemState<( - Commands, - Query<(Entity, &mut Window), Added>, - EventWriter, - NonSendMut, - NonSendMut, - ResMut, - ResMut, - )> = SystemState::from_world(app.world_mut()); - - #[cfg(target_arch = "wasm32")] - let mut create_window_system_state: SystemState<( - Commands, - Query<(Entity, &mut Window), Added>, - EventWriter, - NonSendMut, - NonSendMut, - ResMut, - ResMut, - ResMut, - )> = SystemState::from_world(app.world_mut()); - - let mut finished_and_setup_done = false; - - // setup up the event loop - let event_handler = move |event: Event<()>, - event_loop: &EventLoopWindowTarget<()>, - control_flow: &mut ControlFlow| { - #[cfg(feature = "trace")] - let _span = bevy_utils::tracing::info_span!("winit event_handler").entered(); - - if !finished_and_setup_done { - if !app.is_ready() { - #[cfg(not(target_arch = "wasm32"))] - tick_global_task_pools_on_main_thread(); - } else { - app.finish(); - app.cleanup(); - finished_and_setup_done = true; - } +#[derive(ThreadLocalResource, Deref, DerefMut)] +pub(crate) struct EventLoop(winit::event_loop::EventLoop); - if let Some(app_exit_events) = app.world().get_resource::>() { - if app_exit_event_reader.read(app_exit_events).last().is_some() { - *control_flow = ControlFlow::Exit; - return; - } - } - } +impl EventLoop { + pub fn new(value: winit::event_loop::EventLoop) -> Self { + Self(value) + } - match event { - event::Event::NewEvents(start_cause) => match start_cause { - StartCause::Init => { - #[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))] - { - #[cfg(not(target_arch = "wasm32"))] - let ( - commands, - mut windows, - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - ) = create_window_system_state.get_mut(app.world_mut()); - - #[cfg(target_arch = "wasm32")] - let ( - commands, - mut windows, - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - event_channel, - ) = create_window_system_state.get_mut(app.world_mut()); - - create_windows( - event_loop, - commands, - windows.iter_mut(), - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - #[cfg(target_arch = "wasm32")] - event_channel, - ); + pub fn into_inner(self) -> winit::event_loop::EventLoop { + self.0 + } +} - create_window_system_state.apply(app.world_mut()); - } +/// [`EventLoopWindowTarget`](winit::event_loop::EventLoopWindowTarget) that +/// systems can access through [`ThreadLocal`]. +#[derive(ThreadLocalResource, Deref)] +pub struct EventLoopWindowTarget(&'static winit::event_loop::EventLoopWindowTarget); + +/// [`EventLoopProxy`](winit::event_loop::EventLoopProxy) wrapped in a [`SyncCell`]. +/// Allows systems to wake the [`winit`] event loop from any thread. +#[derive(Resource, Deref, DerefMut)] +pub struct EventLoopProxy(pub SyncCell>); + +impl EventLoopProxy { + pub(crate) fn new(value: winit::event_loop::EventLoopProxy) -> Self { + Self(SyncCell::new(value)) + } +} + +impl ThreadLocalTaskSender for crate::EventLoopProxy { + fn send_task( + &mut self, + task: ThreadLocalTask, + ) -> Result<(), ThreadLocalTaskSendError> { + self.0 + .get() + .send_event(AppEvent::Task(task)) + .map_err(|error| { + let AppEvent::Task(task) = error.0 else { + unreachable!() + }; + ThreadLocalTaskSendError(task) + }) + } +} + +#[cfg(target_arch = "wasm32")] +mod runner { + use crate::WinitWindows; + use crate::{ + converters, run, run_return, system::CachedWindow, UpdateMode, WindowAndInputEventWriters, + WinitAppRunnerState, WinitSettings, + }; + use bevy_app::{App, AppEvent, AppExit}; + use bevy_ecs::{event::ManualEventReader, prelude::*, system::SystemState}; + use bevy_input::{ + mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, + touchpad::{TouchpadMagnify, TouchpadRotate}, + }; + use bevy_math::{ivec2, DVec2, Vec2}; + use bevy_utils::{ + tracing::{trace, warn}, + Duration, Instant, + }; + use bevy_window::{ + CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, ReceivedCharacter, + RequestRedraw, Window, WindowBackendScaleFactorChanged, WindowCloseRequested, + WindowDestroyed, WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged, + WindowThemeChanged, + }; + use winit::{event::StartCause, event_loop::ControlFlow}; + + /// The default [`App::runner`] for the [`WinitPlugin`] plugin. + pub(crate) fn winit_runner(mut app: App) { + let return_from_run = app.world().resource::().return_from_run; + let mut event_loop = app + .tls + .lock() + .remove_resource::>() + .unwrap() + .into_inner(); + let event_loop_proxy = event_loop.create_proxy(); + + // insert app -> winit channel + // + // This is done here because it's the only chance to insert the TLS channel resource to the + // rendering sub-app before it's moved to another thread. + // TODO: rework app setup + app.sub_apps.iter_mut().for_each(|sub_app| { + let sender = crate::EventLoopProxy::new(event_loop_proxy.clone()); + app.tls.insert_channel(sub_app.world_mut(), sender); + }); + + let mut runner_state = WinitAppRunnerState::default(); + + // prepare structures to access data in the world + let mut event_writer_system_state: SystemState<( + WindowAndInputEventWriters, + Query<(&mut Window, &mut CachedWindow)>, + )> = SystemState::new(app.world_mut()); + + let mut app_exit_event_reader = ManualEventReader::::default(); + let mut redraw_event_reader = ManualEventReader::::default(); + + let mut focused_windows_state: SystemState<(Res, Query<&Window>)> = + SystemState::from_world(app.world_mut()); + + let mut finished_and_setup_done = false; + + let event_cb = move |event: winit::event::Event, + event_loop: &winit::event_loop::EventLoopWindowTarget, + control_flow: &mut ControlFlow| { + #[cfg(feature = "trace")] + let _span = bevy_utils::tracing::info_span!("winit event_handler").entered(); + + if !finished_and_setup_done { + if app.is_ready() { + app.finish(); + app.cleanup(); + finished_and_setup_done = true; } - _ => { - if let Some(t) = runner_state.scheduled_update { - let now = Instant::now(); - let remaining = t.checked_duration_since(now).unwrap_or(Duration::ZERO); - runner_state.wait_elapsed = remaining.is_zero(); + + if let Some(app_exit_events) = app.world().get_resource::>() { + if app_exit_event_reader.read(app_exit_events).last().is_some() { + *control_flow = ControlFlow::Exit; + return; } } - }, - event::Event::WindowEvent { - event, window_id, .. - } => { - let (mut event_writers, winit_windows, mut windows) = - event_writer_system_state.get_mut(app.world_mut()); - - let Some(window_entity) = winit_windows.get_window_entity(window_id) else { - warn!( - "Skipped event {:?} for unknown winit Window Id {:?}", - event, window_id - ); - return; - }; - let Ok((mut window, mut cache)) = windows.get_mut(window_entity) else { - warn!( - "Window {:?} is missing `Window` component, skipping event {:?}", - window_entity, event - ); - return; - }; + *control_flow = ControlFlow::Poll; + } - runner_state.window_event_received = true; + // SAFETY: + // - The reference cannot leave this thread. + // - The reference is wrapped and cannot be copied. + // - The reference lives long enough. + let tgt: &'static winit::event_loop::EventLoopWindowTarget = + unsafe { core::mem::transmute(event_loop) }; - match event { - WindowEvent::Resized(size) => { - window - .resolution - .set_physical_resolution(size.width, size.height); + app.tls + .lock() + .insert_resource(crate::EventLoopWindowTarget(tgt)); - event_writers.window_resized.send(WindowResized { - window: window_entity, - width: window.width(), - height: window.height(), - }); + match event { + winit::event::Event::NewEvents(start_cause) => match start_cause { + StartCause::Init => { + #[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))] + { + let mut create_windows = + IntoSystem::into_system(create_windows::); + create_windows.run((), app.world_mut()); + create_windows.apply_deferred(app.world_mut()); + } } - WindowEvent::CloseRequested => { - event_writers - .window_close_requested - .send(WindowCloseRequested { + _ => { + if let Some(next) = runner_state.scheduled_update { + let now = Instant::now(); + let remaining = + next.checked_duration_since(now).unwrap_or(Duration::ZERO); + runner_state.wait_elapsed = remaining.is_zero(); + } + } + }, + winit::event::Event::WindowEvent { + window_id, event, .. + } => 'window_event: { + let tls_guard = app.tls.lock(); + let winit_windows = tls_guard.resource::(); + let (mut event_writers, mut windows) = + event_writer_system_state.get_mut(app.sub_apps.main.world_mut()); + + let Some(window_entity) = winit_windows.get_window_entity(window_id) else { + warn!( + "Skipped event {:?} for unknown winit Window Id {:?}", + event, window_id + ); + break 'window_event; + }; + + let Ok((mut window, mut cache)) = windows.get_mut(window_entity) else { + warn!( + "Window {:?} is missing `Window` component, skipping event {:?}", + window_entity, event + ); + break 'window_event; + }; + + match event { + winit::event::WindowEvent::Resized(size) => { + window + .resolution + .set_physical_resolution(size.width, size.height); + + event_writers.window_resized.send(WindowResized { window: window_entity, + width: window.width(), + height: window.height(), }); - } - WindowEvent::KeyboardInput { ref input, .. } => { - event_writers - .keyboard_input - .send(converters::convert_keyboard_input(input, window_entity)); - } - WindowEvent::CursorMoved { position, .. } => { - let physical_position = DVec2::new(position.x, position.y); - window.set_physical_cursor_position(Some(physical_position)); - event_writers.cursor_moved.send(CursorMoved { - window: window_entity, - position: (physical_position / window.resolution.scale_factor()) - .as_vec2(), - }); - } - WindowEvent::CursorEntered { .. } => { - event_writers.cursor_entered.send(CursorEntered { - window: window_entity, - }); - } - WindowEvent::CursorLeft { .. } => { - window.set_physical_cursor_position(None); - event_writers.cursor_left.send(CursorLeft { - window: window_entity, - }); - } - WindowEvent::MouseInput { state, button, .. } => { - event_writers.mouse_button_input.send(MouseButtonInput { - button: converters::convert_mouse_button(button), - state: converters::convert_element_state(state), - window: window_entity, - }); - } - WindowEvent::TouchpadMagnify { delta, .. } => { - event_writers - .touchpad_magnify_input - .send(TouchpadMagnify(delta as f32)); - } - WindowEvent::TouchpadRotate { delta, .. } => { - event_writers - .touchpad_rotate_input - .send(TouchpadRotate(delta)); - } - WindowEvent::MouseWheel { delta, .. } => match delta { - event::MouseScrollDelta::LineDelta(x, y) => { - event_writers.mouse_wheel_input.send(MouseWheel { - unit: MouseScrollUnit::Line, - x, - y, + } + winit::event::WindowEvent::CloseRequested => { + event_writers + .window_close_requested + .send(WindowCloseRequested { + window: window_entity, + }); + } + winit::event::WindowEvent::KeyboardInput { ref input, .. } => { + event_writers + .keyboard_input + .send(converters::convert_keyboard_input(input, window_entity)); + } + winit::event::WindowEvent::CursorMoved { position, .. } => { + let physical_position = DVec2::new(position.x, position.y); + window.set_physical_cursor_position(Some(physical_position)); + event_writers.cursor_moved.send(CursorMoved { window: window_entity, + position: (physical_position / window.resolution.scale_factor()) + .as_vec2(), }); } - event::MouseScrollDelta::PixelDelta(p) => { - event_writers.mouse_wheel_input.send(MouseWheel { - unit: MouseScrollUnit::Pixel, - x: p.x as f32, - y: p.y as f32, + winit::event::WindowEvent::CursorEntered { .. } => { + event_writers.cursor_entered.send(CursorEntered { window: window_entity, }); } - }, - WindowEvent::Touch(touch) => { - let location = touch.location.to_logical(window.resolution.scale_factor()); - event_writers - .touch_input - .send(converters::convert_touch_input(touch, location)); - } - WindowEvent::ReceivedCharacter(char) => { - event_writers.character_input.send(ReceivedCharacter { - window: window_entity, - char, - }); - } - WindowEvent::ScaleFactorChanged { - scale_factor, - new_inner_size, - } => { - event_writers.window_backend_scale_factor_changed.send( - WindowBackendScaleFactorChanged { + winit::event::WindowEvent::CursorLeft { .. } => { + window.set_physical_cursor_position(None); + event_writers.cursor_left.send(CursorLeft { window: window_entity, - scale_factor, - }, - ); - - let prior_factor = window.resolution.scale_factor(); - window.resolution.set_scale_factor(scale_factor); - let new_factor = window.resolution.scale_factor(); - - 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. - *new_inner_size = - winit::dpi::LogicalSize::new(window.width(), window.height()) - .to_physical::(forced_factor); - } else if approx::relative_ne!(new_factor, prior_factor) { - event_writers.window_scale_factor_changed.send( - WindowScaleFactorChanged { + }); + } + winit::event::WindowEvent::MouseInput { state, button, .. } => { + event_writers.mouse_button_input.send(MouseButtonInput { + button: converters::convert_mouse_button(button), + state: converters::convert_element_state(state), + window: window_entity, + }); + } + winit::event::WindowEvent::TouchpadMagnify { delta, .. } => { + event_writers + .touchpad_magnify_input + .send(TouchpadMagnify(delta as f32)); + } + winit::event::WindowEvent::TouchpadRotate { delta, .. } => { + event_writers + .touchpad_rotate_input + .send(TouchpadRotate(delta)); + } + winit::event::WindowEvent::MouseWheel { delta, .. } => match delta { + winit::event::MouseScrollDelta::LineDelta(x, y) => { + event_writers.mouse_wheel_input.send(MouseWheel { + unit: MouseScrollUnit::Line, + x, + y, + window: window_entity, + }); + } + winit::event::MouseScrollDelta::PixelDelta(p) => { + event_writers.mouse_wheel_input.send(MouseWheel { + unit: MouseScrollUnit::Pixel, + x: p.x as f32, + y: p.y as f32, + window: window_entity, + }); + } + }, + winit::event::WindowEvent::Touch(touch) => { + let location = + touch.location.to_logical(window.resolution.scale_factor()); + event_writers + .touch_input + .send(converters::convert_touch_input(touch, location)); + } + winit::event::WindowEvent::ReceivedCharacter(char) => { + event_writers.character_input.send(ReceivedCharacter { + window: window_entity, + char, + }); + } + winit::event::WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + } => { + event_writers.window_backend_scale_factor_changed.send( + WindowBackendScaleFactorChanged { window: window_entity, scale_factor, }, ); - } - let new_logical_width = (new_inner_size.width as f64 / new_factor) as f32; - let new_logical_height = (new_inner_size.height as f64 / new_factor) as f32; - if approx::relative_ne!(window.width(), new_logical_width) - || approx::relative_ne!(window.height(), new_logical_height) - { - event_writers.window_resized.send(WindowResized { + let prior_factor = window.resolution.scale_factor(); + window.resolution.set_scale_factor(scale_factor); + let new_factor = window.resolution.scale_factor(); + + 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. + *new_inner_size = + winit::dpi::LogicalSize::new(window.width(), window.height()) + .to_physical::(forced_factor); + } else if approx::relative_ne!(new_factor, prior_factor) { + event_writers.window_scale_factor_changed.send( + WindowScaleFactorChanged { + window: window_entity, + scale_factor, + }, + ); + } + + let new_logical_width = + (new_inner_size.width as f64 / new_factor) as f32; + let new_logical_height = + (new_inner_size.height as f64 / new_factor) as f32; + if approx::relative_ne!(window.width(), new_logical_width) + || approx::relative_ne!(window.height(), new_logical_height) + { + event_writers.window_resized.send(WindowResized { + window: window_entity, + width: new_logical_width, + height: new_logical_height, + }); + } + window.resolution.set_physical_resolution( + new_inner_size.width, + new_inner_size.height, + ); + } + winit::event::WindowEvent::Focused(focused) => { + window.focused = focused; + event_writers.window_focused.send(WindowFocused { window: window_entity, - width: new_logical_width, - height: new_logical_height, + focused, }); } - window - .resolution - .set_physical_resolution(new_inner_size.width, new_inner_size.height); - } - WindowEvent::Focused(focused) => { - window.focused = focused; - event_writers.window_focused.send(WindowFocused { - window: window_entity, - focused, - }); - } - WindowEvent::DroppedFile(path_buf) => { - event_writers - .file_drag_and_drop - .send(FileDragAndDrop::DroppedFile { + winit::event::WindowEvent::DroppedFile(path_buf) => { + event_writers + .file_drag_and_drop + .send(FileDragAndDrop::DroppedFile { + window: window_entity, + path_buf, + }); + } + winit::event::WindowEvent::HoveredFile(path_buf) => { + event_writers + .file_drag_and_drop + .send(FileDragAndDrop::HoveredFile { + window: window_entity, + path_buf, + }); + } + winit::event::WindowEvent::HoveredFileCancelled => { + event_writers.file_drag_and_drop.send( + FileDragAndDrop::HoveredFileCanceled { + window: window_entity, + }, + ); + } + winit::event::WindowEvent::Moved(position) => { + let position = ivec2(position.x, position.y); + window.position.set(position); + event_writers.window_moved.send(WindowMoved { + entity: window_entity, + position, + }); + } + winit::event::WindowEvent::Ime(event) => match event { + winit::event::Ime::Preedit(value, cursor) => { + event_writers.ime_input.send(Ime::Preedit { + window: window_entity, + value, + cursor, + }); + } + winit::event::Ime::Commit(value) => { + event_writers.ime_input.send(Ime::Commit { + window: window_entity, + value, + }) + } + winit::event::Ime::Enabled => { + event_writers.ime_input.send(Ime::Enabled { + window: window_entity, + }) + } + winit::event::Ime::Disabled => { + event_writers.ime_input.send(Ime::Disabled { + window: window_entity, + }) + } + }, + winit::event::WindowEvent::ThemeChanged(theme) => { + event_writers.window_theme_changed.send(WindowThemeChanged { window: window_entity, - path_buf, + theme: converters::convert_winit_theme(theme), }); - } - WindowEvent::HoveredFile(path_buf) => { - event_writers - .file_drag_and_drop - .send(FileDragAndDrop::HoveredFile { + } + winit::event::WindowEvent::Destroyed => { + event_writers.window_destroyed.send(WindowDestroyed { window: window_entity, - path_buf, }); + } + _ => {} } - WindowEvent::HoveredFileCancelled => { - event_writers.file_drag_and_drop.send( - FileDragAndDrop::HoveredFileCanceled { - window: window_entity, - }, - ); + + if window.is_changed() { + cache.window = window.clone(); } - WindowEvent::Moved(position) => { - let position = ivec2(position.x, position.y); - window.position.set(position); - event_writers.window_moved.send(WindowMoved { - entity: window_entity, - position, - }); + } + winit::event::Event::DeviceEvent { + event: winit::event::DeviceEvent::MouseMotion { delta: (x, y) }, + .. + } => { + let (mut event_writers, _) = event_writer_system_state.get_mut(app.world_mut()); + event_writers.mouse_motion.send(MouseMotion { + delta: Vec2::new(x as f32, y as f32), + }); + } + winit::event::Event::Resumed => { + runner_state.is_active = true; + } + winit::event::Event::Suspended => { + runner_state.is_active = false; + #[cfg(target_os = "android")] + { + // Android sending this event invalidates all render surfaces. + // TODO + // Upon resume, check if the new render surfaces are compatible with the + // existing render device. If not (which should basically never happen), + // then try to rebuild the renderer. + *control_flow = ControlFlow::Exit; } - WindowEvent::Ime(event) => match event { - event::Ime::Preedit(value, cursor) => { - event_writers.ime_input.send(Ime::Preedit { - window: window_entity, - value, - cursor, - }); + } + winit::event::Event::MainEventsCleared => { + if finished_and_setup_done && runner_state.is_active { + let (config, windows) = focused_windows_state.get(app.world()); + let focused = windows.iter().any(|window| window.focused); + let should_update = match config.update_mode(focused) { + UpdateMode::Continuous | UpdateMode::Reactive { .. } => { + // `Reactive`: In order for `event_handler` to have been called, + // either we received a window or raw input event, the `wait` + // elapsed, or a redraw was requested. There are no other + // conditions, so we can just return `true` here. + true + } + UpdateMode::ReactiveLowPower { .. } => { + runner_state.wait_elapsed + || runner_state.redraw_requested + || runner_state.window_event_received + } + }; + + if should_update { + // reset these on each update + runner_state.wait_elapsed = false; + runner_state.window_event_received = false; + runner_state.redraw_requested = false; + runner_state.last_update = Instant::now(); + + app.update(); + + // decide when to run the next update + let (config, windows) = focused_windows_state.get(app.world()); + let focused = windows.iter().any(|window| window.focused); + match config.update_mode(focused) { + UpdateMode::Continuous => *control_flow = ControlFlow::Poll, + UpdateMode::Reactive { wait } + | UpdateMode::ReactiveLowPower { wait } => { + if let Some(next) = runner_state.last_update.checked_add(wait) { + runner_state.scheduled_update = Some(next); + *control_flow = ControlFlow::WaitUntil(next); + } else { + runner_state.scheduled_update = None; + *control_flow = ControlFlow::Wait; + } + } + } + + if let Some(redraw_events) = + app.world().get_resource::>() + { + if redraw_event_reader.read(redraw_events).last().is_some() { + runner_state.redraw_requested = true; + *control_flow = ControlFlow::Poll; + } + } + + if let Some(exit_events) = app.world().get_resource::>() + { + if app_exit_event_reader.read(exit_events).last().is_some() { + trace!("exiting app"); + *control_flow = ControlFlow::Exit; + } + } } - event::Ime::Commit(value) => event_writers.ime_input.send(Ime::Commit { - window: window_entity, - value, - }), - event::Ime::Enabled => event_writers.ime_input.send(Ime::Enabled { - window: window_entity, - }), - event::Ime::Disabled => event_writers.ime_input.send(Ime::Disabled { - window: window_entity, - }), - }, - WindowEvent::ThemeChanged(theme) => { - event_writers.window_theme_changed.send(WindowThemeChanged { - window: window_entity, - theme: convert_winit_theme(theme), - }); - } - WindowEvent::Destroyed => { - event_writers.window_destroyed.send(WindowDestroyed { - window: window_entity, - }); } - _ => {} } + _ => (), + } + + app.tls + .lock() + .remove_resource::>(); + }; + + trace!("starting winit event loop"); + if return_from_run { + run_return(&mut event_loop, event_cb); + } else { + run(event_loop, event_cb); + } + } +} + +#[cfg(not(target_arch = "wasm32"))] +mod runner { + use std::collections::VecDeque; + use std::mem; + use std::sync::mpsc::{channel, Receiver, RecvError, RecvTimeoutError, Sender, TryRecvError}; - if window.is_changed() { - cache.window = window.clone(); + use bevy_app::{App, AppEvent, AppExit, SubApps}; + use bevy_ecs::{event::ManualEventReader, prelude::*, system::SystemState}; + use bevy_input::{ + mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, + touchpad::{TouchpadMagnify, TouchpadRotate}, + }; + use bevy_math::{ivec2, DVec2, Vec2}; + use bevy_tasks::tick_global_task_pools_on_main_thread; + use bevy_utils::{ + default, + synccell::SyncCell, + tracing::{trace, warn}, + Duration, Instant, + }; + use bevy_window::{ + CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, ReceivedCharacter, + RequestRedraw, Window, WindowBackendScaleFactorChanged, WindowCloseRequested, + WindowDestroyed, WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged, + WindowThemeChanged, + }; + use winit::event_loop::ControlFlow; + + use crate::{ + converters::{self, convert_event}, + run, run_return, + system::CachedWindow, + UpdateMode, WindowAndInputEventWriters, WinitAppRunnerState, WinitSettings, + WinitWindowEntityMap, WinitWindows, + }; + + /// Sending half of an [`Event`] channel. + pub struct WinitEventSender { + pub(crate) event_send: Sender>, + pub(crate) clear_send: Sender, + pub(crate) last_event_sent: u64, + } + + /// Receiving half of an [`Event`] channel. + #[derive(Resource)] + pub struct WinitEventReceiver { + pub(crate) event_recv: SyncCell>>, + pub(crate) clear_recv: SyncCell>, + pub(crate) processed: SyncCell>>, + pub(crate) last_event_processed: u64, + pub(crate) state: WinitAppRunnerState, + } + + /// Constructs a new [`WinitEventSender`] and [`WinitEventReceiver`] channel pair. + pub fn winit_channel() -> (WinitEventSender, WinitEventReceiver) { + let (clear_send, clear_recv) = channel(); + let (event_send, event_recv) = channel(); + let processed = VecDeque::new(); + + let sender = WinitEventSender { + clear_send, + event_send, + last_event_sent: 0, + }; + + let receiver = WinitEventReceiver { + event_recv: SyncCell::new(event_recv), + clear_recv: SyncCell::new(clear_recv), + processed: SyncCell::new(processed), + last_event_processed: 0, + state: default(), + }; + + (sender, receiver) + } + + impl WinitEventSender + where + T: Send + 'static, + { + pub(crate) fn send(&mut self, event: crate::converters::Event) { + self.last_event_sent = self.last_event_sent.checked_add(1).unwrap(); + self.event_send.send(event).unwrap(); + } + + /// Informs the receiver that there is a new batch of events to be read. + pub(crate) fn send_clear(&mut self, event: crate::converters::Event) { + assert!(matches!(event, crate::converters::Event::MainEventsCleared)); + self.send(event); + self.clear_send.send(self.last_event_sent).unwrap(); + } + } + + impl WinitEventReceiver + where + T: Send + 'static, + { + fn process_event(&mut self, event: crate::converters::Event) { + match &event { + crate::converters::Event::WindowEvent { .. } => { + self.state.window_event_received = true; + } + crate::converters::Event::DeviceEvent { .. } => { + self.state.device_event_received = true; + } + crate::converters::Event::Suspended => { + self.state.is_active = false; + } + crate::converters::Event::Resumed => { + self.state.is_active = true; } + crate::converters::Event::RedrawRequested(_) => { + self.state.redraw_requested = true; + } + _ => (), + } + self.last_event_processed = self.last_event_processed.checked_add(1).unwrap(); + self.processed.get().push_back(event); + } + + fn process_events_until(&mut self, clear_event: u64) { + while self.last_event_processed <= clear_event { + let event = self.event_recv.get().try_recv().unwrap(); + self.process_event(event); } - event::Event::DeviceEvent { - event: DeviceEvent::MouseMotion { delta: (x, y) }, - .. - } => { - let (mut event_writers, _, _) = event_writer_system_state.get_mut(app.world_mut()); - event_writers.mouse_motion.send(MouseMotion { - delta: Vec2::new(x as f32, y as f32), - }); + } + + pub(crate) fn recv(&mut self) -> Result<(), RecvError> { + let rx = self.clear_recv.get(); + let mut event = rx.recv()?; + while let Ok(n) = rx.try_recv() { + assert!(n > event); + event = n; + } + self.process_events_until(event); + Ok(()) + } + + pub(crate) fn try_recv(&mut self) -> Result<(), TryRecvError> { + let rx = self.clear_recv.get(); + let mut event = rx.try_recv()?; + while let Ok(n) = rx.try_recv() { + assert!(n > event); + event = n; + } + self.process_events_until(event); + Ok(()) + } + + pub(crate) fn recv_timeout(&mut self, timeout: Duration) -> Result<(), RecvTimeoutError> { + let rx = self.clear_recv.get(); + let mut event = rx.recv_timeout(timeout)?; + while let Ok(n) = rx.try_recv() { + assert!(n > event); + event = n; } - event::Event::Suspended => { - runner_state.is_active = false; - #[cfg(target_os = "android")] - { - // Android sending this event invalidates all render surfaces. - // TODO - // Upon resume, check if the new render surfaces are compatible with the - // existing render device. If not (which should basically never happen), - // then try to rebuild the renderer. - *control_flow = ControlFlow::Exit; + self.process_events_until(event); + Ok(()) + } + } + + pub(crate) fn flush_winit_events( + mut queue: ResMut>, + map: Res, + mut event_writers: WindowAndInputEventWriters, + mut windows: Query<(&mut Window, &mut CachedWindow)>, + ) { + for event in queue.processed.get().drain(..) { + match event { + crate::converters::Event::WindowEvent { + window_id, event, .. + } => { + let Some(window_entity) = map.get_window_entity(window_id) else { + warn!( + "Skipped event {:?} for unknown winit Window Id {:?}", + event, window_id + ); + continue; + }; + + let Ok((mut window, mut cache)) = windows.get_mut(window_entity) else { + warn!( + "Window {:?} is missing `Window` component, skipping event {:?}", + window_entity, event + ); + continue; + }; + + match event { + converters::WindowEvent::Resized(size) => { + window + .resolution + .set_physical_resolution(size.width, size.height); + + event_writers.window_resized.send(WindowResized { + window: window_entity, + width: window.width(), + height: window.height(), + }); + } + converters::WindowEvent::CloseRequested => { + event_writers + .window_close_requested + .send(WindowCloseRequested { + window: window_entity, + }); + } + converters::WindowEvent::KeyboardInput { ref input, .. } => { + event_writers + .keyboard_input + .send(converters::convert_keyboard_input(input, window_entity)); + } + converters::WindowEvent::CursorMoved { position, .. } => { + let physical_position = DVec2::new(position.x, position.y); + window.set_physical_cursor_position(Some(physical_position)); + event_writers.cursor_moved.send(CursorMoved { + window: window_entity, + position: (physical_position / window.resolution.scale_factor()) + .as_vec2(), + }); + } + converters::WindowEvent::CursorEntered { .. } => { + event_writers.cursor_entered.send(CursorEntered { + window: window_entity, + }); + } + converters::WindowEvent::CursorLeft { .. } => { + window.set_physical_cursor_position(None); + event_writers.cursor_left.send(CursorLeft { + window: window_entity, + }); + } + converters::WindowEvent::MouseInput { state, button, .. } => { + event_writers.mouse_button_input.send(MouseButtonInput { + button: converters::convert_mouse_button(button), + state: converters::convert_element_state(state), + window: window_entity, + }); + } + converters::WindowEvent::TouchpadMagnify { delta, .. } => { + event_writers + .touchpad_magnify_input + .send(TouchpadMagnify(delta as f32)); + } + converters::WindowEvent::TouchpadRotate { delta, .. } => { + event_writers + .touchpad_rotate_input + .send(TouchpadRotate(delta)); + } + converters::WindowEvent::MouseWheel { delta, .. } => match delta { + winit::event::MouseScrollDelta::LineDelta(x, y) => { + event_writers.mouse_wheel_input.send(MouseWheel { + unit: MouseScrollUnit::Line, + x, + y, + window: window_entity, + }); + } + winit::event::MouseScrollDelta::PixelDelta(p) => { + event_writers.mouse_wheel_input.send(MouseWheel { + unit: MouseScrollUnit::Pixel, + x: p.x as f32, + y: p.y as f32, + window: window_entity, + }); + } + }, + converters::WindowEvent::Touch(touch) => { + let location = + touch.location.to_logical(window.resolution.scale_factor()); + event_writers + .touch_input + .send(converters::convert_touch_input(touch, location)); + } + converters::WindowEvent::ReceivedCharacter(char) => { + event_writers.character_input.send(ReceivedCharacter { + window: window_entity, + char, + }); + } + converters::WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + } => { + event_writers.window_backend_scale_factor_changed.send( + WindowBackendScaleFactorChanged { + window: window_entity, + scale_factor, + }, + ); + + let prior_factor = window.resolution.scale_factor(); + window.resolution.set_scale_factor(scale_factor); + + if window.resolution.scale_factor_override().is_none() + && approx::relative_ne!(scale_factor, prior_factor) + { + event_writers.window_scale_factor_changed.send( + WindowScaleFactorChanged { + window: window_entity, + scale_factor, + }, + ); + } + + let new_factor = window.resolution.scale_factor(); + let new_logical_width = + (new_inner_size.width as f64 / new_factor) as f32; + let new_logical_height = + (new_inner_size.height as f64 / new_factor) as f32; + if approx::relative_ne!(window.width(), new_logical_width) + || approx::relative_ne!(window.height(), new_logical_height) + { + event_writers.window_resized.send(WindowResized { + window: window_entity, + width: new_logical_width, + height: new_logical_height, + }); + } + window.resolution.set_physical_resolution( + new_inner_size.width, + new_inner_size.height, + ); + } + converters::WindowEvent::Focused(focused) => { + window.focused = focused; + event_writers.window_focused.send(WindowFocused { + window: window_entity, + focused, + }); + } + converters::WindowEvent::DroppedFile(path_buf) => { + event_writers + .file_drag_and_drop + .send(FileDragAndDrop::DroppedFile { + window: window_entity, + path_buf, + }); + } + converters::WindowEvent::HoveredFile(path_buf) => { + event_writers + .file_drag_and_drop + .send(FileDragAndDrop::HoveredFile { + window: window_entity, + path_buf, + }); + } + converters::WindowEvent::HoveredFileCancelled => { + event_writers.file_drag_and_drop.send( + FileDragAndDrop::HoveredFileCanceled { + window: window_entity, + }, + ); + } + converters::WindowEvent::Moved(position) => { + let position = ivec2(position.x, position.y); + window.position.set(position); + event_writers.window_moved.send(WindowMoved { + entity: window_entity, + position, + }); + } + converters::WindowEvent::Ime(event) => match event { + winit::event::Ime::Preedit(value, cursor) => { + event_writers.ime_input.send(Ime::Preedit { + window: window_entity, + value, + cursor, + }); + } + winit::event::Ime::Commit(value) => { + event_writers.ime_input.send(Ime::Commit { + window: window_entity, + value, + }); + } + winit::event::Ime::Enabled => { + event_writers.ime_input.send(Ime::Enabled { + window: window_entity, + }); + } + winit::event::Ime::Disabled => { + event_writers.ime_input.send(Ime::Disabled { + window: window_entity, + }); + } + }, + converters::WindowEvent::ThemeChanged(theme) => { + event_writers.window_theme_changed.send(WindowThemeChanged { + window: window_entity, + theme: converters::convert_winit_theme(theme), + }); + } + converters::WindowEvent::Destroyed => { + event_writers.window_destroyed.send(WindowDestroyed { + window: window_entity, + }); + } + _ => {} + } + + if window.is_changed() { + cache.window = window.clone(); + } + } + crate::converters::Event::DeviceEvent { + event: winit::event::DeviceEvent::MouseMotion { delta: (x, y) }, + .. + } => { + event_writers.mouse_motion.send(MouseMotion { + delta: Vec2::new(x as f32, y as f32), + }); } + _ => (), } - event::Event::Resumed => { - runner_state.is_active = true; + } + } + + pub(crate) fn spawn_app_thread( + mut sub_apps: SubApps, + event_loop_proxy: winit::event_loop::EventLoopProxy, + ) { + std::thread::spawn(move || { + let mut app_exit_event_reader = ManualEventReader::::default(); + let mut redraw_event_reader = ManualEventReader::::default(); + let mut focused_windows_state: SystemState<(Res, Query<&Window>)> = + SystemState::from_world(sub_apps.main.world_mut()); + + let mut rx = sub_apps + .main + .world_mut() + .remove_resource::>() + .unwrap(); + + #[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))] + { + let mut create_windows = + IntoSystem::into_system(crate::system::create_windows::); + create_windows.run((), sub_apps.main.world_mut()); + create_windows.apply_deferred(sub_apps.main.world_mut()); } - event::Event::MainEventsCleared => { - if runner_state.is_active { - let (config, windows) = focused_windows_state.get(app.world()); + + let mut control_flow = ControlFlow::Poll; + loop { + let now = Instant::now(); + match control_flow { + ControlFlow::Poll => match rx.try_recv() { + Ok(_) | Err(TryRecvError::Empty) => {} + Err(TryRecvError::Disconnected) => { + trace!("terminating app because event loop disconnected"); + return; + } + }, + ControlFlow::Wait => match rx.recv() { + Ok(_) => {} + Err(_) => { + trace!("terminating app because event loop disconnected"); + return; + } + }, + ControlFlow::WaitUntil(next) => { + let timeout = next.checked_duration_since(now).unwrap_or(Duration::ZERO); + rx.state.wait_elapsed = timeout.is_zero(); + match rx.recv_timeout(timeout) { + Ok(_) | Err(RecvTimeoutError::Timeout) => {} + Err(RecvTimeoutError::Disconnected) => { + trace!("terminating app because event loop disconnected"); + return; + } + } + } + ControlFlow::ExitWithCode(_) => { + trace!("exiting app"); + // return sub-apps to the main thread + if event_loop_proxy + .send_event(AppEvent::Exit(Box::new(sub_apps))) + .is_err() + { + trace!("terminating app because event loop disconnected"); + } + return; + } + } + + if rx.state.is_active { + let (config, windows) = focused_windows_state.get(sub_apps.main.world()); let focused = windows.iter().any(|window| window.focused); let should_update = match config.update_mode(focused) { - UpdateMode::Continuous | UpdateMode::Reactive { .. } => { - // `Reactive`: In order for `event_handler` to have been called, either - // we received a window or raw input event, the `wait` elapsed, or a - // redraw was requested (by the app or the OS). There are no other - // conditions, so we can just return `true` here. - true + UpdateMode::Continuous => true, + UpdateMode::Reactive { .. } => { + rx.state.wait_elapsed + || rx.state.redraw_requested + || rx.state.window_event_received + || rx.state.device_event_received } UpdateMode::ReactiveLowPower { .. } => { - runner_state.wait_elapsed - || runner_state.redraw_requested - || runner_state.window_event_received + rx.state.wait_elapsed + || rx.state.redraw_requested + || rx.state.window_event_received } }; - if finished_and_setup_done && should_update { - // reset these on each update - runner_state.wait_elapsed = false; - runner_state.window_event_received = false; - runner_state.redraw_requested = false; - runner_state.last_update = Instant::now(); + if should_update { + // reset these flags + rx.state.wait_elapsed = false; + rx.state.redraw_requested = false; + rx.state.window_event_received = false; + rx.state.device_event_received = false; + rx.state.last_update = now; - app.update(); + sub_apps.main.world_mut().insert_resource(rx); + sub_apps.update(); + rx = sub_apps + .main + .world_mut() + .remove_resource::>() + .unwrap(); // decide when to run the next update - let (config, windows) = focused_windows_state.get(app.world()); + let (config, windows) = focused_windows_state.get(sub_apps.main.world()); let focused = windows.iter().any(|window| window.focused); match config.update_mode(focused) { - UpdateMode::Continuous => *control_flow = ControlFlow::Poll, + UpdateMode::Continuous => control_flow = ControlFlow::Poll, UpdateMode::Reactive { wait } | UpdateMode::ReactiveLowPower { wait } => { - if let Some(next) = runner_state.last_update.checked_add(*wait) { - runner_state.scheduled_update = Some(next); - *control_flow = ControlFlow::WaitUntil(next); + if let Some(next) = rx.state.last_update.checked_add(wait) { + rx.state.scheduled_update = Some(next); + control_flow = ControlFlow::WaitUntil(next); } else { - runner_state.scheduled_update = None; - *control_flow = ControlFlow::Wait; + rx.state.scheduled_update = None; + control_flow = ControlFlow::Wait; } } } - if let Some(app_redraw_events) = - app.world().get_resource::>() + if let Some(redraw_events) = sub_apps + .main + .world() + .get_resource::>() { - if redraw_event_reader.read(app_redraw_events).last().is_some() { - runner_state.redraw_requested = true; - *control_flow = ControlFlow::Poll; + if redraw_event_reader.read(redraw_events).last().is_some() { + rx.state.redraw_requested = true; + control_flow = ControlFlow::Poll; } } - if let Some(app_exit_events) = app.world().get_resource::>() + if let Some(exit_events) = + sub_apps.main.world().get_resource::>() { - if app_exit_event_reader.read(app_exit_events).last().is_some() { - *control_flow = ControlFlow::Exit; + if app_exit_event_reader.read(exit_events).last().is_some() { + control_flow = ControlFlow::Exit; } } } + } else { + #[cfg(target_os = "android")] + { + // Android sending this event invalidates all render surfaces. + // TODO + // Upon resume, check if the new render surfaces are compatible with the + // existing render device. If not (which should basically never happen), + // and *then* try to rebuild the renderer. + control_flow = ControlFlow::Exit; + } + } + } + }); + } + + /// The default [`App::runner`] for the [`WinitPlugin`] plugin. + pub(crate) fn winit_runner(mut app: App) { + let return_from_run = app.world().resource::().return_from_run; + let mut event_loop = app + .tls + .lock() + .remove_resource::>() + .unwrap() + .into_inner(); + let event_loop_proxy = event_loop.create_proxy(); + + // insert app -> winit channel + // + // This is done here because it's the only chance to insert the TLS channel resource to the + // rendering sub-app before it's moved to another thread. + // TODO: rework app setup + app.sub_apps.iter_mut().for_each(|sub_app| { + let app_send = crate::EventLoopProxy::new(event_loop_proxy.clone()); + app.tls.insert_channel(sub_app.world_mut(), app_send); + }); + + let mut app_exit_event_reader = ManualEventReader::::default(); - // create any new windows - // (even if app did not update, some may have been created by plugin setup) - #[cfg(not(target_arch = "wasm32"))] - let ( - commands, - mut windows, - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - ) = create_window_system_state.get_mut(app.world_mut()); - - #[cfg(target_arch = "wasm32")] - let ( - commands, - mut windows, - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - event_channel, - ) = create_window_system_state.get_mut(app.world_mut()); - - create_windows( - event_loop, - commands, - windows.iter_mut(), - event_writer, - winit_windows, - adapters, - handlers, - accessibility_requested, - #[cfg(target_arch = "wasm32")] - event_channel, - ); - - create_window_system_state.apply(app.world_mut()); + let (mut winit_send, winit_recv) = winit_channel::(); + let mut winit_recv = Some(winit_recv); + let mut locals = None; + + let mut finished_and_setup_done = false; + + let event_cb = move |event: winit::event::Event, + event_loop: &winit::event_loop::EventLoopWindowTarget, + control_flow: &mut ControlFlow| { + #[cfg(feature = "trace")] + let _span = bevy_utils::tracing::info_span!("winit event_handler").entered(); + + let mut should_start = false; + if !finished_and_setup_done { + if !app.is_ready() { + tick_global_task_pools_on_main_thread(); + } else { + app.finish(); + app.cleanup(); + finished_and_setup_done = true; + should_start = true; + } + + if let Some(app_exit_events) = app.world().get_resource::>() { + if app_exit_event_reader.read(app_exit_events).last().is_some() { + *control_flow = ControlFlow::Exit; + return; + } } + + *control_flow = ControlFlow::Poll; + } else { + // Since the app runs in its own thread, this thread should sleep when + // it has no events to process. + *control_flow = ControlFlow::Wait; } - _ => (), - } - }; - trace!("starting winit event loop"); - if return_from_run { - run_return(&mut event_loop, event_handler); - } else { - run(event_loop, event_handler); + if should_start { + // split app + let (mut sub_apps, tls, _) = mem::take(&mut app).into_parts(); + locals = Some(tls); + + // insert winit -> app channel + let winit_recv = winit_recv.take().unwrap(); + sub_apps.main.world_mut().insert_resource(winit_recv); + + // send sub-apps to separate thread + spawn_app_thread(sub_apps, event_loop_proxy.clone()); + } + + match event { + winit::event::Event::WindowEvent { + window_id, + event: + winit::event::WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + }, + } => { + if let Some(tls) = locals.as_mut() { + // This event requires special handling because writes to `new_inner_size` + // must happen here. It cannot be written asynchronously. + let tls_guard = tls.lock(); + let winit_windows = tls_guard.resource::(); + if let Some(window) = winit_windows.cached_windows.get(&window_id) { + if let Some(sf_override) = 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. + *new_inner_size = + winit::dpi::LogicalSize::new(window.width(), window.height()) + .to_physical::(sf_override); + } + } + } + + winit_send.send(converters::Event::WindowEvent { + window_id, + event: converters::WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size: *new_inner_size, + }, + }); + } + winit::event::Event::UserEvent(event) => { + assert!(finished_and_setup_done); + match event { + AppEvent::Task(f) => { + let tls = locals.as_mut().unwrap(); + + // SAFETY: + // - The reference cannot leave this thread. + // - The reference is wrapped and cannot be copied. + // - The reference lives long enough. + let tgt: &'static winit::event_loop::EventLoopWindowTarget = + unsafe { core::mem::transmute(event_loop) }; + + tls.lock() + .insert_resource(crate::EventLoopWindowTarget(tgt)); + + f(&mut tls.lock()); + + tls.lock() + .remove_resource::>(); + } + AppEvent::Exit(_) => { + *control_flow = ControlFlow::Exit; + } + } + } + winit::event::Event::MainEventsCleared => { + winit_send.send_clear(convert_event(event)); + } + _ => { + winit_send.send(convert_event(event)); + } + } + }; + + trace!("starting winit event loop"); + if return_from_run { + run_return(&mut event_loop, event_cb); + } else { + run(event_loop, event_cb); + } } } diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 896466f3c70734..db4d9d413d61ec 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -1,12 +1,5 @@ use bevy_a11y::AccessibilityRequested; -use bevy_ecs::{ - entity::Entity, - event::EventWriter, - prelude::{Changed, Component, Resource}, - removal_detection::RemovedComponents, - system::{Commands, NonSendMut, Query, ResMut}, - world::Mut, -}; +use bevy_ecs::prelude::*; use bevy_utils::{ tracing::{error, info, warn}, HashMap, @@ -14,20 +7,19 @@ use bevy_utils::{ use bevy_window::{RawHandleWrapper, Window, WindowClosed, WindowCreated}; use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; -use winit::{ - dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, - event_loop::EventLoopWindowTarget, -}; +use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; #[cfg(target_arch = "wasm32")] use crate::web_resize::{CanvasParentResizeEventChannel, WINIT_CANVAS_SELECTOR}; use crate::{ accessibility::{AccessKitAdapters, WinitActionHandlers}, + attempt_grab, converters::{ - self, convert_enabled_buttons, convert_window_level, convert_window_theme, + convert_cursor_icon, convert_enabled_buttons, convert_window_level, convert_window_theme, convert_winit_theme, }, - get_best_videomode, get_fitting_videomode, WinitWindows, + get_best_videomode, get_fitting_videomode, EventLoopWindowTarget, WinitWindowEntityMap, + WinitWindows, }; /// Creates new windows on the [`winit`] backend for each entity with a newly-added @@ -36,68 +28,76 @@ use crate::{ /// If any of these entities are missing required components, those will be added with their /// default values. #[allow(clippy::too_many_arguments)] -pub(crate) fn create_windows<'a>( - event_loop: &EventLoopWindowTarget<()>, +pub(crate) fn create_windows( mut commands: Commands, - created_windows: impl Iterator)>, + mut created_windows: Query<(Entity, &mut Window)>, mut event_writer: EventWriter, - mut winit_windows: NonSendMut, - mut adapters: NonSendMut, + mut window_entity_map: ResMut, mut handlers: ResMut, - accessibility_requested: ResMut, + accessibility_requested: Res, + mut main_thread: ThreadLocal, #[cfg(target_arch = "wasm32")] event_channel: ResMut, ) { - for (entity, mut window) in created_windows { - if winit_windows.get_window(entity).is_some() { - continue; - } + main_thread.run(|tls| { + tls.resource_scope(|tls, mut winit_windows: Mut| { + tls.resource_scope(|tls, mut adapters: Mut| { + for (entity, mut window) in created_windows.iter_mut() { + if winit_windows.get_window(entity).is_some() { + continue; + } - info!( - "Creating new window {:?} ({:?})", - window.title.as_str(), - entity - ); - - let winit_window = winit_windows.create_window( - event_loop, - entity, - &window, - &mut adapters, - &mut handlers, - &accessibility_requested, - ); - - if let Some(theme) = winit_window.theme() { - window.window_theme = Some(convert_winit_theme(theme)); - } + info!( + "Creating new window {:?} ({:?})", + window.title.as_str(), + entity + ); - window - .resolution - .set_scale_factor(winit_window.scale_factor()); - commands - .entity(entity) - .insert(RawHandleWrapper { - window_handle: winit_window.raw_window_handle(), - display_handle: winit_window.raw_display_handle(), - }) - .insert(CachedWindow { - window: window.clone(), - }); + let event_loop = tls.resource::>(); - #[cfg(target_arch = "wasm32")] - { - if window.fit_canvas_to_parent { - let selector = if let Some(selector) = &window.canvas { - selector - } else { - WINIT_CANVAS_SELECTOR - }; - event_channel.listen_to_selector(entity, selector); - } - } + let winit_window = winit_windows.create_window( + event_loop, + entity, + &window, + &mut window_entity_map, + &mut adapters, + &mut handlers, + &accessibility_requested, + ); - event_writer.send(WindowCreated { window: entity }); - } + if let Some(theme) = winit_window.theme() { + window.window_theme = Some(convert_winit_theme(theme)); + } + + window + .resolution + .set_scale_factor(winit_window.scale_factor()); + commands + .entity(entity) + .insert(RawHandleWrapper { + window_handle: winit_window.raw_window_handle(), + display_handle: winit_window.raw_display_handle(), + }) + .insert(CachedWindow { + window: window.clone(), + }); + + #[cfg(target_arch = "wasm32")] + { + if window.fit_canvas_to_parent { + let selector = if let Some(selector) = &window.canvas { + selector + } else { + WINIT_CANVAS_SELECTOR + }; + event_channel.listen_to_selector(entity, selector); + } + } + + event_writer.send(WindowCreated { window: entity }); + } + }); + }); + }); } /// Cache for closing windows so we can get better debug information. @@ -108,17 +108,20 @@ pub(crate) fn despawn_windows( mut closed: RemovedComponents, window_entities: Query<&Window>, mut close_events: EventWriter, - mut winit_windows: NonSendMut, + mut main_thread: ThreadLocal, ) { - for window in closed.read() { - info!("Closing window {:?}", window); - // Guard to verify that the window is in fact actually gone, - // rather than having the component added and removed in the same frame. - if !window_entities.contains(window) { - winit_windows.remove_window(window); - close_events.send(WindowClosed { window }); + main_thread.run(|tls| { + let mut winit_windows = tls.resource_mut::(); + for window in closed.read() { + info!("Closing window {:?}", window); + // Guard to verify that the window is in fact actually gone, + // rather than having the component added and removed in the same frame. + if !window_entities.contains(window) { + winit_windows.remove_window(window); + close_events.send(WindowClosed { window }); + } } - } + }); } /// The cached state of the window so we can check which properties were changed from within the app. @@ -127,7 +130,7 @@ pub struct CachedWindow { pub window: Window, } -/// Propagates changes from [`Window`] entities to the [`winit`] backend. +/// Propagates changes from window entities to the [`winit`] backend. /// /// # Notes /// @@ -137,182 +140,191 @@ pub struct CachedWindow { /// - [`Window::focused`] cannot be manually changed to `false` after the window is created. pub(crate) fn changed_windows( mut changed_windows: Query<(Entity, &mut Window, &mut CachedWindow), Changed>, - winit_windows: NonSendMut, + mut main_thread: ThreadLocal, ) { - for (entity, mut window, mut cache) in &mut changed_windows { - if let Some(winit_window) = winit_windows.get_window(entity) { - if window.title != cache.window.title { - winit_window.set_title(window.title.as_str()); - } + main_thread.run(|tls| { + let mut winit_windows = tls.resource_mut::(); + for (entity, mut window, mut cache) in &mut changed_windows { + if let Some(winit_window) = winit_windows.get_window(entity) { + if window.title != cache.window.title { + winit_window.set_title(window.title.as_str()); + } - if window.mode != cache.window.mode { - let new_mode = match window.mode { - bevy_window::WindowMode::BorderlessFullscreen => { - Some(winit::window::Fullscreen::Borderless(None)) - } - bevy_window::WindowMode::Fullscreen => { - Some(winit::window::Fullscreen::Exclusive(get_best_videomode( - &winit_window.current_monitor().unwrap(), - ))) - } - bevy_window::WindowMode::SizedFullscreen => { - Some(winit::window::Fullscreen::Exclusive(get_fitting_videomode( - &winit_window.current_monitor().unwrap(), - window.width() as u32, - window.height() as u32, - ))) - } - bevy_window::WindowMode::Windowed => None, - }; + if window.mode != cache.window.mode { + let new_mode = match window.mode { + bevy_window::WindowMode::BorderlessFullscreen => { + Some(winit::window::Fullscreen::Borderless(None)) + } + bevy_window::WindowMode::Fullscreen => { + Some(winit::window::Fullscreen::Exclusive(get_best_videomode( + &winit_window.current_monitor().unwrap(), + ))) + } + bevy_window::WindowMode::SizedFullscreen => { + Some(winit::window::Fullscreen::Exclusive(get_fitting_videomode( + &winit_window.current_monitor().unwrap(), + window.width() as u32, + window.height() as u32, + ))) + } + bevy_window::WindowMode::Windowed => None, + }; - if winit_window.fullscreen() != new_mode { - winit_window.set_fullscreen(new_mode); + if winit_window.fullscreen() != new_mode { + winit_window.set_fullscreen(new_mode); + } } - } - if window.resolution != cache.window.resolution { - let physical_size = PhysicalSize::new( - window.resolution.physical_width(), - window.resolution.physical_height(), - ); - winit_window.set_inner_size(physical_size); - } + if window.resolution != cache.window.resolution { + let physical_size = PhysicalSize::new( + window.resolution.physical_width(), + window.resolution.physical_height(), + ); + winit_window.set_inner_size(physical_size); + } + + if window.physical_cursor_position() != cache.window.physical_cursor_position() { + if let Some(physical_position) = window.physical_cursor_position() { + let inner_size = winit_window.inner_size(); - if window.physical_cursor_position() != cache.window.physical_cursor_position() { - if let Some(physical_position) = window.physical_cursor_position() { - let position = PhysicalPosition::new(physical_position.x, physical_position.y); + let position = PhysicalPosition::new( + physical_position.x, + // Flip the coordinate space back to winit's context. + inner_size.height as f32 - physical_position.y, + ); - if let Err(err) = winit_window.set_cursor_position(position) { - error!("could not set cursor position: {:?}", err); + if let Err(err) = winit_window.set_cursor_position(position) { + error!("could not set cursor position: {:?}", err); + } } } - } - - if window.cursor.icon != cache.window.cursor.icon { - winit_window.set_cursor_icon(converters::convert_cursor_icon(window.cursor.icon)); - } - if window.cursor.grab_mode != cache.window.cursor.grab_mode { - crate::winit_windows::attempt_grab(winit_window, window.cursor.grab_mode); - } + if window.cursor.icon != cache.window.cursor.icon { + winit_window.set_cursor_icon(convert_cursor_icon(window.cursor.icon)); + } - if window.cursor.visible != cache.window.cursor.visible { - winit_window.set_cursor_visible(window.cursor.visible); - } + if window.cursor.grab_mode != cache.window.cursor.grab_mode { + attempt_grab(winit_window, window.cursor.grab_mode); + } - if window.cursor.hit_test != cache.window.cursor.hit_test { - if let Err(err) = winit_window.set_cursor_hittest(window.cursor.hit_test) { - window.cursor.hit_test = cache.window.cursor.hit_test; - warn!( - "Could not set cursor hit test for window {:?}: {:?}", - window.title, err - ); + if window.cursor.visible != cache.window.cursor.visible { + winit_window.set_cursor_visible(window.cursor.visible); } - } - if window.decorations != cache.window.decorations - && window.decorations != winit_window.is_decorated() - { - winit_window.set_decorations(window.decorations); - } + if window.cursor.hit_test != cache.window.cursor.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(window.cursor.hit_test) { + window.cursor.hit_test = cache.window.cursor.hit_test; + warn!( + "Could not set cursor hit test for window {:?}: {:?}", + window.title, err + ); + } + } - if window.resizable != cache.window.resizable - && window.resizable != winit_window.is_resizable() - { - winit_window.set_resizable(window.resizable); - } + if window.decorations != cache.window.decorations + && window.decorations != winit_window.is_decorated() + { + winit_window.set_decorations(window.decorations); + } - if window.enabled_buttons != cache.window.enabled_buttons { - winit_window.set_enabled_buttons(convert_enabled_buttons(window.enabled_buttons)); - } + if window.resizable != cache.window.resizable + && window.resizable != winit_window.is_resizable() + { + winit_window.set_resizable(window.resizable); + } - if window.resize_constraints != cache.window.resize_constraints { - let constraints = window.resize_constraints.check_constraints(); - let min_inner_size = LogicalSize { - width: constraints.min_width, - height: constraints.min_height, - }; - let max_inner_size = LogicalSize { - width: constraints.max_width, - height: constraints.max_height, - }; - - winit_window.set_min_inner_size(Some(min_inner_size)); - if constraints.max_width.is_finite() && constraints.max_height.is_finite() { - winit_window.set_max_inner_size(Some(max_inner_size)); + if window.enabled_buttons != cache.window.enabled_buttons { + winit_window.set_enabled_buttons(convert_enabled_buttons(window.enabled_buttons)); } - } - if window.position != cache.window.position { - if let Some(position) = crate::winit_window_position( - &window.position, - &window.resolution, - winit_window.available_monitors(), - winit_window.primary_monitor(), - winit_window.current_monitor(), - ) { - let should_set = match winit_window.outer_position() { - Ok(current_position) => current_position != position, - _ => true, + if window.resize_constraints != cache.window.resize_constraints { + let constraints = window.resize_constraints.check_constraints(); + let min_inner_size = LogicalSize { + width: constraints.min_width, + height: constraints.min_height, + }; + let max_inner_size = LogicalSize { + width: constraints.max_width, + height: constraints.max_height, }; - if should_set { - winit_window.set_outer_position(position); + winit_window.set_min_inner_size(Some(min_inner_size)); + if constraints.max_width.is_finite() && constraints.max_height.is_finite() { + winit_window.set_max_inner_size(Some(max_inner_size)); } } - } - if let Some(maximized) = window.internal.take_maximize_request() { - winit_window.set_maximized(maximized); - } + if window.position != cache.window.position { + if let Some(position) = crate::winit_window_position( + &window.position, + &window.resolution, + winit_window.available_monitors(), + winit_window.primary_monitor(), + winit_window.current_monitor(), + ) { + let should_set = match winit_window.outer_position() { + Ok(current_position) => current_position != position, + _ => true, + }; + + if should_set { + winit_window.set_outer_position(position); + } + } + } - if let Some(minimized) = window.internal.take_minimize_request() { - winit_window.set_minimized(minimized); - } + if let Some(maximized) = window.internal.take_maximize_request() { + winit_window.set_maximized(maximized); + } - if window.focused != cache.window.focused && window.focused { - winit_window.focus_window(); - } + if let Some(minimized) = window.internal.take_minimize_request() { + winit_window.set_minimized(minimized); + } - if window.window_level != cache.window.window_level { - winit_window.set_window_level(convert_window_level(window.window_level)); - } + if window.focused != cache.window.focused && window.focused { + winit_window.focus_window(); + } - // Currently unsupported changes - if window.transparent != cache.window.transparent { - window.transparent = cache.window.transparent; - warn!( - "Winit does not currently support updating transparency after window creation." - ); - } + if window.window_level != cache.window.window_level { + winit_window.set_window_level(convert_window_level(window.window_level)); + } - #[cfg(target_arch = "wasm32")] - if window.canvas != cache.window.canvas { - window.canvas = cache.window.canvas.clone(); - warn!( - "Bevy currently doesn't support modifying the window canvas after initialization." - ); - } + if window.transparent != cache.window.transparent { + window.transparent = cache.window.transparent; + warn!( + "Currently, `winit` does not support changing a window's transparency after it's been created." + ); + } - if window.ime_enabled != cache.window.ime_enabled { - winit_window.set_ime_allowed(window.ime_enabled); - } + #[cfg(target_arch = "wasm32")] + if window.canvas != cache.window.canvas { + window.canvas = cache.window.canvas.clone(); + warn!( + "Currently, `winit` does not support changing a window's canvas after it's been created." + ); + } - if window.ime_position != cache.window.ime_position { - winit_window.set_ime_position(LogicalPosition::new( - window.ime_position.x, - window.ime_position.y, - )); - } + if window.ime_enabled != cache.window.ime_enabled { + winit_window.set_ime_allowed(window.ime_enabled); + } - if window.window_theme != cache.window.window_theme { - winit_window.set_theme(window.window_theme.map(convert_window_theme)); - } + if window.ime_position != cache.window.ime_position { + winit_window.set_ime_position(LogicalPosition::new( + window.ime_position.x, + window.ime_position.y, + )); + } + + if window.window_theme != cache.window.window_theme { + winit_window.set_theme(window.window_theme.map(convert_window_theme)); + } - if window.visible != cache.window.visible { - winit_window.set_visible(window.visible); + cache.window = window.clone(); } - cache.window = window.clone(); + #[cfg(not(target_arch = "wasm32"))] + if let Some(window_id) = winit_windows.get_window_id(entity) { + winit_windows.cached_windows.insert(window_id, window.clone()); + } } - } + }); } diff --git a/crates/bevy_winit/src/web_resize.rs b/crates/bevy_winit/src/web_resize.rs index a53075dea38837..e2acf22111fe71 100644 --- a/crates/bevy_winit/src/web_resize.rs +++ b/crates/bevy_winit/src/web_resize.rs @@ -26,14 +26,17 @@ pub(crate) struct CanvasParentResizeEventChannel { } fn canvas_parent_resize_event_handler( - winit_windows: NonSend, + local_thread: ThreadLocal, resize_events: Res, ) { - for event in resize_events.receiver.try_iter() { - if let Some(window) = winit_windows.get_window(event.window) { - window.set_inner_size(event.size); + local_thread.run(|tls| { + let winit_windows = tls.resource::(); + for event in resize_events.receiver.try_iter() { + if let Some(window) = winit_windows.get_window(event.window) { + window.set_inner_size(event.size); + } } - } + }); } fn get_size(selector: &str) -> Option> { diff --git a/crates/bevy_winit/src/winit_config.rs b/crates/bevy_winit/src/winit_config.rs index 89f67707a0dae7..639a789be0adf2 100644 --- a/crates/bevy_winit/src/winit_config.rs +++ b/crates/bevy_winit/src/winit_config.rs @@ -70,10 +70,10 @@ impl WinitSettings { /// Returns the current [`UpdateMode`]. /// /// **Note:** The output depends on whether the window has focus or not. - pub fn update_mode(&self, focused: bool) -> &UpdateMode { + pub fn update_mode(&self, focused: bool) -> UpdateMode { match focused { - true => &self.focused_mode, - false => &self.unfocused_mode, + true => self.focused_mode, + false => self.unfocused_mode, } } } @@ -88,7 +88,6 @@ impl Default for WinitSettings { } } -#[allow(clippy::doc_markdown)] /// Determines how frequently an [`App`](bevy_app::App) should update. /// /// **Note:** This setting is independent of VSync. VSync is controlled by a window's diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 3ca88ed47ab0a0..5e95bd7d63718c 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -1,4 +1,5 @@ #![warn(missing_docs)] +use std::marker::PhantomData; use std::sync::atomic::Ordering; use accesskit_winit::Adapter; @@ -7,12 +8,13 @@ use bevy_a11y::{ AccessKitEntityExt, AccessibilityRequested, }; use bevy_ecs::entity::Entity; - +use bevy_ecs::prelude::{Resource, ThreadLocalResource}; use bevy_utils::{tracing::warn, HashMap}; use bevy_window::{CursorGrabMode, Window, WindowMode, WindowPosition, WindowResolution}; use winit::{ dpi::{LogicalSize, PhysicalPosition}, + event_loop::EventLoopWindowTarget, monitor::MonitorHandle, }; @@ -21,29 +23,57 @@ use crate::{ converters::{convert_enabled_buttons, convert_window_level, convert_window_theme}, }; -/// A resource mapping window entities to their `winit`-backend [`Window`](winit::window::Window) -/// states. -#[derive(Debug, Default)] -pub struct WinitWindows { - /// Stores [`winit`] windows by window identifier. - pub windows: HashMap, +/// A two-way map between [`Window`] entities and [`winit`] library [`Window`](winit::window::Window) instances. +#[derive(Resource, Debug, Default)] +pub struct WinitWindowEntityMap { /// Maps entities to `winit` window identifiers. pub entity_to_winit: HashMap, /// Maps `winit` window identifiers to entities. pub winit_to_entity: HashMap, +} + +impl WinitWindowEntityMap { + /// Returns the [`WindowId`](winit::window::WindowId) that is mapped to the given [`Entity`]. + pub fn get_window_id(&self, entity: Entity) -> Option { + self.entity_to_winit.get(&entity).cloned() + } + + /// Returns the [`Entity`] that is mapped to the given [`WindowId`](winit::window::WindowId). + pub fn get_window_entity(&self, winit_id: winit::window::WindowId) -> Option { + self.winit_to_entity.get(&winit_id).cloned() + } +} + +/// Collection of `winit` [`Window`](winit::window::Window) instances. +#[derive(ThreadLocalResource, Debug, Default)] +pub struct WinitWindows { + /// Collection of [`winit`] windows indexed by [`WindowId`](winit::window::WindowId). + pub windows: HashMap, + /// Two-way mapping between [`Entity`] and [`WindowId`](winit::window::WindowId). + map: WinitWindowEntityMap, + /// Cached copy of the last-known [`Window`](bevy_window::Window) properties. + /// + /// This copy is needed because some `winit` events require immediate handling, but the [`App`] + /// lives in a different thread. + #[cfg(not(target_arch = "wasm32"))] + pub(crate) cached_windows: 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`. - _not_send_sync: core::marker::PhantomData<*const ()>, + _not_send_sync: PhantomData<*const ()>, } impl WinitWindows { - /// Creates a `winit` window and associates it with our entity. - pub fn create_window( + /// Constructs a new [`Window`](winit::window::Window) and returns a reference to it. + /// + /// Due to platform limitations, this function can only run on the main thread. + #[allow(clippy::too_many_arguments)] + pub fn create_window( &mut self, - event_loop: &winit::event_loop::EventLoopWindowTarget<()>, + event_loop: &EventLoopWindowTarget, entity: Entity, window: &Window, + entity_map: &mut WinitWindowEntityMap, adapters: &mut AccessKitAdapters, handlers: &mut WinitActionHandlers, accessibility_requested: &AccessibilityRequested, @@ -166,6 +196,7 @@ impl WinitWindows { }, Box::new(handler.clone()), ); + adapters.insert(entity, adapter); handlers.insert(entity, handler); @@ -187,8 +218,15 @@ impl WinitWindows { } } - self.entity_to_winit.insert(entity, winit_window.id()); - self.winit_to_entity.insert(winit_window.id(), entity); + self.map.entity_to_winit.insert(entity, winit_window.id()); + self.map.winit_to_entity.insert(winit_window.id(), entity); + + // save copy of window properties so we don't have to synchronize the threads to read them + #[cfg(not(target_arch = "wasm32"))] + { + entity_map.entity_to_winit = self.map.entity_to_winit.clone(); + entity_map.winit_to_entity = self.map.winit_to_entity.clone(); + } #[cfg(target_arch = "wasm32")] { @@ -212,26 +250,35 @@ impl WinitWindows { .into_mut() } - /// Get the winit window that is associated with our entity. - pub fn get_window(&self, entity: Entity) -> Option<&winit::window::Window> { - self.entity_to_winit - .get(&entity) - .and_then(|winit_id| self.windows.get(winit_id)) + /// Returns the [`WindowId`](winit::window::WindowId) that is mapped to the given [`Entity`]. + pub fn get_window_id(&self, entity: Entity) -> Option { + self.map.get_window_id(entity) } - /// Get the entity associated with the winit window id. - /// - /// This is mostly just an intermediary step between us and winit. + /// Returns the [`Entity`] that is mapped to the given [`WindowId`](winit::window::WindowId). pub fn get_window_entity(&self, winit_id: winit::window::WindowId) -> Option { - self.winit_to_entity.get(&winit_id).cloned() + self.map.get_window_entity(winit_id) + } + + /// Returns the [`Window`](winit::window::Window) that is mapped to the given [`Entity`]. + pub fn get_window(&self, entity: Entity) -> Option<&winit::window::Window> { + self.map + .get_window_id(entity) + .and_then(|id| self.windows.get(&id)) } /// 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 { - let winit_id = self.entity_to_winit.remove(&entity)?; - // Don't remove from `winit_to_window_id` so we know the window used to exist. + let winit_id = self.map.entity_to_winit.remove(&entity)?; + + #[cfg(not(target_arch = "wasm32"))] + self.cached_windows.remove(&winit_id); + + // Don't remove from `winit_to_window_id` so we don't forget that the window existed. + // TODO: Investigate the reasoning here. + // self.map.winit_to_entity.remove(&winit_id)?; self.windows.remove(&winit_id) } } @@ -271,7 +318,7 @@ pub fn get_fitting_videomode( modes.first().unwrap().clone() } -/// Gets the "best" videomode from a monitor. +/// Returns the "best" videomode from a monitor. /// /// The heuristic for "best" prioritizes width, height, and refresh rate in that order. pub fn get_best_videomode(monitor: &winit::monitor::MonitorHandle) -> winit::monitor::VideoMode { @@ -315,9 +362,8 @@ pub(crate) fn attempt_grab(winit_window: &winit::window::Window, grab_mode: Curs } } -/// Compute the physical window position for a given [`WindowPosition`]. -// Ideally we could generify this across window backends, but we only really have winit atm -// so whatever. +/// Computes the physical window position for a given [`WindowPosition`]. +// TODO: Ideally, this function is backend-generic, but right now we only internally support winit. pub fn winit_window_position( position: &WindowPosition, resolution: &WindowResolution, @@ -374,9 +420,3 @@ pub fn winit_window_position( } } } - -// WARNING: this only works under the assumption that wasm runtime is single threaded -#[cfg(target_arch = "wasm32")] -unsafe impl Send for WinitWindows {} -#[cfg(target_arch = "wasm32")] -unsafe impl Sync for WinitWindows {}