fix: upgrade to winit v0.30 (#13366)

# Objective

- Upgrade winit to v0.30
- Fixes https://github.com/bevyengine/bevy/issues/13331

## Solution

This is a rewrite/adaptation of the new trait system described and
implemented in `winit` v0.30.

## Migration Guide

The custom UserEvent is now renamed as WakeUp, used to wake up the loop
if anything happens outside the app (a new
[custom_user_event](https://github.com/bevyengine/bevy/pull/13366/files#diff-2de8c0a8d3028d0059a3d80ae31b2bbc1cde2595ce2d317ea378fe3e0cf6ef2d)
shows this behavior.

The internal `UpdateState` has been removed and replaced internally by
the AppLifecycle. When changed, the AppLifecycle is sent as an event.

The `UpdateMode` now accepts only two values: `Continuous` and
`Reactive`, but the latter exposes 3 new properties to enable reactive
to device, user or window events. The previous `UpdateMode::Reactive` is
now equivalent to `UpdateMode::reactive()`, while
`UpdateMode::ReactiveLowPower` to `UpdateMode::reactive_low_power()`.

The `ApplicationLifecycle` has been renamed as `AppLifecycle`, and now
contains the possible values of the application state inside the event
loop:
* `Idle`: the loop has not started yet
* `Running` (previously called `Started`): the loop is running
* `WillSuspend`: the loop is going to be suspended
* `Suspended`: the loop is suspended
* `WillResume`: the loop is going to be resumed

Note: the `Resumed` state has been removed since the resumed app is just
running.

Finally, now that `winit` enables this, it extends the `WinitPlugin` to
support custom events.

## Test platforms

- [x] Windows
- [x] MacOs
- [x] Linux (x11)
- [x] Linux (Wayland)
- [x] Android
- [x] iOS
- [x] WASM/WebGPU
- [x] WASM/WebGL2

## Outstanding issues / regressions

- [ ] iOS: build failed in CI
   - blocking, but may just be flakiness
- [x] Cross-platform: when the window is maximised, changes in the scale
factor don't apply, to make them apply one has to make the window
smaller again. (Re-maximising keeps the updated scale factor)
    - non-blocking, but good to fix
- [ ] Android: it's pretty easy to quickly open and close the app and
then the music keeps playing when suspended.
    - non-blocking but worrying
- [ ]  Web: the application will hang when switching tabs
- Not new, duplicate of https://github.com/bevyengine/bevy/issues/13486
- [ ] Cross-platform?: Screenshot failure, `ERROR present_frames:
wgpu_core::present: No work has been submitted for this frame before`
taking the first screenshot, but after pressing space
    - non-blocking, but good to fix

---------

Co-authored-by: François <francois.mockers@vleue.com>
This commit is contained in:
Pietro 2024-06-03 15:06:48 +02:00 committed by GitHub
parent 44c8cc66c4
commit 061bee7e3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1357 additions and 1029 deletions

View file

@ -359,6 +359,10 @@ argh = "0.1.12"
thiserror = "1.0"
event-listener = "5.3.0"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen = { version = "0.2" }
web-sys = { version = "0.3", features = ["Window"] }
[[example]]
name = "hello_world"
path = "examples/hello_world.rs"
@ -2836,6 +2840,17 @@ description = "Creates a solid color window"
category = "Window"
wasm = true
[[example]]
name = "custom_user_event"
path = "examples/window/custom_user_event.rs"
doc-scrape-examples = true
[package.metadata.example.custom_user_event]
name = "Custom User Event"
description = "Handles custom user events within the event loop"
category = "Window"
wasm = true
[[example]]
name = "low_power"
path = "examples/window/low_power.rs"

View file

@ -14,7 +14,7 @@ bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
accesskit = "0.12"
accesskit = "0.14"
[lints]
workspace = true

View file

@ -69,7 +69,7 @@ impl PluginGroup for DefaultPlugins {
#[cfg(feature = "bevy_winit")]
{
group = group.add(bevy_winit::WinitPlugin::default());
group = group.add::<bevy_winit::WinitPlugin>(bevy_winit::WinitPlugin::default());
}
#[cfg(feature = "bevy_render")]

View file

@ -58,7 +58,7 @@ use bevy_utils::prelude::default;
pub use extract_param::Extract;
use bevy_hierarchy::ValidParentCheckPlugin;
use bevy_window::{PrimaryWindow, RawHandleWrapper};
use bevy_window::{PrimaryWindow, RawHandleWrapperHolder};
use extract_resource::ExtractResourcePlugin;
use globals::GlobalsPlugin;
use render_asset::RenderAssetBytesPerFrame;
@ -268,10 +268,9 @@ impl Plugin for RenderPlugin {
));
let mut system_state: SystemState<
Query<&RawHandleWrapper, With<PrimaryWindow>>,
Query<&RawHandleWrapperHolder, With<PrimaryWindow>>,
> = SystemState::new(app.world_mut());
let primary_window = system_state.get(app.world()).get_single().ok().cloned();
let settings = render_creation.clone();
let async_renderer = async move {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
@ -282,11 +281,20 @@ impl Plugin for RenderPlugin {
});
// SAFETY: Plugins should be set up on the main thread.
let surface = primary_window.map(|wrapper| unsafe {
let handle = wrapper.get_handle();
instance
.create_surface(handle)
.expect("Failed to create wgpu surface")
let surface = primary_window.and_then(|wrapper| unsafe {
let maybe_handle = wrapper.0.lock().expect(
"Couldn't get the window handle in time for renderer initialization",
);
if let Some(wrapper) = maybe_handle.as_ref() {
let handle = wrapper.get_handle();
Some(
instance
.create_surface(handle)
.expect("Failed to create wgpu surface"),
)
} else {
None
}
});
let request_adapter_options = wgpu::RequestAdapterOptions {

View file

@ -22,7 +22,7 @@ concurrent-queue = { version = "2.0.0", optional = true }
wasm-bindgen-futures = "0.4"
[dev-dependencies]
web-time = { version = "0.2" }
web-time = { version = "1.1" }
[lints]
workspace = true

View file

@ -14,7 +14,7 @@ detailed_trace = []
[dependencies]
ahash = "0.8.7"
tracing = { version = "0.1", default-features = false, features = ["std"] }
web-time = { version = "0.2" }
web-time = { version = "1.1" }
hashbrown = { version = "0.14", features = ["serde"] }
bevy_utils_proc_macros = { version = "0.14.0-dev", path = "macros" }
thread_local = "1.0"

View file

@ -388,13 +388,28 @@ pub struct WindowThemeChanged {
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum ApplicationLifetime {
/// The application just started.
Started,
pub enum AppLifecycle {
/// The application is not started yet.
Idle,
/// The application is running.
Running,
/// The application is going to be suspended.
/// Applications have one frame to react to this event before being paused in the background.
WillSuspend,
/// The application was suspended.
///
/// On Android, applications have one frame to react to this event before being paused in the background.
Suspended,
/// The application was resumed.
Resumed,
/// The application is going to be resumed.
/// Applications have one extra frame to react to this event before being fully resumed.
WillResume,
}
impl AppLifecycle {
/// Return `true` if the app can be updated.
#[inline]
pub fn is_active(&self) -> bool {
match self {
Self::Idle | Self::Suspended => false,
Self::Running | Self::WillSuspend | Self::WillResume => true,
}
}
}

View file

@ -11,6 +11,8 @@
//! The [`WindowPlugin`] sets up some global window-related parameters and
//! is part of the [`DefaultPlugins`](https://docs.rs/bevy/latest/bevy/struct.DefaultPlugins.html).
use std::sync::{Arc, Mutex};
use bevy_a11y::Focus;
mod cursor;
@ -106,13 +108,16 @@ impl Plugin for WindowPlugin {
.add_event::<FileDragAndDrop>()
.add_event::<WindowMoved>()
.add_event::<WindowThemeChanged>()
.add_event::<ApplicationLifetime>();
.add_event::<AppLifecycle>();
if let Some(primary_window) = &self.primary_window {
let initial_focus = app
.world_mut()
.spawn(primary_window.clone())
.insert(PrimaryWindow)
.insert((
PrimaryWindow,
RawHandleWrapperHolder(Arc::new(Mutex::new(None))),
))
.id();
if let Some(mut focus) = app.world_mut().get_resource_mut::<Focus>() {
**focus = Some(initial_focus);
@ -153,7 +158,7 @@ impl Plugin for WindowPlugin {
.register_type::<FileDragAndDrop>()
.register_type::<WindowMoved>()
.register_type::<WindowThemeChanged>()
.register_type::<ApplicationLifetime>();
.register_type::<AppLifecycle>();
// Register window descriptor and related types
app.register_type::<Window>()

View file

@ -5,7 +5,12 @@ use raw_window_handle::{
DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle,
RawWindowHandle, WindowHandle,
};
use std::{any::Any, marker::PhantomData, ops::Deref, sync::Arc};
use std::{
any::Any,
marker::PhantomData,
ops::Deref,
sync::{Arc, Mutex},
};
/// A wrapper over a window.
///
@ -116,3 +121,7 @@ impl HasDisplayHandle for ThreadLockedRawWindowHandleWrapper {
Ok(unsafe { DisplayHandle::borrow_raw(self.0.display_handle) })
}
}
/// Holder of the [`RawHandleWrapper`] with wrappers, to allow use in asynchronous context
#[derive(Debug, Clone, Component)]
pub struct RawHandleWrapperHolder(pub Arc<Mutex<Option<RawHandleWrapper>>>);

View file

@ -687,10 +687,10 @@ impl Default for WindowResolution {
impl WindowResolution {
/// Creates a new [`WindowResolution`].
pub fn new(logical_width: f32, logical_height: f32) -> Self {
pub fn new(physical_width: f32, physical_height: f32) -> Self {
Self {
physical_width: logical_width as u32,
physical_height: logical_height as u32,
physical_width: physical_width as u32,
physical_height: physical_height as u32,
..Default::default()
}
}
@ -783,9 +783,7 @@ impl WindowResolution {
/// Set the window's scale factor, this may get overridden by the backend.
#[inline]
pub fn set_scale_factor(&mut self, scale_factor: f32) {
let (width, height) = (self.width(), self.height());
self.scale_factor = scale_factor;
self.set(width, height);
}
/// Set the window's scale factor, this will be used over what the backend decides.
@ -794,9 +792,7 @@ impl WindowResolution {
/// size is not within the limits.
#[inline]
pub fn set_scale_factor_override(&mut self, scale_factor_override: Option<f32>) {
let (width, height) = (self.width(), self.height());
self.scale_factor_override = scale_factor_override;
self.set(width, height);
}
}

View file

@ -32,8 +32,8 @@ bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }
# other
# feature rwh_06 refers to window_raw_handle@v0.6
winit = { version = "0.29", default-features = false, features = ["rwh_06"] }
accesskit_winit = { version = "0.17", default-features = false, features = [
winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
accesskit_winit = { version = "0.20", default-features = false, features = [
"rwh_06",
] }
approx = { version = "0.5", default-features = false }
@ -42,7 +42,7 @@ raw-window-handle = "0.6"
serde = { version = "1.0", features = ["derive"], optional = true }
[target.'cfg(target_os = "android")'.dependencies]
winit = { version = "0.29", default-features = false, features = [
winit = { version = "0.30", default-features = false, features = [
"android-native-activity",
"rwh_06",
] }

View file

@ -6,10 +6,9 @@ use std::{
};
use accesskit_winit::Adapter;
use bevy_a11y::accesskit::{ActivationHandler, DeactivationHandler, Node};
use bevy_a11y::{
accesskit::{
ActionHandler, ActionRequest, NodeBuilder, NodeClassSet, NodeId, Role, Tree, TreeUpdate,
},
accesskit::{ActionHandler, ActionRequest, NodeBuilder, NodeId, Role, Tree, TreeUpdate},
AccessibilityNode, AccessibilityRequested, AccessibilitySystem, Focus,
};
use bevy_a11y::{ActionRequest as ActionRequestWrapper, ManageAccessibilityUpdates};
@ -20,7 +19,7 @@ use bevy_ecs::{
prelude::{DetectChanges, Entity, EventReader, EventWriter},
query::With,
schedule::IntoSystemConfigs,
system::{NonSend, NonSendMut, Query, Res, ResMut, Resource},
system::{NonSendMut, Query, Res, ResMut, Resource},
};
use bevy_hierarchy::{Children, Parent};
use bevy_window::{PrimaryWindow, Window, WindowClosed};
@ -29,13 +28,78 @@ use bevy_window::{PrimaryWindow, Window, WindowClosed};
#[derive(Default, Deref, DerefMut)]
pub struct AccessKitAdapters(pub EntityHashMap<Adapter>);
/// Maps window entities to their respective [`WinitActionHandler`]s.
/// Maps window entities to their respective [`WinitActionRequests`]s.
#[derive(Resource, Default, Deref, DerefMut)]
pub struct WinitActionHandlers(pub EntityHashMap<WinitActionHandler>);
pub struct WinitActionRequestHandlers(pub EntityHashMap<Arc<Mutex<WinitActionRequestHandler>>>);
/// Forwards `AccessKit` [`ActionRequest`]s from winit to an event channel.
#[derive(Clone, Default, Deref, DerefMut)]
pub struct WinitActionHandler(pub Arc<Mutex<VecDeque<ActionRequest>>>);
pub struct WinitActionRequestHandler(pub VecDeque<ActionRequest>);
impl WinitActionRequestHandler {
fn new() -> Arc<Mutex<Self>> {
Arc::new(Mutex::new(Self(VecDeque::new())))
}
}
struct AccessKitState {
name: String,
entity: Entity,
requested: AccessibilityRequested,
}
impl AccessKitState {
fn new(
name: impl Into<String>,
entity: Entity,
requested: AccessibilityRequested,
) -> Arc<Mutex<Self>> {
let name = name.into();
Arc::new(Mutex::new(Self {
name,
entity,
requested,
}))
}
fn build_root(&mut self) -> Node {
let mut builder = NodeBuilder::new(Role::Window);
builder.set_name(self.name.clone());
builder.build()
}
fn build_initial_tree(&mut self) -> TreeUpdate {
let root = self.build_root();
let accesskit_window_id = NodeId(self.entity.to_bits());
let mut tree = Tree::new(accesskit_window_id);
tree.app_name = Some(self.name.clone());
self.requested.set(true);
TreeUpdate {
nodes: vec![(accesskit_window_id, root)],
tree: Some(tree),
focus: accesskit_window_id,
}
}
}
struct WinitActivationHandler(Arc<Mutex<AccessKitState>>);
impl ActivationHandler for WinitActivationHandler {
fn request_initial_tree(&mut self) -> Option<TreeUpdate> {
Some(self.0.lock().unwrap().build_initial_tree())
}
}
impl WinitActivationHandler {
pub fn new(state: Arc<Mutex<AccessKitState>>) -> Self {
Self(state)
}
}
#[derive(Clone, Default)]
struct WinitActionHandler(Arc<Mutex<WinitActionRequestHandler>>);
impl ActionHandler for WinitActionHandler {
fn do_action(&mut self, request: ActionRequest) {
@ -44,6 +108,18 @@ impl ActionHandler for WinitActionHandler {
}
}
impl WinitActionHandler {
pub fn new(handler: Arc<Mutex<WinitActionRequestHandler>>) -> Self {
Self(handler)
}
}
struct WinitDeactivationHandler;
impl DeactivationHandler for WinitDeactivationHandler {
fn deactivate_accessibility(&mut self) {}
}
/// Prepares accessibility for a winit window.
pub(crate) fn prepare_accessibility_for_window(
winit_window: &winit::window::Window,
@ -51,43 +127,39 @@ pub(crate) fn prepare_accessibility_for_window(
name: String,
accessibility_requested: AccessibilityRequested,
adapters: &mut AccessKitAdapters,
handlers: &mut WinitActionHandlers,
handlers: &mut WinitActionRequestHandlers,
) {
let mut root_builder = NodeBuilder::new(Role::Window);
root_builder.set_name(name.into_boxed_str());
let root = root_builder.build(&mut NodeClassSet::lock_global());
let state = AccessKitState::new(name, entity, accessibility_requested);
let activation_handler = WinitActivationHandler::new(Arc::clone(&state));
let accesskit_window_id = NodeId(entity.to_bits());
let handler = WinitActionHandler::default();
let adapter = Adapter::with_action_handler(
let action_request_handler = WinitActionRequestHandler::new();
let action_handler = WinitActionHandler::new(Arc::clone(&action_request_handler));
let deactivation_handler = WinitDeactivationHandler;
let adapter = Adapter::with_direct_handlers(
winit_window,
move || {
accessibility_requested.set(true);
TreeUpdate {
nodes: vec![(accesskit_window_id, root)],
tree: Some(Tree::new(accesskit_window_id)),
focus: accesskit_window_id,
}
},
Box::new(handler.clone()),
activation_handler,
action_handler,
deactivation_handler,
);
adapters.insert(entity, adapter);
handlers.insert(entity, handler);
handlers.insert(entity, action_request_handler);
}
fn window_closed(
mut adapters: NonSendMut<AccessKitAdapters>,
mut receivers: ResMut<WinitActionHandlers>,
mut handlers: ResMut<WinitActionRequestHandlers>,
mut events: EventReader<WindowClosed>,
) {
for WindowClosed { window, .. } in events.read() {
adapters.remove(window);
receivers.remove(window);
handlers.remove(window);
}
}
fn poll_receivers(
handlers: Res<WinitActionHandlers>,
handlers: Res<WinitActionRequestHandlers>,
mut actions: EventWriter<ActionRequestWrapper>,
) {
for (_id, handler) in handlers.iter() {
@ -106,7 +178,7 @@ fn should_update_accessibility_nodes(
}
fn update_accessibility_nodes(
adapters: NonSend<AccessKitAdapters>,
mut adapters: NonSendMut<AccessKitAdapters>,
focus: Res<Focus>,
primary_window: Query<(Entity, &Window), With<PrimaryWindow>>,
nodes: Query<(
@ -120,7 +192,7 @@ fn update_accessibility_nodes(
let Ok((primary_window_id, primary_window)) = primary_window.get_single() else {
return;
};
let Some(adapter) = adapters.get(&primary_window_id) else {
let Some(adapter) = adapters.get_mut(&primary_window_id) else {
return;
};
if focus.is_changed() || !nodes.is_empty() {
@ -155,7 +227,7 @@ fn update_adapter(
queue_node_for_update(entity, parent, &node_entities, &mut window_children);
add_children_nodes(children, &node_entities, &mut node);
let node_id = NodeId(entity.to_bits());
let node = node.build(&mut NodeClassSet::lock_global());
let node = node.build();
to_update.push((node_id, node));
}
let mut window_node = NodeBuilder::new(Role::Window);
@ -164,7 +236,7 @@ fn update_adapter(
window_node.set_name(title.into_boxed_str());
}
window_node.set_children(window_children);
let window_node = window_node.build(&mut NodeClassSet::lock_global());
let window_node = window_node.build();
let node_id = NodeId(primary_window_id.to_bits());
let window_update = (node_id, window_node);
to_update.insert(0, window_update);
@ -214,7 +286,7 @@ pub struct AccessKitPlugin;
impl Plugin for AccessKitPlugin {
fn build(&self, app: &mut App) {
app.init_non_send_resource::<AccessKitAdapters>()
.init_resource::<WinitActionHandlers>()
.init_resource::<WinitActionRequestHandlers>()
.add_event::<ActionRequestWrapper>()
.add_systems(
PostUpdate,

View file

@ -12,60 +12,33 @@
//! The app's [runner](bevy_app::App::runner) is set by `WinitPlugin` and handles the `winit` [`EventLoop`].
//! See `winit_runner` for details.
pub mod accessibility;
mod converters;
mod system;
mod winit_config;
pub mod winit_event;
mod winit_windows;
use bevy_window::RawHandleWrapperHolder;
use std::marker::PhantomData;
use winit::event_loop::EventLoop;
#[cfg(target_os = "android")]
pub use winit::platform::android::activity as android_activity;
use std::sync::mpsc::{sync_channel, SyncSender};
use approx::relative_eq;
use bevy_a11y::AccessibilityRequested;
use bevy_utils::Instant;
use bevy_app::{App, Last, Plugin};
use bevy_ecs::prelude::*;
#[allow(deprecated)]
use bevy_window::{exit_on_all_closed, Window, WindowCreated};
pub use system::create_windows;
use system::{changed_windows, despawn_windows, CachedWindow};
use winit::dpi::{LogicalSize, PhysicalSize};
use system::{changed_windows, despawn_windows};
pub use winit_config::*;
pub use winit_event::*;
pub use winit_windows::*;
use bevy_app::{App, AppExit, Last, Plugin, PluginsState};
use bevy_ecs::event::ManualEventReader;
use bevy_ecs::prelude::*;
use bevy_ecs::system::SystemState;
use bevy_input::{
mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel},
touchpad::{TouchpadMagnify, TouchpadRotate},
};
use bevy_math::{ivec2, DVec2, Vec2};
#[cfg(not(target_arch = "wasm32"))]
use bevy_tasks::tick_global_task_pools_on_main_thread;
use bevy_utils::tracing::{error, trace, warn};
#[allow(deprecated)]
use bevy_window::{
exit_on_all_closed, ApplicationLifetime, CursorEntered, CursorLeft, CursorMoved,
FileDragAndDrop, Ime, ReceivedCharacter, RequestRedraw, Window,
WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, WindowDestroyed,
WindowFocused, WindowMoved, WindowOccluded, WindowResized, WindowScaleFactorChanged,
WindowThemeChanged,
};
#[cfg(target_os = "android")]
use bevy_window::{PrimaryWindow, RawHandleWrapper};
use crate::accessibility::{AccessKitAdapters, AccessKitPlugin, WinitActionRequestHandlers};
use crate::state::winit_runner;
#[cfg(target_os = "android")]
pub use winit::platform::android::activity as android_activity;
use winit::event::StartCause;
use winit::{
event::{self, DeviceEvent, Event, WindowEvent},
event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopWindowTarget},
};
use crate::accessibility::{AccessKitAdapters, AccessKitPlugin, WinitActionHandlers};
use crate::converters::convert_winit_theme;
pub mod accessibility;
mod converters;
mod state;
mod system;
mod winit_config;
pub mod winit_event;
mod winit_windows;
/// [`AndroidApp`] provides an interface to query the application state as well as monitor events
/// (for example lifecycle and input events).
@ -79,8 +52,11 @@ pub static ANDROID_APP: std::sync::OnceLock<android_activity::AndroidApp> =
/// This plugin will add systems and resources that sync with the `winit` backend and also
/// replace the existing [`App`] runner with one that constructs an [event loop](EventLoop) to
/// receive window and input events from the OS.
///
/// The `T` event type can be used to pass custom events to the `winit`'s loop, and handled as events
/// in systems.
#[derive(Default)]
pub struct WinitPlugin {
pub struct WinitPlugin<T: Event = WakeUp> {
/// Allows the window (and the event loop) to be created on any thread
/// instead of only the main thread.
///
@ -91,18 +67,23 @@ pub struct WinitPlugin {
/// Only works on Linux (X11/Wayland) and Windows.
/// This field is ignored on other platforms.
pub run_on_any_thread: bool,
marker: PhantomData<T>,
}
impl Plugin for WinitPlugin {
impl<T: Event> Plugin for WinitPlugin<T> {
fn name(&self) -> &str {
"bevy_winit::WinitPlugin"
}
fn build(&self, app: &mut App) {
let mut event_loop_builder = EventLoopBuilder::<UserEvent>::with_user_event();
let mut event_loop_builder = EventLoop::<T>::with_user_event();
// linux check is needed because x11 might be enabled on other platforms.
#[cfg(all(target_os = "linux", feature = "x11"))]
{
use winit::platform::x11::EventLoopBuilderExtX11;
// This allows a Bevy app to be started and ran outside of the main thread.
// This allows a Bevy app to be started and ran outside the main thread.
// A use case for this is to allow external applications to spawn a thread
// which runs a Bevy app without requiring the Bevy app to need to reside on
// the main thread, which can be problematic.
@ -132,7 +113,7 @@ impl Plugin for WinitPlugin {
app.init_non_send_resource::<WinitWindows>()
.init_resource::<WinitSettings>()
.add_event::<WinitEvent>()
.set_runner(winit_runner)
.set_runner(winit_runner::<T>)
.add_systems(
Last,
(
@ -150,691 +131,50 @@ impl Plugin for WinitPlugin {
.build()
.expect("Failed to build event loop");
// iOS, macOS, and Android don't like it if you create windows before the event loop is
// initialized.
//
// See:
// - https://github.com/rust-windowing/winit/blob/master/README.md#macos
// - https://github.com/rust-windowing/winit/blob/master/README.md#ios
#[cfg(not(any(target_os = "android", target_os = "ios", target_os = "macos")))]
{
// Otherwise, we want to create a window before `bevy_render` initializes the renderer
// so that we have a surface to use as a hint. This improves compatibility with `wgpu`
// backends, especially WASM/WebGL2.
let mut create_window = SystemState::<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 default event that can be used to wake the window loop
/// Wakes up the loop if in wait state
#[derive(Debug, Default, Clone, Copy, Event)]
pub struct WakeUp;
/// The [`winit::event_loop::EventLoopProxy`] with the specific [`winit::event::Event::UserEvent`] used in the [`winit_runner`].
///
/// The `EventLoopProxy` can be used to request a redraw from outside bevy.
///
/// Use `NonSend<EventLoopProxy>` to receive this resource.
pub type EventLoopProxy = winit::event_loop::EventLoopProxy<UserEvent>;
pub type EventLoopProxy<T> = winit::event_loop::EventLoopProxy<T>;
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())
trait AppSendEvent {
fn send(&mut self, event: impl Into<WinitEvent>);
}
#[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();
impl AppSendEvent for Vec<WinitEvent> {
fn send(&mut self, event: impl Into<WinitEvent>) {
self.push(Into::<WinitEvent>::into(event));
}
}
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(),
});
}
/// The parameters of the [`create_windows`] system.
pub type CreateWindowParams<'w, 's, F = ()> = (
Commands<'w, 's>,
Query<
'w,
's,
(
Entity,
&'static mut Window,
Option<&'static RawHandleWrapperHolder>,
),
F,
>,
EventWriter<'w, WindowCreated>,
NonSendMut<'w, WinitWindows>,
NonSendMut<'w, AccessKitAdapters>,
ResMut<'w, WinitActionRequestHandlers>,
Res<'w, AccessibilityRequested>,
);

View file

@ -0,0 +1,771 @@
use approx::relative_eq;
use bevy_app::{App, AppExit, PluginsState};
use bevy_ecs::change_detection::{DetectChanges, NonSendMut, Res};
use bevy_ecs::entity::Entity;
use bevy_ecs::event::{EventWriter, ManualEventReader};
use bevy_ecs::prelude::*;
use bevy_ecs::system::SystemState;
use bevy_ecs::world::FromWorld;
use bevy_input::{
mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel},
touchpad::{TouchpadMagnify, TouchpadRotate},
};
use bevy_log::{error, trace, warn};
use bevy_math::{ivec2, DVec2, Vec2};
#[cfg(not(target_arch = "wasm32"))]
use bevy_tasks::tick_global_task_pools_on_main_thread;
use bevy_utils::Instant;
use std::marker::PhantomData;
use winit::application::ApplicationHandler;
use winit::dpi::PhysicalSize;
use winit::event;
use winit::event::{DeviceEvent, DeviceId, StartCause, WindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
use winit::window::WindowId;
#[allow(deprecated)]
use bevy_window::{
AppLifecycle, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, ReceivedCharacter,
RequestRedraw, Window, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowDestroyed,
WindowFocused, WindowMoved, WindowOccluded, WindowResized, WindowScaleFactorChanged,
WindowThemeChanged,
};
#[cfg(target_os = "android")]
use bevy_window::{PrimaryWindow, RawHandleWrapper};
use crate::accessibility::AccessKitAdapters;
use crate::system::CachedWindow;
use crate::{
converters, create_windows, AppSendEvent, CreateWindowParams, UpdateMode, WinitEvent,
WinitSettings, WinitWindows,
};
/// Persistent state that is used to run the [`App`] according to the current
/// [`UpdateMode`].
struct WinitAppRunnerState<T: Event> {
/// The running app.
app: App,
/// Exit value once the loop is finished.
app_exit: Option<AppExit>,
/// Current update mode of the app.
update_mode: UpdateMode,
/// Is `true` if a new [`WindowEvent`] event has been received since the last update.
window_event_received: bool,
/// Is `true` if a new [`DeviceEvent`] event has been received since the last update.
device_event_received: bool,
/// Is `true` if a new [`T`] event has been received since the last update.
user_event_received: bool,
/// Is `true` if the app has requested a redraw since the last update.
redraw_requested: bool,
/// Is `true` if enough time has elapsed since `last_update` to run another update.
wait_elapsed: bool,
/// Number of "forced" updates to trigger on application start
startup_forced_updates: u32,
/// Current app lifecycle state.
lifecycle: AppLifecycle,
/// The previous app lifecycle state.
previous_lifecycle: AppLifecycle,
/// Winit events to send
winit_events: Vec<WinitEvent>,
_marker: PhantomData<T>,
event_writer_system_state: SystemState<(
EventWriter<'static, WindowResized>,
EventWriter<'static, WindowBackendScaleFactorChanged>,
EventWriter<'static, WindowScaleFactorChanged>,
NonSend<'static, WinitWindows>,
Query<'static, 'static, (&'static mut Window, &'static mut CachedWindow)>,
NonSendMut<'static, AccessKitAdapters>,
)>,
}
impl<T: Event> WinitAppRunnerState<T> {
fn new(mut app: App) -> Self {
app.add_event::<T>();
let event_writer_system_state: SystemState<(
EventWriter<WindowResized>,
EventWriter<WindowBackendScaleFactorChanged>,
EventWriter<WindowScaleFactorChanged>,
NonSend<WinitWindows>,
Query<(&mut Window, &mut CachedWindow)>,
NonSendMut<AccessKitAdapters>,
)> = SystemState::new(app.world_mut());
Self {
app,
lifecycle: AppLifecycle::Idle,
previous_lifecycle: AppLifecycle::Idle,
app_exit: None,
update_mode: UpdateMode::Continuous,
window_event_received: false,
device_event_received: false,
user_event_received: false,
redraw_requested: false,
wait_elapsed: false,
// 3 seems to be enough, 5 is a safe margin
startup_forced_updates: 5,
winit_events: Vec::new(),
_marker: PhantomData,
event_writer_system_state,
}
}
fn reset_on_update(&mut self) {
self.window_event_received = false;
self.device_event_received = false;
self.user_event_received = false;
}
fn world(&self) -> &World {
self.app.world()
}
fn world_mut(&mut self) -> &mut World {
self.app.world_mut()
}
}
impl<T: Event> ApplicationHandler<T> for WinitAppRunnerState<T> {
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
if event_loop.exiting() {
return;
}
#[cfg(feature = "trace")]
let _span = bevy_utils::tracing::info_span!("winit event_handler").entered();
if self.app.plugins_state() != PluginsState::Cleaned {
if self.app.plugins_state() != PluginsState::Ready {
#[cfg(not(target_arch = "wasm32"))]
tick_global_task_pools_on_main_thread();
} else {
self.app.finish();
self.app.cleanup();
}
self.redraw_requested = true;
}
self.wait_elapsed = match cause {
StartCause::WaitCancelled {
requested_resume: Some(resume),
..
} => {
// If the resume time is not after now, it means that at least the wait timeout
// has elapsed.
resume <= Instant::now()
}
_ => true,
};
}
fn resumed(&mut self, _event_loop: &ActiveEventLoop) {
// Mark the state as `WillResume`. This will let the schedule run one extra time
// when actually resuming the app
self.lifecycle = AppLifecycle::WillResume;
}
fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: T) {
self.user_event_received = true;
self.world_mut().send_event(event);
self.redraw_requested = true;
}
fn window_event(
&mut self,
_event_loop: &ActiveEventLoop,
window_id: WindowId,
event: WindowEvent,
) {
self.window_event_received = true;
let (
mut window_resized,
mut window_backend_scale_factor_changed,
mut window_scale_factor_changed,
winit_windows,
mut windows,
mut access_kit_adapters,
) = self.event_writer_system_state.get_mut(self.app.world_mut());
let Some(window) = winit_windows.get_window_entity(window_id) else {
warn!("Skipped event {event:?} for unknown winit Window Id {window_id:?}");
return;
};
let Ok((mut win, _)) = windows.get_mut(window) else {
warn!("Window {window:?} is missing `Window` component, skipping event {event:?}");
return;
};
// Allow AccessKit to respond to `WindowEvent`s before they reach
// the engine.
if let Some(adapter) = access_kit_adapters.get_mut(&window) {
if let Some(winit_window) = winit_windows.get_window(window) {
adapter.process_event(winit_window, &event);
}
}
match event {
WindowEvent::Resized(size) => {
react_to_resize(window, &mut win, size, &mut window_resized);
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
react_to_scale_factor_change(
window,
&mut win,
scale_factor,
&mut window_backend_scale_factor_changed,
&mut window_scale_factor_changed,
);
}
WindowEvent::CloseRequested => self.winit_events.send(WindowCloseRequested { window }),
WindowEvent::KeyboardInput { ref event, .. } => {
if event.state.is_pressed() {
if let Some(char) = &event.text {
let char = char.clone();
#[allow(deprecated)]
self.winit_events.send(ReceivedCharacter { window, char });
}
}
self.winit_events
.send(converters::convert_keyboard_input(event, window));
}
WindowEvent::CursorMoved { position, .. } => {
let physical_position = DVec2::new(position.x, position.y);
let last_position = win.physical_cursor_position();
let delta = last_position.map(|last_pos| {
(physical_position.as_vec2() - last_pos) / win.resolution.scale_factor()
});
win.set_physical_cursor_position(Some(physical_position));
let position = (physical_position / win.resolution.scale_factor() as f64).as_vec2();
self.winit_events.send(CursorMoved {
window,
position,
delta,
});
}
WindowEvent::CursorEntered { .. } => {
self.winit_events.send(CursorEntered { window });
}
WindowEvent::CursorLeft { .. } => {
win.set_physical_cursor_position(None);
self.winit_events.send(CursorLeft { window });
}
WindowEvent::MouseInput { state, button, .. } => {
self.winit_events.send(MouseButtonInput {
button: converters::convert_mouse_button(button),
state: converters::convert_element_state(state),
window,
});
}
WindowEvent::PinchGesture { delta, .. } => {
self.winit_events.send(TouchpadMagnify(delta as f32));
}
WindowEvent::RotationGesture { delta, .. } => {
self.winit_events.send(TouchpadRotate(delta));
}
WindowEvent::MouseWheel { delta, .. } => match delta {
event::MouseScrollDelta::LineDelta(x, y) => {
self.winit_events.send(MouseWheel {
unit: MouseScrollUnit::Line,
x,
y,
window,
});
}
event::MouseScrollDelta::PixelDelta(p) => {
self.winit_events.send(MouseWheel {
unit: MouseScrollUnit::Pixel,
x: p.x as f32,
y: p.y as f32,
window,
});
}
},
WindowEvent::Touch(touch) => {
let location = touch
.location
.to_logical(win.resolution.scale_factor() as f64);
self.winit_events
.send(converters::convert_touch_input(touch, location, window));
}
WindowEvent::Focused(focused) => {
win.focused = focused;
self.winit_events.send(WindowFocused { window, focused });
}
WindowEvent::Occluded(occluded) => {
self.winit_events.send(WindowOccluded { window, occluded });
}
WindowEvent::DroppedFile(path_buf) => {
self.winit_events
.send(FileDragAndDrop::DroppedFile { window, path_buf });
}
WindowEvent::HoveredFile(path_buf) => {
self.winit_events
.send(FileDragAndDrop::HoveredFile { window, path_buf });
}
WindowEvent::HoveredFileCancelled => {
self.winit_events
.send(FileDragAndDrop::HoveredFileCanceled { window });
}
WindowEvent::Moved(position) => {
let position = ivec2(position.x, position.y);
win.position.set(position);
self.winit_events.send(WindowMoved { window, position });
}
WindowEvent::Ime(event) => match event {
event::Ime::Preedit(value, cursor) => {
self.winit_events.send(Ime::Preedit {
window,
value,
cursor,
});
}
event::Ime::Commit(value) => {
self.winit_events.send(Ime::Commit { window, value });
}
event::Ime::Enabled => {
self.winit_events.send(Ime::Enabled { window });
}
event::Ime::Disabled => {
self.winit_events.send(Ime::Disabled { window });
}
},
WindowEvent::ThemeChanged(theme) => {
self.winit_events.send(WindowThemeChanged {
window,
theme: converters::convert_winit_theme(theme),
});
}
WindowEvent::Destroyed => {
self.winit_events.send(WindowDestroyed { window });
}
_ => {}
}
let mut windows = self.world_mut().query::<(&mut Window, &mut CachedWindow)>();
if let Ok((window_component, mut cache)) = windows.get_mut(self.world_mut(), window) {
if window_component.is_changed() {
cache.window = window_component.clone();
}
}
}
fn device_event(
&mut self,
_event_loop: &ActiveEventLoop,
_device_id: DeviceId,
event: DeviceEvent,
) {
self.device_event_received = true;
if let DeviceEvent::MouseMotion { delta: (x, y) } = event {
let delta = Vec2::new(x as f32, y as f32);
self.winit_events.send(MouseMotion { delta });
}
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
// create any new windows
// (even if app did not update, some may have been created by plugin setup)
let mut create_window =
SystemState::<CreateWindowParams<Added<Window>>>::from_world(self.world_mut());
create_windows(event_loop, create_window.get_mut(self.world_mut()));
create_window.apply(self.world_mut());
let mut redraw_event_reader = ManualEventReader::<RequestRedraw>::default();
let mut focused_windows_state: SystemState<(Res<WinitSettings>, Query<(Entity, &Window)>)> =
SystemState::new(self.world_mut());
if let Some(app_redraw_events) = self.world().get_resource::<Events<RequestRedraw>>() {
if redraw_event_reader.read(app_redraw_events).last().is_some() {
self.redraw_requested = true;
}
}
let (config, windows) = focused_windows_state.get(self.world());
let focused = windows.iter().any(|(_, window)| window.focused);
let mut update_mode = config.update_mode(focused);
let mut should_update = self.should_update(update_mode);
if self.startup_forced_updates > 0 {
self.startup_forced_updates -= 1;
// Ensure that an update is triggered on the first iterations for app initialization
should_update = true;
}
if self.lifecycle == AppLifecycle::WillSuspend {
self.lifecycle = AppLifecycle::Suspended;
// Trigger one last update to enter the suspended state
should_update = true;
#[cfg(target_os = "android")]
{
// Remove the `RawHandleWrapper` from the primary window.
// This will trigger the surface destruction.
let mut query = self
.world_mut()
.query_filtered::<Entity, With<PrimaryWindow>>();
let entity = query.single(&self.world());
self.world_mut()
.entity_mut(entity)
.remove::<RawHandleWrapper>();
}
}
if self.lifecycle == AppLifecycle::WillResume {
self.lifecycle = AppLifecycle::Running;
// Trigger the update to enter the running state
should_update = true;
// Trigger the next redraw to refresh the screen immediately
self.redraw_requested = true;
#[cfg(target_os = "android")]
{
// Get windows that are cached but without raw handles. Those window were already created, but got their
// handle wrapper removed when the app was suspended.
let mut query = self.world_mut()
.query_filtered::<(Entity, &Window), (With<CachedWindow>, Without<bevy_window::RawHandleWrapper>)>();
if let Ok((entity, window)) = query.get_single(&self.world()) {
let window = window.clone();
let mut create_window =
SystemState::<CreateWindowParams>::from_world(self.world_mut());
let (
..,
mut winit_windows,
mut adapters,
mut handlers,
accessibility_requested,
) = create_window.get_mut(self.world_mut());
let winit_window = winit_windows.create_window(
event_loop,
entity,
&window,
&mut adapters,
&mut handlers,
&accessibility_requested,
);
let wrapper = RawHandleWrapper::new(winit_window).unwrap();
self.world_mut().entity_mut(entity).insert(wrapper);
}
}
}
// Notifies a lifecycle change
if self.lifecycle != self.previous_lifecycle {
self.previous_lifecycle = self.lifecycle;
self.winit_events.send(self.lifecycle);
}
// This is recorded before running app.update(), to run the next cycle after a correct timeout.
// If the cycle takes more than the wait timeout, it will be re-executed immediately.
let begin_frame_time = Instant::now();
if should_update {
// Not redrawing, but the timeout elapsed.
self.run_app_update();
// Running the app may have changed the WinitSettings resource, so we have to re-extract it.
let (config, windows) = focused_windows_state.get(self.world());
let focused = windows.iter().any(|(_, window)| window.focused);
update_mode = config.update_mode(focused);
}
// The update mode could have been changed, so we need to redraw and force an update
if update_mode != self.update_mode {
// Trigger the next redraw since we're changing the update mode
self.redraw_requested = true;
// Consider the wait as elapsed since it could have been cancelled by a user event
self.wait_elapsed = true;
self.update_mode = update_mode;
}
match update_mode {
UpdateMode::Continuous => {
// per winit's docs on [Window::is_visible](https://docs.rs/winit/latest/winit/window/struct.Window.html#method.is_visible),
// we cannot use the visibility to drive rendering on these platforms
// so we cannot discern whether to beneficially use `Poll` or not?
cfg_if::cfg_if! {
if #[cfg(not(any(
target_arch = "wasm32",
target_os = "android",
target_os = "ios",
all(target_os = "linux", any(feature = "x11", feature = "wayland"))
)))]
{
let winit_windows = self.world().non_send_resource::<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() {
self.redraw_requested = true;
}
}
UpdateMode::Reactive { wait, .. } => {
// Set the next timeout, starting from the instant before running app.update() to avoid frame delays
if let Some(next) = begin_frame_time.checked_add(wait) {
if self.wait_elapsed {
event_loop.set_control_flow(ControlFlow::WaitUntil(next));
}
}
}
}
if self.redraw_requested && self.lifecycle != AppLifecycle::Suspended {
let winit_windows = self.world().non_send_resource::<WinitWindows>();
for window in winit_windows.windows.values() {
window.request_redraw();
}
self.redraw_requested = false;
}
if let Some(app_exit) = self.app.should_exit() {
self.app_exit = Some(app_exit);
event_loop.exit();
}
}
fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
// Mark the state as `WillSuspend`. This will let the schedule run one last time
// before actually suspending to let the application react
self.lifecycle = AppLifecycle::WillSuspend;
}
fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
let world = self.world_mut();
world.clear_all();
}
}
impl<T: Event> WinitAppRunnerState<T> {
fn should_update(&self, update_mode: UpdateMode) -> bool {
let handle_event = match update_mode {
UpdateMode::Continuous => {
self.wait_elapsed
|| self.user_event_received
|| self.window_event_received
|| self.device_event_received
}
UpdateMode::Reactive {
react_to_device_events,
react_to_user_events,
react_to_window_events,
..
} => {
self.wait_elapsed
|| (react_to_device_events && self.device_event_received)
|| (react_to_user_events && self.user_event_received)
|| (react_to_window_events && self.window_event_received)
}
};
handle_event && self.lifecycle.is_active()
}
fn run_app_update(&mut self) {
self.reset_on_update();
self.forward_winit_events();
if self.app.plugins_state() == PluginsState::Cleaned {
self.app.update();
}
}
fn forward_winit_events(&mut self) {
let buffered_events = self.winit_events.drain(..).collect::<Vec<_>>();
if buffered_events.is_empty() {
return;
}
let world = self.world_mut();
for winit_event in buffered_events.iter() {
match winit_event.clone() {
WinitEvent::AppLifecycle(e) => {
world.send_event(e);
}
WinitEvent::CursorEntered(e) => {
world.send_event(e);
}
WinitEvent::CursorLeft(e) => {
world.send_event(e);
}
WinitEvent::CursorMoved(e) => {
world.send_event(e);
}
WinitEvent::FileDragAndDrop(e) => {
world.send_event(e);
}
WinitEvent::Ime(e) => {
world.send_event(e);
}
WinitEvent::ReceivedCharacter(e) => {
world.send_event(e);
}
WinitEvent::RequestRedraw(e) => {
world.send_event(e);
}
WinitEvent::WindowBackendScaleFactorChanged(e) => {
world.send_event(e);
}
WinitEvent::WindowCloseRequested(e) => {
world.send_event(e);
}
WinitEvent::WindowCreated(e) => {
world.send_event(e);
}
WinitEvent::WindowDestroyed(e) => {
world.send_event(e);
}
WinitEvent::WindowFocused(e) => {
world.send_event(e);
}
WinitEvent::WindowMoved(e) => {
world.send_event(e);
}
WinitEvent::WindowOccluded(e) => {
world.send_event(e);
}
WinitEvent::WindowResized(e) => {
world.send_event(e);
}
WinitEvent::WindowScaleFactorChanged(e) => {
world.send_event(e);
}
WinitEvent::WindowThemeChanged(e) => {
world.send_event(e);
}
WinitEvent::MouseButtonInput(e) => {
world.send_event(e);
}
WinitEvent::MouseMotion(e) => {
world.send_event(e);
}
WinitEvent::MouseWheel(e) => {
world.send_event(e);
}
WinitEvent::TouchpadMagnify(e) => {
world.send_event(e);
}
WinitEvent::TouchpadRotate(e) => {
world.send_event(e);
}
WinitEvent::TouchInput(e) => {
world.send_event(e);
}
WinitEvent::KeyboardInput(e) => {
world.send_event(e);
}
}
}
world
.resource_mut::<Events<WinitEvent>>()
.send_batch(buffered_events);
}
}
/// The default [`App::runner`] for the [`WinitPlugin`] plugin.
///
/// Overriding the app's [runner](bevy_app::App::runner) while using `WinitPlugin` will bypass the
/// `EventLoop`.
pub fn winit_runner<T: Event>(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<T>>()
.unwrap();
app.world_mut()
.insert_non_send_resource(event_loop.create_proxy());
let mut runner_state = WinitAppRunnerState::new(app);
trace!("starting winit event loop");
// TODO(clean): the winit docs mention using `spawn` instead of `run` on WASM.
if let Err(err) = event_loop.run_app(&mut runner_state) {
error!("winit event loop returned an error: {err}");
}
// If everything is working correctly then the event loop only exits after it's sent an exit code.
runner_state.app_exit.unwrap_or_else(|| {
error!("Failed to receive a app exit code! This is a bug");
AppExit::error()
})
}
pub(crate) fn react_to_resize(
window_entity: Entity,
window: &mut Mut<'_, Window>,
size: PhysicalSize<u32>,
window_resized: &mut EventWriter<WindowResized>,
) {
window
.resolution
.set_physical_resolution(size.width, size.height);
window_resized.send(WindowResized {
window: window_entity,
width: window.width(),
height: window.height(),
});
}
pub(crate) fn react_to_scale_factor_change(
window_entity: Entity,
window: &mut Mut<'_, Window>,
scale_factor: f64,
window_backend_scale_factor_changed: &mut EventWriter<WindowBackendScaleFactorChanged>,
window_scale_factor_changed: &mut EventWriter<WindowScaleFactorChanged>,
) {
window.resolution.set_scale_factor(scale_factor as f32);
window_backend_scale_factor_changed.send(WindowBackendScaleFactorChanged {
window: window_entity,
scale_factor,
});
let prior_factor = window.resolution.scale_factor();
let scale_factor_override = window.resolution.scale_factor_override();
if scale_factor_override.is_none() && !relative_eq!(scale_factor as f32, prior_factor) {
window_scale_factor_changed.send(WindowScaleFactorChanged {
window: window_entity,
scale_factor,
});
}
}

View file

@ -12,15 +12,14 @@ use bevy_window::{
WindowMode, WindowResized,
};
use winit::{
dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize},
event_loop::EventLoopWindowTarget,
};
use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize};
use winit::event_loop::ActiveEventLoop;
use bevy_ecs::query::With;
#[cfg(target_arch = "wasm32")]
use winit::platform::web::WindowExtWebSys;
use crate::state::react_to_resize;
use crate::{
converters::{
self, convert_enabled_buttons, convert_window_level, convert_window_theme,
@ -36,7 +35,7 @@ use crate::{
/// default values.
#[allow(clippy::too_many_arguments)]
pub fn create_windows<F: QueryFilter + 'static>(
event_loop: &EventLoopWindowTarget<crate::UserEvent>,
event_loop: &ActiveEventLoop,
(
mut commands,
mut created_windows,
@ -47,7 +46,7 @@ pub fn create_windows<F: QueryFilter + 'static>(
accessibility_requested,
): SystemParamItem<CreateWindowParams<F>>,
) {
for (entity, mut window) in &mut created_windows {
for (entity, mut window, handle_holder) in &mut created_windows {
if winit_windows.get_window(entity).is_some() {
continue;
}
@ -80,7 +79,11 @@ pub fn create_windows<F: QueryFilter + 'static>(
});
if let Ok(handle_wrapper) = RawHandleWrapper::new(winit_window) {
commands.entity(entity).insert(handle_wrapper);
let mut entity = commands.entity(entity);
entity.insert(handle_wrapper.clone());
if let Some(handle_holder) = handle_holder {
*handle_holder.0.lock().unwrap() = Some(handle_wrapper);
}
}
#[cfg(target_arch = "wasm32")]
@ -181,13 +184,46 @@ pub(crate) fn changed_windows(
}
}
}
if window.resolution != cache.window.resolution {
let physical_size = PhysicalSize::new(
window.resolution.physical_width(),
window.resolution.physical_height(),
let mut physical_size = winit_window.inner_size();
let cached_physical_size = PhysicalSize::new(
cache.window.physical_width(),
cache.window.physical_height(),
);
if let Some(size_now) = winit_window.request_inner_size(physical_size) {
crate::react_to_resize(&mut window, size_now, &mut window_resized, entity);
let base_scale_factor = window.resolution.base_scale_factor();
// Note: this may be different from `winit`'s base scale factor if
// `scale_factor_override` is set to Some(f32)
let scale_factor = window.scale_factor();
let cached_scale_factor = cache.window.scale_factor();
// Check and update `winit`'s physical size only if the window is not maximized
if scale_factor != cached_scale_factor && !winit_window.is_maximized() {
let logical_size =
if let Some(cached_factor) = cache.window.resolution.scale_factor_override() {
physical_size.to_logical::<f32>(cached_factor as f64)
} else {
physical_size.to_logical::<f32>(base_scale_factor as f64)
};
// Scale factor changed, updating physical and logical size
if let Some(forced_factor) = window.resolution.scale_factor_override() {
// This window is overriding the OS-suggested DPI, so its physical size
// should be set based on the overriding value. Its logical size already
// incorporates any resize constraints.
physical_size = logical_size.to_physical::<u32>(forced_factor as f64);
} else {
physical_size = logical_size.to_physical::<u32>(base_scale_factor as f64);
}
}
if physical_size != cached_physical_size {
if let Some(new_physical_size) = winit_window.request_inner_size(physical_size) {
react_to_resize(entity, &mut window, new_physical_size, &mut window_resized);
}
}
}
@ -202,7 +238,7 @@ pub(crate) fn changed_windows(
}
if window.cursor.icon != cache.window.cursor.icon {
winit_window.set_cursor_icon(converters::convert_cursor_icon(window.cursor.icon));
winit_window.set_cursor(converters::convert_cursor_icon(window.cursor.icon));
}
if window.cursor.grab_mode != cache.window.cursor.grab_mode {

View file

@ -18,9 +18,7 @@ impl WinitSettings {
pub fn game() -> Self {
WinitSettings {
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::ReactiveLowPower {
wait: Duration::from_secs_f64(1.0 / 60.0), // 60Hz
},
unfocused_mode: UpdateMode::reactive_low_power(Duration::from_secs_f64(1.0 / 60.0)), // 60Hz,
}
}
@ -32,12 +30,8 @@ impl WinitSettings {
/// Use the [`EventLoopProxy`](crate::EventLoopProxy) to request a redraw from outside bevy.
pub fn desktop_app() -> Self {
WinitSettings {
focused_mode: UpdateMode::Reactive {
wait: Duration::from_secs(5),
},
unfocused_mode: UpdateMode::ReactiveLowPower {
wait: Duration::from_secs(60),
},
focused_mode: UpdateMode::reactive(Duration::from_secs(5)),
unfocused_mode: UpdateMode::reactive_low_power(Duration::from_secs(60)),
}
}
@ -72,7 +66,7 @@ pub enum UpdateMode {
/// [`AppExit`](bevy_app::AppExit) event appears:
/// - `wait` time has elapsed since the previous update
/// - a redraw has been requested by [`RequestRedraw`](bevy_window::RequestRedraw)
/// - new [window](`winit::event::WindowEvent`) or [raw input](`winit::event::DeviceEvent`)
/// - new [window](`winit::event::WindowEvent`), [raw input](`winit::event::DeviceEvent`), or custom
/// events have appeared
/// - a redraw has been requested with the [`EventLoopProxy`](crate::EventLoopProxy)
Reactive {
@ -81,23 +75,38 @@ pub enum UpdateMode {
/// **Note:** This has no upper limit.
/// The [`App`](bevy_app::App) will wait indefinitely if you set this to [`Duration::MAX`].
wait: Duration,
},
/// The [`App`](bevy_app::App) will update in response to the following, until an
/// [`AppExit`](bevy_app::AppExit) event appears:
/// - `wait` time has elapsed since the previous update
/// - a redraw has been requested by [`RequestRedraw`](bevy_window::RequestRedraw)
/// - new [window events](`winit::event::WindowEvent`) have appeared
/// - a redraw has been requested with the [`EventLoopProxy`](crate::EventLoopProxy)
///
/// **Note:** Unlike [`Reactive`](`UpdateMode::Reactive`), this mode will ignore events that
/// don't come from interacting with a window, like [`MouseMotion`](winit::event::DeviceEvent::MouseMotion).
/// Use this mode if, for example, you only want your app to update when the mouse cursor is
/// moving over a window, not just moving in general. This can greatly reduce power consumption.
ReactiveLowPower {
/// The approximate time from the start of one update to the next.
///
/// **Note:** This has no upper limit.
/// The [`App`](bevy_app::App) will wait indefinitely if you set this to [`Duration::MAX`].
wait: Duration,
/// Reacts to device events, that will wake up the loop if it's in a wait wtate
react_to_device_events: bool,
/// Reacts to user events, that will wake up the loop if it's in a wait wtate
react_to_user_events: bool,
/// Reacts to window events, that will wake up the loop if it's in a wait wtate
react_to_window_events: bool,
},
}
impl UpdateMode {
/// Reactive mode, will update the app for any kind of event
pub fn reactive(wait: Duration) -> Self {
Self::Reactive {
wait,
react_to_device_events: true,
react_to_user_events: true,
react_to_window_events: true,
}
}
/// Low power mode
///
/// Unlike [`Reactive`](`UpdateMode::reactive()`), this will ignore events that
/// don't come from interacting with a window, like [`MouseMotion`](winit::event::DeviceEvent::MouseMotion).
/// Use this if, for example, you only want your app to update when the mouse cursor is
/// moving over a window, not just moving in general. This can greatly reduce power consumption.
pub fn reactive_low_power(wait: Duration) -> Self {
Self::Reactive {
wait,
react_to_device_events: false,
react_to_user_events: true,
react_to_window_events: true,
}
}
}

View file

@ -1,7 +1,6 @@
#![allow(deprecated)]
#![allow(missing_docs)]
use bevy_app::App;
use bevy_ecs::prelude::*;
use bevy_input::keyboard::KeyboardInput;
use bevy_input::touch::TouchInput;
@ -13,9 +12,9 @@ use bevy_reflect::Reflect;
#[cfg(feature = "serialize")]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
use bevy_window::{
ApplicationLifetime, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime,
ReceivedCharacter, RequestRedraw, WindowBackendScaleFactorChanged, WindowCloseRequested,
WindowCreated, WindowDestroyed, WindowFocused, WindowMoved, WindowOccluded, WindowResized,
AppLifecycle, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, ReceivedCharacter,
RequestRedraw, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated,
WindowDestroyed, WindowFocused, WindowMoved, WindowOccluded, WindowResized,
WindowScaleFactorChanged, WindowThemeChanged,
};
@ -33,7 +32,7 @@ use bevy_window::{
reflect(Serialize, Deserialize)
)]
pub enum WinitEvent {
ApplicationLifetime(ApplicationLifetime),
AppLifecycle(AppLifecycle),
CursorEntered(CursorEntered),
CursorLeft(CursorLeft),
CursorMoved(CursorMoved),
@ -64,9 +63,9 @@ pub enum WinitEvent {
KeyboardInput(KeyboardInput),
}
impl From<ApplicationLifetime> for WinitEvent {
fn from(e: ApplicationLifetime) -> Self {
Self::ApplicationLifetime(e)
impl From<AppLifecycle> for WinitEvent {
fn from(e: AppLifecycle) -> Self {
Self::AppLifecycle(e)
}
}
impl From<CursorEntered> for WinitEvent {
@ -189,92 +188,3 @@ impl From<KeyboardInput> for WinitEvent {
Self::KeyboardInput(e)
}
}
/// Forwards buffered [`WinitEvent`] events to the app.
pub(crate) fn forward_winit_events(buffered_events: &mut Vec<WinitEvent>, app: &mut App) {
if buffered_events.is_empty() {
return;
}
for winit_event in buffered_events.iter() {
match winit_event.clone() {
WinitEvent::ApplicationLifetime(e) => {
app.world_mut().send_event(e);
}
WinitEvent::CursorEntered(e) => {
app.world_mut().send_event(e);
}
WinitEvent::CursorLeft(e) => {
app.world_mut().send_event(e);
}
WinitEvent::CursorMoved(e) => {
app.world_mut().send_event(e);
}
WinitEvent::FileDragAndDrop(e) => {
app.world_mut().send_event(e);
}
WinitEvent::Ime(e) => {
app.world_mut().send_event(e);
}
WinitEvent::ReceivedCharacter(e) => {
app.world_mut().send_event(e);
}
WinitEvent::RequestRedraw(e) => {
app.world_mut().send_event(e);
}
WinitEvent::WindowBackendScaleFactorChanged(e) => {
app.world_mut().send_event(e);
}
WinitEvent::WindowCloseRequested(e) => {
app.world_mut().send_event(e);
}
WinitEvent::WindowCreated(e) => {
app.world_mut().send_event(e);
}
WinitEvent::WindowDestroyed(e) => {
app.world_mut().send_event(e);
}
WinitEvent::WindowFocused(e) => {
app.world_mut().send_event(e);
}
WinitEvent::WindowMoved(e) => {
app.world_mut().send_event(e);
}
WinitEvent::WindowOccluded(e) => {
app.world_mut().send_event(e);
}
WinitEvent::WindowResized(e) => {
app.world_mut().send_event(e);
}
WinitEvent::WindowScaleFactorChanged(e) => {
app.world_mut().send_event(e);
}
WinitEvent::WindowThemeChanged(e) => {
app.world_mut().send_event(e);
}
WinitEvent::MouseButtonInput(e) => {
app.world_mut().send_event(e);
}
WinitEvent::MouseMotion(e) => {
app.world_mut().send_event(e);
}
WinitEvent::MouseWheel(e) => {
app.world_mut().send_event(e);
}
WinitEvent::TouchpadMagnify(e) => {
app.world_mut().send_event(e);
}
WinitEvent::TouchpadRotate(e) => {
app.world_mut().send_event(e);
}
WinitEvent::TouchInput(e) => {
app.world_mut().send_event(e);
}
WinitEvent::KeyboardInput(e) => {
app.world_mut().send_event(e);
}
}
}
app.world_mut()
.resource_mut::<Events<WinitEvent>>()
.send_batch(buffered_events.drain(..));
}

View file

@ -9,11 +9,15 @@ use bevy_window::{
use winit::{
dpi::{LogicalSize, PhysicalPosition},
monitor::MonitorHandle,
event_loop::ActiveEventLoop,
monitor::{MonitorHandle, VideoModeHandle},
window::{CursorGrabMode as WinitCursorGrabMode, Fullscreen, Window as WinitWindow, WindowId},
};
use crate::{
accessibility::{prepare_accessibility_for_window, AccessKitAdapters, WinitActionHandlers},
accessibility::{
prepare_accessibility_for_window, AccessKitAdapters, WinitActionRequestHandlers,
},
converters::{convert_enabled_buttons, convert_window_level, convert_window_theme},
};
@ -22,11 +26,11 @@ use crate::{
#[derive(Debug, Default)]
pub struct WinitWindows {
/// Stores [`winit`] windows by window identifier.
pub windows: HashMap<winit::window::WindowId, WindowWrapper<winit::window::Window>>,
pub windows: HashMap<WindowId, WindowWrapper<WinitWindow>>,
/// Maps entities to `winit` window identifiers.
pub entity_to_winit: EntityHashMap<winit::window::WindowId>,
pub entity_to_winit: EntityHashMap<WindowId>,
/// Maps `winit` window identifiers to entities.
pub winit_to_entity: HashMap<winit::window::WindowId, Entity>,
pub winit_to_entity: HashMap<WindowId, Entity>,
// Many `winit` window functions (e.g. `set_window_icon`) can only be called on the main thread.
// If they're called on other threads, the program might hang. This marker indicates that this
// type is not thread-safe and will be `!Send` and `!Sync`.
@ -37,23 +41,22 @@ impl WinitWindows {
/// Creates a `winit` window and associates it with our entity.
pub fn create_window(
&mut self,
event_loop: &winit::event_loop::EventLoopWindowTarget<crate::UserEvent>,
event_loop: &ActiveEventLoop,
entity: Entity,
window: &Window,
adapters: &mut AccessKitAdapters,
handlers: &mut WinitActionHandlers,
handlers: &mut WinitActionRequestHandlers,
accessibility_requested: &AccessibilityRequested,
) -> &WindowWrapper<winit::window::Window> {
let mut winit_window_builder = winit::window::WindowBuilder::new();
) -> &WindowWrapper<WinitWindow> {
let mut winit_window_attributes = WinitWindow::default_attributes();
// Due to a UIA limitation, winit windows need to be invisible for the
// AccessKit adapter is initialized.
winit_window_builder = winit_window_builder.with_visible(false);
winit_window_attributes = winit_window_attributes.with_visible(false);
winit_window_builder = match window.mode {
WindowMode::BorderlessFullscreen => winit_window_builder.with_fullscreen(Some(
winit::window::Fullscreen::Borderless(event_loop.primary_monitor()),
)),
winit_window_attributes = match window.mode {
WindowMode::BorderlessFullscreen => winit_window_attributes
.with_fullscreen(Some(Fullscreen::Borderless(event_loop.primary_monitor()))),
mode @ (WindowMode::Fullscreen | WindowMode::SizedFullscreen) => {
if let Some(primary_monitor) = event_loop.primary_monitor() {
let videomode = match mode {
@ -66,11 +69,10 @@ impl WinitWindows {
_ => unreachable!(),
};
winit_window_builder
.with_fullscreen(Some(winit::window::Fullscreen::Exclusive(videomode)))
winit_window_attributes.with_fullscreen(Some(Fullscreen::Exclusive(videomode)))
} else {
warn!("Could not determine primary monitor, ignoring exclusive fullscreen request for window {:?}", window.title);
winit_window_builder
winit_window_attributes
}
}
WindowMode::Windowed => {
@ -81,19 +83,20 @@ impl WinitWindows {
event_loop.primary_monitor(),
None,
) {
winit_window_builder = winit_window_builder.with_position(position);
winit_window_attributes = winit_window_attributes.with_position(position);
}
let logical_size = LogicalSize::new(window.width(), window.height());
if let Some(sf) = window.resolution.scale_factor_override() {
winit_window_builder.with_inner_size(logical_size.to_physical::<f64>(sf.into()))
winit_window_attributes
.with_inner_size(logical_size.to_physical::<f64>(sf.into()))
} else {
winit_window_builder.with_inner_size(logical_size)
winit_window_attributes.with_inner_size(logical_size)
}
}
};
winit_window_builder = winit_window_builder
winit_window_attributes = winit_window_attributes
.with_window_level(convert_window_level(window.window_level))
.with_theme(window.window_theme.map(convert_window_theme))
.with_resizable(window.resizable)
@ -104,8 +107,9 @@ impl WinitWindows {
#[cfg(target_os = "windows")]
{
use winit::platform::windows::WindowBuilderExtWindows;
winit_window_builder = winit_window_builder.with_skip_taskbar(window.skip_taskbar);
use winit::platform::windows::WindowAttributesExtWindows;
winit_window_attributes =
winit_window_attributes.with_skip_taskbar(window.skip_taskbar);
}
#[cfg(any(
@ -128,11 +132,12 @@ impl WinitWindows {
)
))]
{
winit_window_builder = winit::platform::wayland::WindowBuilderExtWayland::with_name(
winit_window_builder,
name.clone(),
"",
);
winit_window_attributes =
winit::platform::wayland::WindowAttributesExtWayland::with_name(
winit_window_attributes,
name.clone(),
"",
);
}
#[cfg(all(
@ -146,17 +151,17 @@ impl WinitWindows {
)
))]
{
winit_window_builder = winit::platform::x11::WindowBuilderExtX11::with_name(
winit_window_builder,
winit_window_attributes = winit::platform::x11::WindowAttributesExtX11::with_name(
winit_window_attributes,
name.clone(),
"",
);
}
#[cfg(target_os = "windows")]
{
winit_window_builder =
winit::platform::windows::WindowBuilderExtWindows::with_class_name(
winit_window_builder,
winit_window_attributes =
winit::platform::windows::WindowAttributesExtWindows::with_class_name(
winit_window_attributes,
name.clone(),
);
}
@ -172,22 +177,22 @@ impl WinitWindows {
height: constraints.max_height,
};
let winit_window_builder =
let winit_window_attributes =
if constraints.max_width.is_finite() && constraints.max_height.is_finite() {
winit_window_builder
winit_window_attributes
.with_min_inner_size(min_inner_size)
.with_max_inner_size(max_inner_size)
} else {
winit_window_builder.with_min_inner_size(min_inner_size)
winit_window_attributes.with_min_inner_size(min_inner_size)
};
#[allow(unused_mut)]
let mut winit_window_builder = winit_window_builder.with_title(window.title.as_str());
let mut winit_window_attributes = winit_window_attributes.with_title(window.title.as_str());
#[cfg(target_arch = "wasm32")]
{
use wasm_bindgen::JsCast;
use winit::platform::web::WindowBuilderExtWebSys;
use winit::platform::web::WindowAttributesExtWebSys;
if let Some(selector) = &window.canvas {
let window = web_sys::window().unwrap();
@ -197,18 +202,18 @@ impl WinitWindows {
.expect("Cannot query for canvas element.");
if let Some(canvas) = canvas {
let canvas = canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok();
winit_window_builder = winit_window_builder.with_canvas(canvas);
winit_window_attributes = winit_window_attributes.with_canvas(canvas);
} else {
panic!("Cannot find element: {}.", selector);
}
}
winit_window_builder =
winit_window_builder.with_prevent_default(window.prevent_default_event_handling);
winit_window_builder = winit_window_builder.with_append(true);
winit_window_attributes =
winit_window_attributes.with_prevent_default(window.prevent_default_event_handling);
winit_window_attributes = winit_window_attributes.with_append(true);
}
let winit_window = winit_window_builder.build(event_loop).unwrap();
let winit_window = event_loop.create_window(winit_window_attributes).unwrap();
let name = window.title.clone();
prepare_accessibility_for_window(
&winit_window,
@ -247,7 +252,7 @@ impl WinitWindows {
}
/// Get the winit window that is associated with our entity.
pub fn get_window(&self, entity: Entity) -> Option<&WindowWrapper<winit::window::Window>> {
pub fn get_window(&self, entity: Entity) -> Option<&WindowWrapper<WinitWindow>> {
self.entity_to_winit
.get(&entity)
.and_then(|winit_id| self.windows.get(winit_id))
@ -256,17 +261,14 @@ impl WinitWindows {
/// Get the entity associated with the winit window id.
///
/// This is mostly just an intermediary step between us and winit.
pub fn get_window_entity(&self, winit_id: winit::window::WindowId) -> Option<Entity> {
pub fn get_window_entity(&self, winit_id: WindowId) -> Option<Entity> {
self.winit_to_entity.get(&winit_id).cloned()
}
/// Remove a window from winit.
///
/// This should mostly just be called when the window is closing.
pub fn remove_window(
&mut self,
entity: Entity,
) -> Option<WindowWrapper<winit::window::Window>> {
pub fn remove_window(&mut self, entity: Entity) -> Option<WindowWrapper<WinitWindow>> {
let winit_id = self.entity_to_winit.remove(&entity)?;
self.winit_to_entity.remove(&winit_id);
self.windows.remove(&winit_id)
@ -276,11 +278,7 @@ impl WinitWindows {
/// Gets the "best" video mode which fits the given dimensions.
///
/// The heuristic for "best" prioritizes width, height, and refresh rate in that order.
pub fn get_fitting_videomode(
monitor: &MonitorHandle,
width: u32,
height: u32,
) -> winit::monitor::VideoMode {
pub fn get_fitting_videomode(monitor: &MonitorHandle, width: u32, height: u32) -> VideoModeHandle {
let mut modes = monitor.video_modes().collect::<Vec<_>>();
fn abs_diff(a: u32, b: u32) -> u32 {
@ -308,10 +306,10 @@ pub fn get_fitting_videomode(
modes.first().unwrap().clone()
}
/// Gets the "best" videomode from a monitor.
/// Gets the "best" video-mode handle from a monitor.
///
/// The heuristic for "best" prioritizes width, height, and refresh rate in that order.
pub fn get_best_videomode(monitor: &MonitorHandle) -> winit::monitor::VideoMode {
pub fn get_best_videomode(monitor: &MonitorHandle) -> VideoModeHandle {
let mut modes = monitor.video_modes().collect::<Vec<_>>();
modes.sort_by(|a, b| {
use std::cmp::Ordering::*;
@ -329,15 +327,15 @@ pub fn get_best_videomode(monitor: &MonitorHandle) -> winit::monitor::VideoMode
modes.first().unwrap().clone()
}
pub(crate) fn attempt_grab(winit_window: &winit::window::Window, grab_mode: CursorGrabMode) {
pub(crate) fn attempt_grab(winit_window: &WinitWindow, grab_mode: CursorGrabMode) {
let grab_result = match grab_mode {
CursorGrabMode::None => winit_window.set_cursor_grab(winit::window::CursorGrabMode::None),
CursorGrabMode::None => winit_window.set_cursor_grab(WinitCursorGrabMode::None),
CursorGrabMode::Confined => winit_window
.set_cursor_grab(winit::window::CursorGrabMode::Confined)
.or_else(|_e| winit_window.set_cursor_grab(winit::window::CursorGrabMode::Locked)),
.set_cursor_grab(WinitCursorGrabMode::Confined)
.or_else(|_e| winit_window.set_cursor_grab(WinitCursorGrabMode::Locked)),
CursorGrabMode::Locked => winit_window
.set_cursor_grab(winit::window::CursorGrabMode::Locked)
.or_else(|_e| winit_window.set_cursor_grab(winit::window::CursorGrabMode::Confined)),
.set_cursor_grab(WinitCursorGrabMode::Locked)
.or_else(|_e| winit_window.set_cursor_grab(WinitCursorGrabMode::Confined)),
};
if let Err(err) = grab_result {

View file

@ -459,6 +459,7 @@ Example | Description
Example | Description
--- | ---
[Clear Color](../examples/window/clear_color.rs) | Creates a solid color window
[Custom User Event](../examples/window/custom_user_event.rs) | Handles custom user events within the event loop
[Low Power](../examples/window/low_power.rs) | Demonstrates settings to reduce power use for bevy applications
[Multiple Windows](../examples/window/multiple_windows.rs) | Demonstrates creating multiple windows, and rendering to them
[Scale Factor Override](../examples/window/scale_factor_override.rs) | Illustrates how to customize the default window settings

View file

@ -4,7 +4,7 @@ use bevy::{
color::palettes::basic::*,
input::touch::TouchPhase,
prelude::*,
window::{ApplicationLifetime, WindowMode},
window::{AppLifecycle, WindowMode},
};
// the `bevy_main` proc_macro generates the required boilerplate for iOS and Android
@ -166,14 +166,18 @@ fn setup_music(asset_server: Res<AssetServer>, mut commands: Commands) {
// Pause audio when app goes into background and resume when it returns.
// This is handled by the OS on iOS, but not on Android.
fn handle_lifetime(
mut lifetime_events: EventReader<ApplicationLifetime>,
mut lifecycle_events: EventReader<AppLifecycle>,
music_controller: Query<&AudioSink>,
) {
for event in lifetime_events.read() {
let Ok(music_controller) = music_controller.get_single() else {
return;
};
for event in lifecycle_events.read() {
match event {
ApplicationLifetime::Suspended => music_controller.single().pause(),
ApplicationLifetime::Resumed => music_controller.single().play(),
ApplicationLifetime::Started => (),
AppLifecycle::Idle | AppLifecycle::WillSuspend | AppLifecycle::WillResume => {}
AppLifecycle::Suspended => music_controller.pause(),
AppLifecycle::Running => music_controller.play(),
}
}
}

View file

@ -0,0 +1,122 @@
//! Shows how to create a custom event that can be handled by `winit`'s event loop.
use bevy::prelude::*;
use bevy::winit::{EventLoopProxy, WakeUp, WinitPlugin};
use std::fmt::Formatter;
use std::sync::OnceLock;
#[derive(Default, Debug, Event)]
enum CustomEvent {
#[default]
WakeUp,
Key(char),
}
impl std::fmt::Display for CustomEvent {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::WakeUp => write!(f, "Wake up"),
Self::Key(ch) => write!(f, "Key: {ch}"),
}
}
}
static EVENT_LOOP_PROXY: OnceLock<EventLoopProxy<CustomEvent>> = OnceLock::new();
fn main() {
let winit_plugin = WinitPlugin::<CustomEvent>::default();
App::new()
.add_plugins(
DefaultPlugins
.build()
// Only one event type can be handled at once
// so we must disable the default event type
.disable::<WinitPlugin<WakeUp>>()
.add(winit_plugin),
)
.add_systems(
Startup,
(
setup,
expose_event_loop_proxy,
#[cfg(target_arch = "wasm32")]
wasm::setup_js_closure,
),
)
.add_systems(Update, (send_event, handle_event))
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
}
fn send_event(input: Res<ButtonInput<KeyCode>>) {
let Some(event_loop_proxy) = EVENT_LOOP_PROXY.get() else {
return;
};
if input.just_pressed(KeyCode::Space) {
let _ = event_loop_proxy.send_event(CustomEvent::WakeUp);
}
// This simulates sending a custom event through an external thread.
#[cfg(not(target_arch = "wasm32"))]
if input.just_pressed(KeyCode::KeyE) {
let handler = std::thread::spawn(|| {
let _ = event_loop_proxy.send_event(CustomEvent::Key('e'));
});
handler.join().unwrap();
}
}
fn expose_event_loop_proxy(event_loop_proxy: NonSend<EventLoopProxy<CustomEvent>>) {
EVENT_LOOP_PROXY.set((*event_loop_proxy).clone()).unwrap();
}
fn handle_event(mut events: EventReader<CustomEvent>) {
for evt in events.read() {
info!("Received event: {evt:?}");
}
}
/// Since the [`EventLoopProxy`] can be exposed to the javascript environment, it can
/// be used to send events inside the loop, to be handled by a system or simply to wake up
/// the loop if that's currently waiting for a timeout or a user event.
#[cfg(target_arch = "wasm32")]
pub(crate) mod wasm {
use crate::{CustomEvent, EVENT_LOOP_PROXY};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::KeyboardEvent;
pub(crate) fn setup_js_closure() {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let closure = Closure::wrap(Box::new(move |event: KeyboardEvent| {
let key = event.key();
if key == "e" {
send_custom_event('e').unwrap();
}
}) as Box<dyn FnMut(KeyboardEvent)>);
document
.add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())
.unwrap();
closure.forget();
}
fn send_custom_event(ch: char) -> Result<(), String> {
if let Some(proxy) = EVENT_LOOP_PROXY.get() {
proxy
.send_event(CustomEvent::Key(ch))
.map_err(|_| "Failed to send event".to_string())
} else {
Err("Event loop proxy not found".to_string())
}
}
}

View file

@ -3,10 +3,12 @@
//! This is useful for making desktop applications, or any other program that doesn't need to be
//! running the event loop non-stop.
use bevy::window::WindowResolution;
use bevy::winit::WakeUp;
use bevy::{
prelude::*,
utils::Duration,
window::{PresentMode, RequestRedraw, WindowPlugin},
window::{PresentMode, WindowPlugin},
winit::{EventLoopProxy, WinitSettings},
};
@ -19,15 +21,14 @@ fn main() {
// You can also customize update behavior with the fields of [`WinitSettings`]
.insert_resource(WinitSettings {
focused_mode: bevy::winit::UpdateMode::Continuous,
unfocused_mode: bevy::winit::UpdateMode::ReactiveLowPower {
wait: Duration::from_millis(10),
},
unfocused_mode: bevy::winit::UpdateMode::reactive_low_power(Duration::from_millis(10)),
})
.insert_resource(ExampleMode::Game)
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
// Turn off vsync to maximize CPU/GPU usage
present_mode: PresentMode::AutoNoVsync,
resolution: WindowResolution::new(800., 640.).with_scale_factor_override(1.),
..default()
}),
..default()
@ -56,7 +57,7 @@ enum ExampleMode {
fn update_winit(
mode: Res<ExampleMode>,
mut winit_config: ResMut<WinitSettings>,
event_loop_proxy: NonSend<EventLoopProxy>,
event_loop_proxy: NonSend<EventLoopProxy<WakeUp>>,
) {
use ExampleMode::*;
*winit_config = match *mode {
@ -78,7 +79,10 @@ fn update_winit(
// (e.g. the mouse hovers over a visible part of the out of focus window), a
// [`RequestRedraw`] event is received, or one minute has passed without the app
// updating.
WinitSettings::desktop_app()
WinitSettings {
focused_mode: bevy::winit::UpdateMode::reactive(Duration::from_secs(1)),
unfocused_mode: bevy::winit::UpdateMode::reactive_low_power(Duration::from_secs(5)),
}
}
ApplicationWithRedraw => {
// Sending a `RequestRedraw` event is useful when you want the app to update the next
@ -87,7 +91,7 @@ fn update_winit(
// when there are no inputs, so you send redraw requests while the animation is playing.
// Note that in this example the RequestRedraw winit event will make the app run in the same
// way as continuous
let _ = event_loop_proxy.send_event(RequestRedraw);
let _ = event_loop_proxy.send_event(WakeUp);
WinitSettings::desktop_app()
}
};

View file

@ -3,6 +3,9 @@
use bevy::{prelude::*, window::WindowResolution};
#[derive(Component)]
struct CustomText;
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
@ -39,7 +42,7 @@ fn setup(mut commands: Commands) {
parent
.spawn(NodeBundle {
style: Style {
width: Val::Px(200.0),
width: Val::Px(300.0),
height: Val::Percent(100.0),
border: UiRect::all(Val::Px(2.0)),
..default()
@ -48,7 +51,8 @@ fn setup(mut commands: Commands) {
..default()
})
.with_children(|parent| {
parent.spawn(
parent.spawn((
CustomText,
TextBundle::from_section(
"Example text",
TextStyle {
@ -60,19 +64,32 @@ fn setup(mut commands: Commands) {
align_self: AlignSelf::FlexEnd,
..default()
}),
);
));
});
});
}
/// Set the title of the window to the current override
fn display_override(mut windows: Query<&mut Window>) {
fn display_override(
mut windows: Query<&mut Window>,
mut custom_text: Query<&mut Text, With<CustomText>>,
) {
let mut window = windows.single_mut();
window.title = format!(
"Scale override: {:?}",
window.resolution.scale_factor_override()
let text = format!(
"Scale factor: {:.1} {}",
window.scale_factor(),
if window.resolution.scale_factor_override().is_some() {
"(overridden)"
} else {
"(default)"
}
);
window.title.clone_from(&text);
let mut custom_text = custom_text.single_mut();
custom_text.sections[0].value = text;
}
/// This system toggles scale factor overrides when enter is pressed

View file

@ -1,17 +1,17 @@
diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs
index 63d79b1d2..83ed56293 100644
--- a/crates/bevy_winit/src/lib.rs
+++ b/crates/bevy_winit/src/lib.rs
@@ -429,6 +429,12 @@ fn handle_winit_event(
diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs
index df0aab42d..6e28a6e9c 100644
--- a/crates/bevy_winit/src/state.rs
+++ b/crates/bevy_winit/src/state.rs
@@ -208,6 +208,12 @@ impl<T: Event> ApplicationHandler<T> for WinitAppRunnerState<T> {
}
}
runner_state.window_event_received = true;
+ window_resized.send(WindowResized {
+ window,
+ width: win.width(),
+ height: win.height(),
+ });
+ window_resized.send(WindowResized {
+ window,
+ width: win.width(),
+ height: win.height(),
+ });
+
match event {
WindowEvent::Resized(size) => {
react_to_resize(&mut win, size, &mut window_resized, window);
match event {
WindowEvent::Resized(size) => {
react_to_resize(window, &mut win, size, &mut window_resized);

View file

@ -1,18 +1,14 @@
diff --git a/crates/bevy_winit/src/winit_config.rs b/crates/bevy_winit/src/winit_config.rs
index f2cb424ec..e68e01de0 100644
index 104384086..6e3c8dd83 100644
--- a/crates/bevy_winit/src/winit_config.rs
+++ b/crates/bevy_winit/src/winit_config.rs
@@ -31,14 +31,7 @@ impl WinitSettings {
@@ -29,10 +29,7 @@ impl WinitSettings {
///
/// Use the [`EventLoopProxy`](crate::EventLoopProxy) to request a redraw from outside bevy.
pub fn desktop_app() -> Self {
- WinitSettings {
- focused_mode: UpdateMode::Reactive {
- wait: Duration::from_secs(5),
- },
- unfocused_mode: UpdateMode::ReactiveLowPower {
- wait: Duration::from_secs(60),
- },
- focused_mode: UpdateMode::reactive(Duration::from_secs(5)),
- unfocused_mode: UpdateMode::reactive_low_power(Duration::from_secs(60)),
- }
+ Self::default()
}