Add WinitEvent aggregate event for synchronized window event reading (#12100)

# Objective

- Allow users to read window events in the sequence they appeared. This
is important for precise input handling when there are multiple input
events in a single frame (e.g. click and release vs release and click).

## Solution

- Add a mega-enum `WinitEvent` that collects window events, and send
those alongside the existing more granular window events.

---

## Changelog

- Added `WinitEvent` event that aggregates all window events into a
synchronized event stream.
This commit is contained in:
UkoeHB 2024-03-03 17:51:53 -06:00 committed by GitHub
parent faa2ce4d55
commit ef8a617e12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 331 additions and 35 deletions

View file

@ -23,6 +23,7 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
bevy_input = { path = "../bevy_input", version = "0.14.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.14.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }

View file

@ -9,6 +9,7 @@ pub mod accessibility;
mod converters;
mod system;
mod winit_config;
pub mod winit_event;
mod winit_windows;
use approx::relative_eq;
@ -17,6 +18,7 @@ use bevy_utils::{Duration, Instant};
use system::{changed_windows, create_windows, despawn_windows, CachedWindow};
use winit::dpi::{LogicalSize, PhysicalSize};
pub use winit_config::*;
pub use winit_event::*;
pub use winit_windows::*;
use bevy_app::{App, AppExit, Last, Plugin, PluginsState};
@ -117,6 +119,7 @@ impl Plugin for WinitPlugin {
app.init_non_send_resource::<WinitWindows>()
.init_resource::<WinitSettings>()
.add_event::<WinitEvent>()
.set_runner(winit_runner)
.add_systems(
Last,
@ -158,11 +161,11 @@ impl Plugin for WinitPlugin {
}
trait AppSendEvent {
fn send_event<E: bevy_ecs::event::Event>(&mut self, event: E);
fn send(&mut self, event: impl Into<WinitEvent>);
}
impl AppSendEvent for App {
fn send_event<E: bevy_ecs::event::Event>(&mut self, event: E) {
self.world.send_event(event);
impl AppSendEvent for Vec<WinitEvent> {
fn send(&mut self, event: impl Into<WinitEvent>) {
self.push(Into::<WinitEvent>::into(event));
}
}
@ -276,6 +279,7 @@ pub fn winit_runner(mut app: App) {
let mut create_window =
SystemState::<CreateWindowParams<Added<Window>>>::from_world(&mut app.world);
let mut winit_events = Vec::default();
// set up the event loop
let event_handler = move |event, event_loop: &EventLoopWindowTarget<()>| {
handle_winit_event(
@ -286,6 +290,7 @@ pub fn winit_runner(mut app: App) {
&mut event_writer_system_state,
&mut focused_windows_state,
&mut redraw_event_reader,
&mut winit_events,
event,
event_loop,
);
@ -312,6 +317,7 @@ fn handle_winit_event(
)>,
focused_windows_state: &mut SystemState<(Res<WinitSettings>, Query<&Window>)>,
redraw_event_reader: &mut ManualEventReader<RequestRedraw>,
winit_events: &mut Vec<WinitEvent>,
event: Event<()>,
event_loop: &EventLoopWindowTarget<()>,
) {
@ -388,6 +394,7 @@ fn handle_winit_event(
create_window,
app_exit_event_reader,
redraw_event_reader,
winit_events,
);
if runner_state.active != ActiveState::Suspended {
event_loop.set_control_flow(ControlFlow::Poll);
@ -432,15 +439,15 @@ fn handle_winit_event(
WindowEvent::Resized(size) => {
react_to_resize(&mut win, size, &mut window_resized, window);
}
WindowEvent::CloseRequested => app.send_event(WindowCloseRequested { 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();
app.send_event(ReceivedCharacter { window, char });
winit_events.send(ReceivedCharacter { window, char });
}
}
app.send_event(converters::convert_keyboard_input(event, window));
winit_events.send(converters::convert_keyboard_input(event, window));
}
WindowEvent::CursorMoved { position, .. } => {
let physical_position = DVec2::new(position.x, position.y);
@ -453,35 +460,35 @@ fn handle_winit_event(
win.set_physical_cursor_position(Some(physical_position));
let position =
(physical_position / win.resolution.scale_factor() as f64).as_vec2();
app.send_event(CursorMoved {
winit_events.send(CursorMoved {
window,
position,
delta,
});
}
WindowEvent::CursorEntered { .. } => {
app.send_event(CursorEntered { window });
winit_events.send(CursorEntered { window });
}
WindowEvent::CursorLeft { .. } => {
win.set_physical_cursor_position(None);
app.send_event(CursorLeft { window });
winit_events.send(CursorLeft { window });
}
WindowEvent::MouseInput { state, button, .. } => {
app.send_event(MouseButtonInput {
winit_events.send(MouseButtonInput {
button: converters::convert_mouse_button(button),
state: converters::convert_element_state(state),
window,
});
}
WindowEvent::TouchpadMagnify { delta, .. } => {
app.send_event(TouchpadMagnify(delta as f32));
winit_events.send(TouchpadMagnify(delta as f32));
}
WindowEvent::TouchpadRotate { delta, .. } => {
app.send_event(TouchpadRotate(delta));
winit_events.send(TouchpadRotate(delta));
}
WindowEvent::MouseWheel { delta, .. } => match delta {
event::MouseScrollDelta::LineDelta(x, y) => {
app.send_event(MouseWheel {
winit_events.send(MouseWheel {
unit: MouseScrollUnit::Line,
x,
y,
@ -489,7 +496,7 @@ fn handle_winit_event(
});
}
event::MouseScrollDelta::PixelDelta(p) => {
app.send_event(MouseWheel {
winit_events.send(MouseWheel {
unit: MouseScrollUnit::Pixel,
x: p.x as f32,
y: p.y as f32,
@ -501,7 +508,7 @@ fn handle_winit_event(
let location = touch
.location
.to_logical(win.resolution.scale_factor() as f64);
app.send_event(converters::convert_touch_input(touch, location, window));
winit_events.send(converters::convert_touch_input(touch, location, window));
}
WindowEvent::ScaleFactorChanged {
scale_factor,
@ -536,19 +543,19 @@ fn handle_winit_event(
win.resolution
.set_physical_resolution(new_inner_size.width, new_inner_size.height);
app.send_event(WindowBackendScaleFactorChanged {
winit_events.send(WindowBackendScaleFactorChanged {
window,
scale_factor,
});
if scale_factor_override.is_none() && !relative_eq!(new_factor, prior_factor) {
app.send_event(WindowScaleFactorChanged {
winit_events.send(WindowScaleFactorChanged {
window,
scale_factor,
});
}
if !width_equal || !height_equal {
app.send_event(WindowResized {
winit_events.send(WindowResized {
window,
width: new_logical_width,
height: new_logical_height,
@ -557,51 +564,51 @@ fn handle_winit_event(
}
WindowEvent::Focused(focused) => {
win.focused = focused;
app.send_event(WindowFocused { window, focused });
winit_events.send(WindowFocused { window, focused });
}
WindowEvent::Occluded(occluded) => {
app.send_event(WindowOccluded { window, occluded });
winit_events.send(WindowOccluded { window, occluded });
}
WindowEvent::DroppedFile(path_buf) => {
app.send_event(FileDragAndDrop::DroppedFile { window, path_buf });
winit_events.send(FileDragAndDrop::DroppedFile { window, path_buf });
}
WindowEvent::HoveredFile(path_buf) => {
app.send_event(FileDragAndDrop::HoveredFile { window, path_buf });
winit_events.send(FileDragAndDrop::HoveredFile { window, path_buf });
}
WindowEvent::HoveredFileCancelled => {
app.send_event(FileDragAndDrop::HoveredFileCanceled { window });
winit_events.send(FileDragAndDrop::HoveredFileCanceled { window });
}
WindowEvent::Moved(position) => {
let position = ivec2(position.x, position.y);
win.position.set(position);
app.send_event(WindowMoved { window, position });
winit_events.send(WindowMoved { window, position });
}
WindowEvent::Ime(event) => match event {
event::Ime::Preedit(value, cursor) => {
app.send_event(Ime::Preedit {
winit_events.send(Ime::Preedit {
window,
value,
cursor,
});
}
event::Ime::Commit(value) => {
app.send_event(Ime::Commit { window, value });
winit_events.send(Ime::Commit { window, value });
}
event::Ime::Enabled => {
app.send_event(Ime::Enabled { window });
winit_events.send(Ime::Enabled { window });
}
event::Ime::Disabled => {
app.send_event(Ime::Disabled { window });
winit_events.send(Ime::Disabled { window });
}
},
WindowEvent::ThemeChanged(theme) => {
app.send_event(WindowThemeChanged {
winit_events.send(WindowThemeChanged {
window,
theme: convert_winit_theme(theme),
});
}
WindowEvent::Destroyed => {
app.send_event(WindowDestroyed { window });
winit_events.send(WindowDestroyed { window });
}
WindowEvent::RedrawRequested => {
run_app_update_if_should(
@ -612,6 +619,7 @@ fn handle_winit_event(
create_window,
app_exit_event_reader,
redraw_event_reader,
winit_events,
);
}
_ => {}
@ -628,11 +636,11 @@ fn handle_winit_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);
app.send_event(MouseMotion { delta });
winit_events.send(MouseMotion { delta });
}
}
Event::Suspended => {
app.send_event(ApplicationLifetime::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.active = ActiveState::WillSuspend;
@ -647,8 +655,8 @@ fn handle_winit_event(
}
match runner_state.active {
ActiveState::NotYetStarted => app.send_event(ApplicationLifetime::Started),
_ => app.send_event(ApplicationLifetime::Resumed),
ActiveState::NotYetStarted => winit_events.send(ApplicationLifetime::Started),
_ => winit_events.send(ApplicationLifetime::Resumed),
}
runner_state.active = ActiveState::Active;
runner_state.redraw_requested = true;
@ -692,8 +700,14 @@ fn handle_winit_event(
}
_ => (),
}
// 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);
}
#[allow(clippy::too_many_arguments)]
fn run_app_update_if_should(
runner_state: &mut WinitAppRunnerState,
app: &mut App,
@ -702,12 +716,16 @@ fn run_app_update_if_should(
create_window: &mut SystemState<CreateWindowParams<Added<Window>>>,
app_exit_event_reader: &mut ManualEventReader<AppExit>,
redraw_event_reader: &mut ManualEventReader<RequestRedraw>,
winit_events: &mut Vec<WinitEvent>,
) {
runner_state.reset_on_update();
if !runner_state.active.should_run() {
return;
}
forward_winit_events(winit_events, app);
if runner_state.active == ActiveState::WillSuspend {
runner_state.active = ActiveState::Suspended;
#[cfg(target_os = "android")]

View file

@ -0,0 +1,277 @@
#![allow(missing_docs)]
use bevy_app::App;
use bevy_ecs::prelude::*;
use bevy_input::keyboard::KeyboardInput;
use bevy_input::touch::TouchInput;
use bevy_input::{
mouse::{MouseButtonInput, MouseMotion, MouseWheel},
touchpad::{TouchpadMagnify, TouchpadRotate},
};
use bevy_reflect::Reflect;
use bevy_window::{
ApplicationLifetime, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime,
ReceivedCharacter, RequestRedraw, WindowBackendScaleFactorChanged, WindowCloseRequested,
WindowCreated, WindowDestroyed, WindowFocused, WindowMoved, WindowOccluded, WindowResized,
WindowScaleFactorChanged, WindowThemeChanged,
};
/// Wraps all `bevy_window` events in a common enum.
///
/// Read these events with `EventReader<WinitEvent>` if you need to
/// access window events in the order they were received from `winit`.
/// Otherwise, the event types are individually readable with
/// `EventReader<E>` (e.g. `EventReader<KeyboardInput>`).
#[derive(Event, Debug, Clone, PartialEq, Reflect)]
#[reflect(Debug, PartialEq)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum WinitEvent {
ApplicationLifetime(ApplicationLifetime),
CursorEntered(CursorEntered),
CursorLeft(CursorLeft),
CursorMoved(CursorMoved),
FileDragAndDrop(FileDragAndDrop),
Ime(Ime),
ReceivedCharacter(ReceivedCharacter),
RequestRedraw(RequestRedraw),
WindowBackendScaleFactorChanged(WindowBackendScaleFactorChanged),
WindowCloseRequested(WindowCloseRequested),
WindowCreated(WindowCreated),
WindowDestroyed(WindowDestroyed),
WindowFocused(WindowFocused),
WindowMoved(WindowMoved),
WindowOccluded(WindowOccluded),
WindowResized(WindowResized),
WindowScaleFactorChanged(WindowScaleFactorChanged),
WindowThemeChanged(WindowThemeChanged),
MouseButtonInput(MouseButtonInput),
MouseMotion(MouseMotion),
MouseWheel(MouseWheel),
TouchpadMagnify(TouchpadMagnify),
TouchpadRotate(TouchpadRotate),
TouchInput(TouchInput),
KeyboardInput(KeyboardInput),
}
impl From<ApplicationLifetime> for WinitEvent {
fn from(e: ApplicationLifetime) -> Self {
Self::ApplicationLifetime(e)
}
}
impl From<CursorEntered> for WinitEvent {
fn from(e: CursorEntered) -> Self {
Self::CursorEntered(e)
}
}
impl From<CursorLeft> for WinitEvent {
fn from(e: CursorLeft) -> Self {
Self::CursorLeft(e)
}
}
impl From<CursorMoved> for WinitEvent {
fn from(e: CursorMoved) -> Self {
Self::CursorMoved(e)
}
}
impl From<FileDragAndDrop> for WinitEvent {
fn from(e: FileDragAndDrop) -> Self {
Self::FileDragAndDrop(e)
}
}
impl From<Ime> for WinitEvent {
fn from(e: Ime) -> Self {
Self::Ime(e)
}
}
impl From<ReceivedCharacter> for WinitEvent {
fn from(e: ReceivedCharacter) -> Self {
Self::ReceivedCharacter(e)
}
}
impl From<RequestRedraw> for WinitEvent {
fn from(e: RequestRedraw) -> Self {
Self::RequestRedraw(e)
}
}
impl From<WindowBackendScaleFactorChanged> for WinitEvent {
fn from(e: WindowBackendScaleFactorChanged) -> Self {
Self::WindowBackendScaleFactorChanged(e)
}
}
impl From<WindowCloseRequested> for WinitEvent {
fn from(e: WindowCloseRequested) -> Self {
Self::WindowCloseRequested(e)
}
}
impl From<WindowCreated> for WinitEvent {
fn from(e: WindowCreated) -> Self {
Self::WindowCreated(e)
}
}
impl From<WindowDestroyed> for WinitEvent {
fn from(e: WindowDestroyed) -> Self {
Self::WindowDestroyed(e)
}
}
impl From<WindowFocused> for WinitEvent {
fn from(e: WindowFocused) -> Self {
Self::WindowFocused(e)
}
}
impl From<WindowMoved> for WinitEvent {
fn from(e: WindowMoved) -> Self {
Self::WindowMoved(e)
}
}
impl From<WindowOccluded> for WinitEvent {
fn from(e: WindowOccluded) -> Self {
Self::WindowOccluded(e)
}
}
impl From<WindowResized> for WinitEvent {
fn from(e: WindowResized) -> Self {
Self::WindowResized(e)
}
}
impl From<WindowScaleFactorChanged> for WinitEvent {
fn from(e: WindowScaleFactorChanged) -> Self {
Self::WindowScaleFactorChanged(e)
}
}
impl From<WindowThemeChanged> for WinitEvent {
fn from(e: WindowThemeChanged) -> Self {
Self::WindowThemeChanged(e)
}
}
impl From<MouseButtonInput> for WinitEvent {
fn from(e: MouseButtonInput) -> Self {
Self::MouseButtonInput(e)
}
}
impl From<MouseMotion> for WinitEvent {
fn from(e: MouseMotion) -> Self {
Self::MouseMotion(e)
}
}
impl From<MouseWheel> for WinitEvent {
fn from(e: MouseWheel) -> Self {
Self::MouseWheel(e)
}
}
impl From<TouchpadMagnify> for WinitEvent {
fn from(e: TouchpadMagnify) -> Self {
Self::TouchpadMagnify(e)
}
}
impl From<TouchpadRotate> for WinitEvent {
fn from(e: TouchpadRotate) -> Self {
Self::TouchpadRotate(e)
}
}
impl From<TouchInput> for WinitEvent {
fn from(e: TouchInput) -> Self {
Self::TouchInput(e)
}
}
impl From<KeyboardInput> for WinitEvent {
fn from(e: KeyboardInput) -> Self {
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.send_event(e);
}
WinitEvent::CursorEntered(e) => {
app.world.send_event(e);
}
WinitEvent::CursorLeft(e) => {
app.world.send_event(e);
}
WinitEvent::CursorMoved(e) => {
app.world.send_event(e);
}
WinitEvent::FileDragAndDrop(e) => {
app.world.send_event(e);
}
WinitEvent::Ime(e) => {
app.world.send_event(e);
}
WinitEvent::ReceivedCharacter(e) => {
app.world.send_event(e);
}
WinitEvent::RequestRedraw(e) => {
app.world.send_event(e);
}
WinitEvent::WindowBackendScaleFactorChanged(e) => {
app.world.send_event(e);
}
WinitEvent::WindowCloseRequested(e) => {
app.world.send_event(e);
}
WinitEvent::WindowCreated(e) => {
app.world.send_event(e);
}
WinitEvent::WindowDestroyed(e) => {
app.world.send_event(e);
}
WinitEvent::WindowFocused(e) => {
app.world.send_event(e);
}
WinitEvent::WindowMoved(e) => {
app.world.send_event(e);
}
WinitEvent::WindowOccluded(e) => {
app.world.send_event(e);
}
WinitEvent::WindowResized(e) => {
app.world.send_event(e);
}
WinitEvent::WindowScaleFactorChanged(e) => {
app.world.send_event(e);
}
WinitEvent::WindowThemeChanged(e) => {
app.world.send_event(e);
}
WinitEvent::MouseButtonInput(e) => {
app.world.send_event(e);
}
WinitEvent::MouseMotion(e) => {
app.world.send_event(e);
}
WinitEvent::MouseWheel(e) => {
app.world.send_event(e);
}
WinitEvent::TouchpadMagnify(e) => {
app.world.send_event(e);
}
WinitEvent::TouchpadRotate(e) => {
app.world.send_event(e);
}
WinitEvent::TouchInput(e) => {
app.world.send_event(e);
}
WinitEvent::KeyboardInput(e) => {
app.world.send_event(e);
}
}
}
app.world
.resource_mut::<Events<WinitEvent>>()
.send_batch(buffered_events.drain(..));
}