
# Objective Fixes two issues related to #13208. First, we ensure render resources for a window are always dropped first to ensure that the `winit::Window` always drops on the main thread when it is removed from `WinitWindows`. Previously, changes in #12978 caused the window to drop in the render world, causing issues. We accomplish this by delaying despawning the window by a frame by inserting a marker component `ClosingWindow` that indicates the window has been requested to close and is in the process of closing. The render world now responds to the equivalent `WindowClosing` event rather than `WindowCloseed` which now fires after the render resources are guarunteed to be cleaned up. Secondly, fixing the above caused (revealed?) that additional events were being delivered to the the event loop handler after exit had already been requested: in my testing `RedrawRequested` and `LoopExiting`. This caused errors to be reported try to send an exit event on the close channel. There are two options here: - Guard the handler so no additional events are delivered once the app is exiting. I ~considered this but worried it might be confusing or bug prone if in the future someone wants to handle `LoopExiting` or some other event to clean-up while exiting.~ We are now taking this approach. - Only send an exit signal if we are not already exiting. ~It doesn't appear to cause any problems to handle the extra events so this seems safer.~ Fixing this also appears to have fixed #13231. Fixes #10260. ## Testing Tested on mac only. --- ## Changelog ### Added - A `WindowClosing` event has been added that indicates the window will be despawned on the next frame. ### Changed - Windows now close a frame after their exit has been requested. ## Migration Guide - Ensure custom exit logic does not rely on the app exiting the same frame as a window is closed.
841 lines
34 KiB
Rust
841 lines
34 KiB
Rust
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
#![forbid(unsafe_code)]
|
|
#![doc(
|
|
html_logo_url = "https://bevyengine.org/assets/icon.png",
|
|
html_favicon_url = "https://bevyengine.org/assets/icon.png"
|
|
)]
|
|
|
|
//! `bevy_winit` provides utilities to handle window creation and the eventloop 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`].
|
|
//! See `winit_runner` for details.
|
|
|
|
pub mod accessibility;
|
|
mod converters;
|
|
mod system;
|
|
mod winit_config;
|
|
pub mod winit_event;
|
|
mod winit_windows;
|
|
|
|
use std::sync::mpsc::{sync_channel, SyncSender};
|
|
|
|
use approx::relative_eq;
|
|
use bevy_a11y::AccessibilityRequested;
|
|
use bevy_utils::Instant;
|
|
pub use system::create_windows;
|
|
use system::{changed_windows, despawn_windows, CachedWindow};
|
|
use winit::dpi::{LogicalSize, PhysicalSize};
|
|
pub use winit_config::*;
|
|
pub use winit_event::*;
|
|
pub use winit_windows::*;
|
|
|
|
use bevy_app::{App, AppExit, Last, Plugin, PluginsState};
|
|
use bevy_ecs::event::ManualEventReader;
|
|
use bevy_ecs::prelude::*;
|
|
use bevy_ecs::system::SystemState;
|
|
use bevy_input::{
|
|
mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel},
|
|
touchpad::{TouchpadMagnify, TouchpadRotate},
|
|
};
|
|
use bevy_math::{ivec2, DVec2, Vec2};
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
use bevy_tasks::tick_global_task_pools_on_main_thread;
|
|
use bevy_utils::tracing::{error, trace, warn};
|
|
#[allow(deprecated)]
|
|
use bevy_window::{
|
|
exit_on_all_closed, ApplicationLifetime, CursorEntered, CursorLeft, CursorMoved,
|
|
FileDragAndDrop, Ime, ReceivedCharacter, RequestRedraw, Window,
|
|
WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, WindowDestroyed,
|
|
WindowFocused, WindowMoved, WindowOccluded, WindowResized, WindowScaleFactorChanged,
|
|
WindowThemeChanged,
|
|
};
|
|
#[cfg(target_os = "android")]
|
|
use bevy_window::{PrimaryWindow, RawHandleWrapper};
|
|
|
|
#[cfg(target_os = "android")]
|
|
pub use winit::platform::android::activity as android_activity;
|
|
|
|
use winit::event::StartCause;
|
|
use winit::{
|
|
event::{self, DeviceEvent, Event, WindowEvent},
|
|
event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopWindowTarget},
|
|
};
|
|
|
|
use crate::accessibility::{AccessKitAdapters, AccessKitPlugin, WinitActionHandlers};
|
|
|
|
use crate::converters::convert_winit_theme;
|
|
|
|
/// [`AndroidApp`] provides an interface to query the application state as well as monitor events
|
|
/// (for example lifecycle and input events).
|
|
#[cfg(target_os = "android")]
|
|
pub static ANDROID_APP: std::sync::OnceLock<android_activity::AndroidApp> =
|
|
std::sync::OnceLock::new();
|
|
|
|
/// A [`Plugin`] that uses `winit` to create and manage windows, and receive window and input
|
|
/// events.
|
|
///
|
|
/// This plugin will add systems and resources that sync with the `winit` backend and also
|
|
/// replace the existing [`App`] runner with one that constructs an [event loop](EventLoop) to
|
|
/// receive window and input events from the OS.
|
|
#[derive(Default)]
|
|
pub struct WinitPlugin {
|
|
/// Allows the window (and the event loop) to be created on any thread
|
|
/// instead of only the main thread.
|
|
///
|
|
/// See [`EventLoopBuilder::build`] for more information on this.
|
|
///
|
|
/// # Supported platforms
|
|
///
|
|
/// Only works on Linux (X11/Wayland) and Windows.
|
|
/// This field is ignored on other platforms.
|
|
pub run_on_any_thread: bool,
|
|
}
|
|
|
|
impl Plugin for WinitPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
let mut event_loop_builder = EventLoopBuilder::<UserEvent>::with_user_event();
|
|
|
|
// linux check is needed because x11 might be enabled on other platforms.
|
|
#[cfg(all(target_os = "linux", feature = "x11"))]
|
|
{
|
|
use winit::platform::x11::EventLoopBuilderExtX11;
|
|
|
|
// This allows a Bevy app to be started and ran outside of the main thread.
|
|
// A use case for this is to allow external applications to spawn a thread
|
|
// which runs a Bevy app without requiring the Bevy app to need to reside on
|
|
// the main thread, which can be problematic.
|
|
event_loop_builder.with_any_thread(self.run_on_any_thread);
|
|
}
|
|
|
|
// linux check is needed because wayland might be enabled on other platforms.
|
|
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
|
{
|
|
use winit::platform::wayland::EventLoopBuilderExtWayland;
|
|
event_loop_builder.with_any_thread(self.run_on_any_thread);
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
use winit::platform::windows::EventLoopBuilderExtWindows;
|
|
event_loop_builder.with_any_thread(self.run_on_any_thread);
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
use winit::platform::android::EventLoopBuilderExtAndroid;
|
|
let msg = "Bevy must be setup with the #[bevy_main] macro on Android";
|
|
event_loop_builder.with_android_app(ANDROID_APP.get().expect(msg).clone());
|
|
}
|
|
|
|
app.init_non_send_resource::<WinitWindows>()
|
|
.init_resource::<WinitSettings>()
|
|
.add_event::<WinitEvent>()
|
|
.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`
|
|
changed_windows.ambiguous_with(exit_on_all_closed),
|
|
despawn_windows,
|
|
)
|
|
.chain(),
|
|
);
|
|
|
|
app.add_plugins(AccessKitPlugin);
|
|
|
|
let event_loop = event_loop_builder
|
|
.build()
|
|
.expect("Failed to build event loop");
|
|
|
|
// iOS, macOS, and Android don't like it if you create windows before the event loop is
|
|
// initialized.
|
|
//
|
|
// See:
|
|
// - https://github.com/rust-windowing/winit/blob/master/README.md#macos
|
|
// - https://github.com/rust-windowing/winit/blob/master/README.md#ios
|
|
#[cfg(not(any(target_os = "android", target_os = "ios", target_os = "macos")))]
|
|
{
|
|
// Otherwise, we want to create a window before `bevy_render` initializes the renderer
|
|
// so that we have a surface to use as a hint. This improves compatibility with `wgpu`
|
|
// backends, especially WASM/WebGL2.
|
|
let mut create_window = SystemState::<CreateWindowParams>::from_world(app.world_mut());
|
|
create_windows(&event_loop, create_window.get_mut(app.world_mut()));
|
|
create_window.apply(app.world_mut());
|
|
}
|
|
|
|
// `winit`'s windows are bound to the event loop that created them, so the event loop must
|
|
// be inserted as a resource here to pass it onto the runner.
|
|
app.insert_non_send_resource(event_loop);
|
|
}
|
|
}
|
|
|
|
trait AppSendEvent {
|
|
fn send(&mut self, event: impl Into<WinitEvent>);
|
|
}
|
|
impl AppSendEvent for Vec<WinitEvent> {
|
|
fn send(&mut self, event: impl Into<WinitEvent>) {
|
|
self.push(Into::<WinitEvent>::into(event));
|
|
}
|
|
}
|
|
|
|
/// Persistent state that is used to run the [`App`] according to the current
|
|
/// [`UpdateMode`].
|
|
struct WinitAppRunnerState {
|
|
/// Current activity state of the app.
|
|
activity_state: UpdateState,
|
|
/// Current update mode of the app.
|
|
update_mode: UpdateMode,
|
|
/// Is `true` if a new [`WindowEvent`] has been received since the last update.
|
|
window_event_received: bool,
|
|
/// Is `true` if a new [`DeviceEvent`] has been received since the last update.
|
|
device_event_received: bool,
|
|
/// Is `true` if the app has requested a redraw since the last update.
|
|
redraw_requested: bool,
|
|
/// Is `true` if enough time has elapsed since `last_update` to run another update.
|
|
wait_elapsed: bool,
|
|
/// Number of "forced" updates to trigger on application start
|
|
startup_forced_updates: u32,
|
|
}
|
|
|
|
impl WinitAppRunnerState {
|
|
fn reset_on_update(&mut self) {
|
|
self.window_event_received = false;
|
|
self.device_event_received = false;
|
|
}
|
|
}
|
|
|
|
impl Default for WinitAppRunnerState {
|
|
fn default() -> Self {
|
|
Self {
|
|
activity_state: UpdateState::NotYetStarted,
|
|
update_mode: UpdateMode::Continuous,
|
|
window_event_received: false,
|
|
device_event_received: false,
|
|
redraw_requested: false,
|
|
wait_elapsed: false,
|
|
// 3 seems to be enough, 5 is a safe margin
|
|
startup_forced_updates: 5,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, Debug)]
|
|
enum UpdateState {
|
|
NotYetStarted,
|
|
Active,
|
|
Suspended,
|
|
WillSuspend,
|
|
WillResume,
|
|
}
|
|
|
|
impl UpdateState {
|
|
#[inline]
|
|
fn is_active(&self) -> bool {
|
|
match self {
|
|
Self::NotYetStarted | Self::Suspended => false,
|
|
Self::Active | Self::WillSuspend | Self::WillResume => true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The parameters of the [`create_windows`] system.
|
|
pub type CreateWindowParams<'w, 's, F = ()> = (
|
|
Commands<'w, 's>,
|
|
Query<'w, 's, (Entity, &'static mut Window), F>,
|
|
EventWriter<'w, WindowCreated>,
|
|
NonSendMut<'w, WinitWindows>,
|
|
NonSendMut<'w, AccessKitAdapters>,
|
|
ResMut<'w, WinitActionHandlers>,
|
|
Res<'w, AccessibilityRequested>,
|
|
);
|
|
|
|
/// The [`winit::event_loop::EventLoopProxy`] with the specific [`winit::event::Event::UserEvent`] used in the [`winit_runner`].
|
|
///
|
|
/// The `EventLoopProxy` can be used to request a redraw from outside bevy.
|
|
///
|
|
/// Use `NonSend<EventLoopProxy>` to receive this resource.
|
|
pub type EventLoopProxy = winit::event_loop::EventLoopProxy<UserEvent>;
|
|
|
|
type UserEvent = RequestRedraw;
|
|
|
|
/// The default [`App::runner`] for the [`WinitPlugin`] plugin.
|
|
///
|
|
/// Overriding the app's [runner](bevy_app::App::runner) while using `WinitPlugin` will bypass the
|
|
/// `EventLoop`.
|
|
pub fn winit_runner(mut app: App) -> AppExit {
|
|
if app.plugins_state() == PluginsState::Ready {
|
|
app.finish();
|
|
app.cleanup();
|
|
}
|
|
|
|
let event_loop = app
|
|
.world_mut()
|
|
.remove_non_send_resource::<EventLoop<UserEvent>>()
|
|
.unwrap();
|
|
|
|
app.world_mut()
|
|
.insert_non_send_resource(event_loop.create_proxy());
|
|
|
|
let mut runner_state = WinitAppRunnerState::default();
|
|
|
|
// Create a channel with a size of 1, since ideally only one exit code will be sent before exiting the app.
|
|
let (exit_sender, exit_receiver) = sync_channel(1);
|
|
|
|
// prepare structures to access data in the world
|
|
let mut redraw_event_reader = ManualEventReader::<RequestRedraw>::default();
|
|
|
|
let mut focused_windows_state: SystemState<(Res<WinitSettings>, Query<(Entity, &Window)>)> =
|
|
SystemState::new(app.world_mut());
|
|
|
|
let mut event_writer_system_state: SystemState<(
|
|
EventWriter<WindowResized>,
|
|
NonSend<WinitWindows>,
|
|
Query<(&mut Window, &mut CachedWindow)>,
|
|
NonSend<AccessKitAdapters>,
|
|
)> = SystemState::new(app.world_mut());
|
|
|
|
let mut create_window =
|
|
SystemState::<CreateWindowParams<Added<Window>>>::from_world(app.world_mut());
|
|
let mut winit_events = Vec::default();
|
|
// set up the event loop
|
|
let event_handler = move |event, event_loop: &EventLoopWindowTarget<UserEvent>| {
|
|
// The event loop is in the process of exiting, so don't deliver any new events
|
|
if event_loop.exiting() {
|
|
return;
|
|
}
|
|
|
|
handle_winit_event(
|
|
&mut app,
|
|
&mut runner_state,
|
|
&mut create_window,
|
|
&mut event_writer_system_state,
|
|
&mut focused_windows_state,
|
|
&mut redraw_event_reader,
|
|
&mut winit_events,
|
|
&exit_sender,
|
|
event,
|
|
event_loop,
|
|
);
|
|
};
|
|
|
|
trace!("starting winit event loop");
|
|
// TODO(clean): the winit docs mention using `spawn` instead of `run` on WASM.
|
|
if let Err(err) = event_loop.run(event_handler) {
|
|
error!("winit event loop returned an error: {err}");
|
|
}
|
|
|
|
// If everything is working correctly then the event loop only exits after it's sent a exit code.
|
|
exit_receiver
|
|
.try_recv()
|
|
.map_err(|err| error!("Failed to receive a app exit code! This is a bug. Reason: {err}"))
|
|
.unwrap_or(AppExit::error())
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments /* TODO: probs can reduce # of args */)]
|
|
fn handle_winit_event(
|
|
app: &mut App,
|
|
runner_state: &mut WinitAppRunnerState,
|
|
create_window: &mut SystemState<CreateWindowParams<Added<Window>>>,
|
|
event_writer_system_state: &mut SystemState<(
|
|
EventWriter<WindowResized>,
|
|
NonSend<WinitWindows>,
|
|
Query<(&mut Window, &mut CachedWindow)>,
|
|
NonSend<AccessKitAdapters>,
|
|
)>,
|
|
focused_windows_state: &mut SystemState<(Res<WinitSettings>, Query<(Entity, &Window)>)>,
|
|
redraw_event_reader: &mut ManualEventReader<RequestRedraw>,
|
|
winit_events: &mut Vec<WinitEvent>,
|
|
exit_notify: &SyncSender<AppExit>,
|
|
event: Event<UserEvent>,
|
|
event_loop: &EventLoopWindowTarget<UserEvent>,
|
|
) {
|
|
#[cfg(feature = "trace")]
|
|
let _span = bevy_utils::tracing::info_span!("winit event_handler").entered();
|
|
|
|
if app.plugins_state() != PluginsState::Cleaned {
|
|
if app.plugins_state() != PluginsState::Ready {
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
tick_global_task_pools_on_main_thread();
|
|
} else {
|
|
app.finish();
|
|
app.cleanup();
|
|
}
|
|
runner_state.redraw_requested = true;
|
|
}
|
|
|
|
// create any new windows
|
|
// (even if app did not update, some may have been created by plugin setup)
|
|
create_windows(event_loop, create_window.get_mut(app.world_mut()));
|
|
create_window.apply(app.world_mut());
|
|
|
|
match event {
|
|
Event::AboutToWait => {
|
|
if let Some(app_redraw_events) = app.world().get_resource::<Events<RequestRedraw>>() {
|
|
if redraw_event_reader.read(app_redraw_events).last().is_some() {
|
|
runner_state.redraw_requested = true;
|
|
}
|
|
}
|
|
|
|
let (config, windows) = focused_windows_state.get(app.world());
|
|
let focused = windows.iter().any(|(_, window)| window.focused);
|
|
|
|
let mut update_mode = config.update_mode(focused);
|
|
let mut should_update = should_update(runner_state, update_mode);
|
|
|
|
if runner_state.startup_forced_updates > 0 {
|
|
runner_state.startup_forced_updates -= 1;
|
|
// Ensure that an update is triggered on the first iterations for app initialization
|
|
should_update = true;
|
|
}
|
|
|
|
if runner_state.activity_state == UpdateState::WillSuspend {
|
|
runner_state.activity_state = UpdateState::Suspended;
|
|
// Trigger one last update to enter the suspended state
|
|
should_update = true;
|
|
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
// Remove the `RawHandleWrapper` from the primary window.
|
|
// This will trigger the surface destruction.
|
|
let mut query = app
|
|
.world_mut()
|
|
.query_filtered::<Entity, With<PrimaryWindow>>();
|
|
let entity = query.single(&app.world());
|
|
app.world_mut()
|
|
.entity_mut(entity)
|
|
.remove::<RawHandleWrapper>();
|
|
}
|
|
}
|
|
|
|
if runner_state.activity_state == UpdateState::WillResume {
|
|
runner_state.activity_state = UpdateState::Active;
|
|
// Trigger the update to enter the active state
|
|
should_update = true;
|
|
// Trigger the next redraw ro refresh the screen immediately
|
|
runner_state.redraw_requested = true;
|
|
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
// Get windows that are cached but without raw handles. Those window were already created, but got their
|
|
// handle wrapper removed when the app was suspended.
|
|
let mut query = app
|
|
.world_mut()
|
|
.query_filtered::<(Entity, &Window), (With<CachedWindow>, Without<bevy_window::RawHandleWrapper>)>();
|
|
if let Ok((entity, window)) = query.get_single(&app.world()) {
|
|
let window = window.clone();
|
|
|
|
let (
|
|
..,
|
|
mut winit_windows,
|
|
mut adapters,
|
|
mut handlers,
|
|
accessibility_requested,
|
|
) = create_window.get_mut(app.world_mut());
|
|
|
|
let winit_window = winit_windows.create_window(
|
|
event_loop,
|
|
entity,
|
|
&window,
|
|
&mut adapters,
|
|
&mut handlers,
|
|
&accessibility_requested,
|
|
);
|
|
|
|
let wrapper = RawHandleWrapper::new(winit_window).unwrap();
|
|
|
|
app.world_mut().entity_mut(entity).insert(wrapper);
|
|
}
|
|
}
|
|
}
|
|
|
|
// This is recorded before running app.update(), to run the next cycle after a correct timeout.
|
|
// If the cycle takes more than the wait timeout, it will be re-executed immediately.
|
|
let begin_frame_time = Instant::now();
|
|
|
|
if should_update {
|
|
// Not redrawing, but the timeout elapsed.
|
|
run_app_update(runner_state, app, winit_events);
|
|
|
|
// Running the app may have changed the WinitSettings resource, so we have to re-extract it.
|
|
let (config, windows) = focused_windows_state.get(app.world());
|
|
let focused = windows.iter().any(|(_, window)| window.focused);
|
|
|
|
update_mode = config.update_mode(focused);
|
|
}
|
|
|
|
match update_mode {
|
|
UpdateMode::Continuous => {
|
|
// per winit's docs on [Window::is_visible](https://docs.rs/winit/latest/winit/window/struct.Window.html#method.is_visible),
|
|
// we cannot use the visibility to drive rendering on these platforms
|
|
// so we cannot discern whether to beneficially use `Poll` or not?
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(not(any(
|
|
target_arch = "wasm32",
|
|
target_os = "android",
|
|
target_os = "ios",
|
|
all(target_os = "linux", any(feature = "x11", feature = "wayland"))
|
|
)))]
|
|
{
|
|
let winit_windows = app.world().non_send_resource::<WinitWindows>();
|
|
let visible = winit_windows.windows.iter().any(|(_, w)| {
|
|
w.is_visible().unwrap_or(false)
|
|
});
|
|
|
|
event_loop.set_control_flow(if visible {
|
|
ControlFlow::Wait
|
|
} else {
|
|
ControlFlow::Poll
|
|
});
|
|
}
|
|
else {
|
|
event_loop.set_control_flow(ControlFlow::Wait);
|
|
}
|
|
}
|
|
|
|
// Trigger the next redraw to refresh the screen immediately if waiting
|
|
if let ControlFlow::Wait = event_loop.control_flow() {
|
|
runner_state.redraw_requested = true;
|
|
}
|
|
}
|
|
UpdateMode::Reactive { wait } | UpdateMode::ReactiveLowPower { wait } => {
|
|
// Set the next timeout, starting from the instant before running app.update() to avoid frame delays
|
|
if let Some(next) = begin_frame_time.checked_add(wait) {
|
|
if runner_state.wait_elapsed {
|
|
event_loop.set_control_flow(ControlFlow::WaitUntil(next));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if update_mode != runner_state.update_mode {
|
|
// Trigger the next redraw since we're changing the update mode
|
|
runner_state.redraw_requested = true;
|
|
runner_state.update_mode = update_mode;
|
|
}
|
|
|
|
if runner_state.redraw_requested
|
|
&& runner_state.activity_state != UpdateState::Suspended
|
|
{
|
|
let winit_windows = app.world().non_send_resource::<WinitWindows>();
|
|
for window in winit_windows.windows.values() {
|
|
window.request_redraw();
|
|
}
|
|
runner_state.redraw_requested = false;
|
|
}
|
|
}
|
|
Event::NewEvents(cause) => {
|
|
runner_state.wait_elapsed = match cause {
|
|
StartCause::WaitCancelled {
|
|
requested_resume: Some(resume),
|
|
..
|
|
} => {
|
|
// If the resume time is not after now, it means that at least the wait timeout
|
|
// has elapsed.
|
|
resume <= Instant::now()
|
|
}
|
|
_ => true,
|
|
};
|
|
}
|
|
Event::WindowEvent {
|
|
event, window_id, ..
|
|
} => {
|
|
let (mut window_resized, winit_windows, mut windows, access_kit_adapters) =
|
|
event_writer_system_state.get_mut(app.world_mut());
|
|
|
|
let Some(window) = winit_windows.get_window_entity(window_id) else {
|
|
warn!("Skipped event {event:?} for unknown winit Window Id {window_id:?}");
|
|
return;
|
|
};
|
|
|
|
let Ok((mut win, _)) = windows.get_mut(window) else {
|
|
warn!("Window {window:?} is missing `Window` component, skipping event {event:?}");
|
|
return;
|
|
};
|
|
|
|
// Allow AccessKit to respond to `WindowEvent`s before they reach
|
|
// the engine.
|
|
if let Some(adapter) = access_kit_adapters.get(&window) {
|
|
if let Some(winit_window) = winit_windows.get_window(window) {
|
|
adapter.process_event(winit_window, &event);
|
|
}
|
|
}
|
|
|
|
runner_state.window_event_received = true;
|
|
|
|
match event {
|
|
WindowEvent::Resized(size) => {
|
|
react_to_resize(&mut win, size, &mut window_resized, window);
|
|
}
|
|
WindowEvent::CloseRequested => winit_events.send(WindowCloseRequested { window }),
|
|
WindowEvent::KeyboardInput { ref event, .. } => {
|
|
if event.state.is_pressed() {
|
|
if let Some(char) = &event.text {
|
|
let char = char.clone();
|
|
#[allow(deprecated)]
|
|
winit_events.send(ReceivedCharacter { window, char });
|
|
}
|
|
}
|
|
winit_events.send(converters::convert_keyboard_input(event, window));
|
|
}
|
|
WindowEvent::CursorMoved { position, .. } => {
|
|
let physical_position = DVec2::new(position.x, position.y);
|
|
|
|
let last_position = win.physical_cursor_position();
|
|
let delta = last_position.map(|last_pos| {
|
|
(physical_position.as_vec2() - last_pos) / win.resolution.scale_factor()
|
|
});
|
|
|
|
win.set_physical_cursor_position(Some(physical_position));
|
|
let position =
|
|
(physical_position / win.resolution.scale_factor() as f64).as_vec2();
|
|
winit_events.send(CursorMoved {
|
|
window,
|
|
position,
|
|
delta,
|
|
});
|
|
}
|
|
WindowEvent::CursorEntered { .. } => {
|
|
winit_events.send(CursorEntered { window });
|
|
}
|
|
WindowEvent::CursorLeft { .. } => {
|
|
win.set_physical_cursor_position(None);
|
|
winit_events.send(CursorLeft { window });
|
|
}
|
|
WindowEvent::MouseInput { state, button, .. } => {
|
|
winit_events.send(MouseButtonInput {
|
|
button: converters::convert_mouse_button(button),
|
|
state: converters::convert_element_state(state),
|
|
window,
|
|
});
|
|
}
|
|
WindowEvent::TouchpadMagnify { delta, .. } => {
|
|
winit_events.send(TouchpadMagnify(delta as f32));
|
|
}
|
|
WindowEvent::TouchpadRotate { delta, .. } => {
|
|
winit_events.send(TouchpadRotate(delta));
|
|
}
|
|
WindowEvent::MouseWheel { delta, .. } => match delta {
|
|
event::MouseScrollDelta::LineDelta(x, y) => {
|
|
winit_events.send(MouseWheel {
|
|
unit: MouseScrollUnit::Line,
|
|
x,
|
|
y,
|
|
window,
|
|
});
|
|
}
|
|
event::MouseScrollDelta::PixelDelta(p) => {
|
|
winit_events.send(MouseWheel {
|
|
unit: MouseScrollUnit::Pixel,
|
|
x: p.x as f32,
|
|
y: p.y as f32,
|
|
window,
|
|
});
|
|
}
|
|
},
|
|
WindowEvent::Touch(touch) => {
|
|
let location = touch
|
|
.location
|
|
.to_logical(win.resolution.scale_factor() as f64);
|
|
winit_events.send(converters::convert_touch_input(touch, location, window));
|
|
}
|
|
WindowEvent::ScaleFactorChanged {
|
|
scale_factor,
|
|
mut inner_size_writer,
|
|
} => {
|
|
let prior_factor = win.resolution.scale_factor();
|
|
win.resolution.set_scale_factor(scale_factor as f32);
|
|
// Note: this may be different from new_scale_factor if
|
|
// `scale_factor_override` is set to Some(thing)
|
|
let new_factor = win.resolution.scale_factor();
|
|
|
|
let mut new_inner_size =
|
|
PhysicalSize::new(win.physical_width(), win.physical_height());
|
|
let scale_factor_override = win.resolution.scale_factor_override();
|
|
if let Some(forced_factor) = scale_factor_override {
|
|
// This window is overriding the OS-suggested DPI, so its physical size
|
|
// should be set based on the overriding value. Its logical size already
|
|
// incorporates any resize constraints.
|
|
let maybe_new_inner_size = LogicalSize::new(win.width(), win.height())
|
|
.to_physical::<u32>(forced_factor as f64);
|
|
if let Err(err) = inner_size_writer.request_inner_size(new_inner_size) {
|
|
warn!("Winit Failed to resize the window: {err}");
|
|
} else {
|
|
new_inner_size = maybe_new_inner_size;
|
|
}
|
|
}
|
|
let new_logical_width = new_inner_size.width as f32 / new_factor;
|
|
let new_logical_height = new_inner_size.height as f32 / new_factor;
|
|
|
|
let width_equal = relative_eq!(win.width(), new_logical_width);
|
|
let height_equal = relative_eq!(win.height(), new_logical_height);
|
|
win.resolution
|
|
.set_physical_resolution(new_inner_size.width, new_inner_size.height);
|
|
|
|
winit_events.send(WindowBackendScaleFactorChanged {
|
|
window,
|
|
scale_factor,
|
|
});
|
|
if scale_factor_override.is_none() && !relative_eq!(new_factor, prior_factor) {
|
|
winit_events.send(WindowScaleFactorChanged {
|
|
window,
|
|
scale_factor,
|
|
});
|
|
}
|
|
|
|
if !width_equal || !height_equal {
|
|
winit_events.send(WindowResized {
|
|
window,
|
|
width: new_logical_width,
|
|
height: new_logical_height,
|
|
});
|
|
}
|
|
}
|
|
WindowEvent::Focused(focused) => {
|
|
win.focused = focused;
|
|
winit_events.send(WindowFocused { window, focused });
|
|
}
|
|
WindowEvent::Occluded(occluded) => {
|
|
winit_events.send(WindowOccluded { window, occluded });
|
|
}
|
|
WindowEvent::DroppedFile(path_buf) => {
|
|
winit_events.send(FileDragAndDrop::DroppedFile { window, path_buf });
|
|
}
|
|
WindowEvent::HoveredFile(path_buf) => {
|
|
winit_events.send(FileDragAndDrop::HoveredFile { window, path_buf });
|
|
}
|
|
WindowEvent::HoveredFileCancelled => {
|
|
winit_events.send(FileDragAndDrop::HoveredFileCanceled { window });
|
|
}
|
|
WindowEvent::Moved(position) => {
|
|
let position = ivec2(position.x, position.y);
|
|
win.position.set(position);
|
|
winit_events.send(WindowMoved { window, position });
|
|
}
|
|
WindowEvent::Ime(event) => match event {
|
|
event::Ime::Preedit(value, cursor) => {
|
|
winit_events.send(Ime::Preedit {
|
|
window,
|
|
value,
|
|
cursor,
|
|
});
|
|
}
|
|
event::Ime::Commit(value) => {
|
|
winit_events.send(Ime::Commit { window, value });
|
|
}
|
|
event::Ime::Enabled => {
|
|
winit_events.send(Ime::Enabled { window });
|
|
}
|
|
event::Ime::Disabled => {
|
|
winit_events.send(Ime::Disabled { window });
|
|
}
|
|
},
|
|
WindowEvent::ThemeChanged(theme) => {
|
|
winit_events.send(WindowThemeChanged {
|
|
window,
|
|
theme: convert_winit_theme(theme),
|
|
});
|
|
}
|
|
WindowEvent::Destroyed => {
|
|
winit_events.send(WindowDestroyed { window });
|
|
}
|
|
WindowEvent::RedrawRequested => {
|
|
run_app_update(runner_state, app, winit_events);
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
let mut windows = app.world_mut().query::<(&mut Window, &mut CachedWindow)>();
|
|
if let Ok((window_component, mut cache)) = windows.get_mut(app.world_mut(), window) {
|
|
if window_component.is_changed() {
|
|
cache.window = window_component.clone();
|
|
}
|
|
}
|
|
}
|
|
Event::DeviceEvent { event, .. } => {
|
|
runner_state.device_event_received = true;
|
|
if let DeviceEvent::MouseMotion { delta: (x, y) } = event {
|
|
let delta = Vec2::new(x as f32, y as f32);
|
|
winit_events.send(MouseMotion { delta });
|
|
}
|
|
}
|
|
Event::Suspended => {
|
|
winit_events.send(ApplicationLifetime::Suspended);
|
|
// Mark the state as `WillSuspend`. This will let the schedule run one last time
|
|
// before actually suspending to let the application react
|
|
runner_state.activity_state = UpdateState::WillSuspend;
|
|
}
|
|
Event::Resumed => {
|
|
match runner_state.activity_state {
|
|
UpdateState::NotYetStarted => winit_events.send(ApplicationLifetime::Started),
|
|
_ => winit_events.send(ApplicationLifetime::Resumed),
|
|
}
|
|
runner_state.activity_state = UpdateState::WillResume;
|
|
}
|
|
Event::UserEvent(RequestRedraw) => {
|
|
runner_state.redraw_requested = true;
|
|
}
|
|
_ => (),
|
|
}
|
|
|
|
if let Some(app_exit) = app.should_exit() {
|
|
if let Err(err) = exit_notify.try_send(app_exit) {
|
|
error!("Failed to send a app exit notification! This is a bug. Reason: {err}");
|
|
};
|
|
event_loop.exit();
|
|
return;
|
|
}
|
|
|
|
// We drain events after every received winit event in addition to on app update to ensure
|
|
// the work of pushing events into event queues is spread out over time in case the app becomes
|
|
// dormant for a long stretch.
|
|
forward_winit_events(winit_events, app);
|
|
}
|
|
|
|
fn should_update(runner_state: &WinitAppRunnerState, update_mode: UpdateMode) -> bool {
|
|
let handle_event = match update_mode {
|
|
UpdateMode::Continuous | UpdateMode::Reactive { .. } => {
|
|
runner_state.wait_elapsed
|
|
|| runner_state.window_event_received
|
|
|| runner_state.device_event_received
|
|
}
|
|
UpdateMode::ReactiveLowPower { .. } => {
|
|
runner_state.wait_elapsed || runner_state.window_event_received
|
|
}
|
|
};
|
|
|
|
handle_event && runner_state.activity_state.is_active()
|
|
}
|
|
|
|
fn run_app_update(
|
|
runner_state: &mut WinitAppRunnerState,
|
|
app: &mut App,
|
|
winit_events: &mut Vec<WinitEvent>,
|
|
) {
|
|
runner_state.reset_on_update();
|
|
|
|
forward_winit_events(winit_events, app);
|
|
|
|
if app.plugins_state() == PluginsState::Cleaned {
|
|
app.update();
|
|
}
|
|
}
|
|
|
|
fn react_to_resize(
|
|
win: &mut Mut<'_, Window>,
|
|
size: winit::dpi::PhysicalSize<u32>,
|
|
window_resized: &mut EventWriter<WindowResized>,
|
|
window: Entity,
|
|
) {
|
|
win.resolution
|
|
.set_physical_resolution(size.width, size.height);
|
|
|
|
window_resized.send(WindowResized {
|
|
window,
|
|
width: win.width(),
|
|
height: win.height(),
|
|
});
|
|
}
|