moved some things around.

Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
2025-12-14 21:25:52 +00:00
parent 52953c531a
commit 8f829e9537
16 changed files with 861 additions and 298 deletions

View File

@@ -7,7 +7,7 @@ use bevy::prelude::*;
use bevy::input::keyboard::KeyboardInput; use bevy::input::keyboard::KeyboardInput;
use bevy::input::mouse::{MouseButtonInput, MouseWheel}; use bevy::input::mouse::{MouseButtonInput, MouseWheel};
use bevy::window::CursorMoved; use bevy::window::CursorMoved;
use libmarathon::engine::{InputEvent, InputEventBuffer, KeyCode as EngineKeyCode, MouseButton as EngineMouseButton, TouchPhase, Modifiers}; use libmarathon::platform::input::{InputEvent, InputEventBuffer, KeyCode as EngineKeyCode, MouseButton as EngineMouseButton, TouchPhase, Modifiers};
/// Convert Bevy's Vec2 to glam::Vec2 /// Convert Bevy's Vec2 to glam::Vec2
/// ///

View File

@@ -2,4 +2,4 @@
//! //!
//! InputEventBuffer is now defined in libmarathon::engine //! InputEventBuffer is now defined in libmarathon::engine
pub use libmarathon::engine::InputEventBuffer; pub use libmarathon::platform::input::InputEventBuffer;

View File

@@ -4,7 +4,8 @@
use bevy::prelude::*; use bevy::prelude::*;
use libmarathon::{ use libmarathon::{
engine::{GameAction, InputController}, engine::GameAction,
platform::input::InputController,
networking::{EntityLockRegistry, NetworkedEntity, NetworkedSelection, NodeVectorClock}, networking::{EntityLockRegistry, NetworkedEntity, NetworkedSelection, NodeVectorClock},
}; };

View File

@@ -3,7 +3,7 @@
//! This module integrates the platform-agnostic pencil bridge with Bevy. //! This module integrates the platform-agnostic pencil bridge with Bevy.
use bevy::prelude::*; use bevy::prelude::*;
use libmarathon::{engine::InputEvent, platform::ios}; use libmarathon::{platform::input::InputEvent, platform::ios};
pub struct PencilInputPlugin; pub struct PencilInputPlugin;

View File

@@ -12,7 +12,6 @@ use std::path::PathBuf;
mod camera; mod camera;
mod cube; mod cube;
mod debug_ui; mod debug_ui;
mod executor;
mod engine_bridge; mod engine_bridge;
mod rendering; mod rendering;
mod selection; mod selection;
@@ -101,5 +100,5 @@ fn main() {
app.add_systems(Startup, initialize_offline_resources); app.add_systems(Startup, initialize_offline_resources);
// Run with our executor (unbounded event loop) // Run with our executor (unbounded event loop)
executor::run(app).expect("Failed to run executor"); libmarathon::platform::desktop::run_executor(app).expect("Failed to run executor");
} }

View File

@@ -25,7 +25,7 @@ use bevy::window::{CursorMoved, FileDragAndDrop, Ime, Window};
use egui::Modifiers; use egui::Modifiers;
// Import engine InputEvent types for custom input system // Import engine InputEvent types for custom input system
use crate::engine::{InputEvent, InputEventBuffer, TouchPhase, MouseButton as EngineMouseButton, KeyCode as EngineKeyCode}; use crate::platform::input::{InputEvent, InputEventBuffer, TouchPhase, MouseButton as EngineMouseButton, KeyCode as EngineKeyCode};
/// Cached pointer position, used to populate [`egui::Event::PointerButton`] messages. /// Cached pointer position, used to populate [`egui::Event::PointerButton`] messages.
#[derive(Component, Default)] #[derive(Component, Default)]

View File

@@ -1,12 +1,18 @@
//! Core Engine module - networking and persistence outside Bevy //! Core Engine module - application logic and coordination
//!
//! This module handles the core application logic that sits between the
//! platform layer and the game systems:
//! - **bridge**: Communication bridge between async EngineCore and Bevy ECS
//! - **core**: Async EngineCore running on tokio (CRDT sync, networking, persistence)
//! - **commands**: Commands that can be sent to EngineCore
//! - **events**: Events emitted by EngineCore
//! - **game_actions**: High-level game actions (SelectEntity, MoveEntity, etc.)
mod bridge; mod bridge;
mod commands; mod commands;
mod core; mod core;
mod events; mod events;
mod game_actions; mod game_actions;
mod input_controller;
mod input_events;
mod networking; mod networking;
mod persistence; mod persistence;
@@ -15,7 +21,5 @@ pub use commands::EngineCommand;
pub use core::EngineCore; pub use core::EngineCore;
pub use events::EngineEvent; pub use events::EngineEvent;
pub use game_actions::GameAction; pub use game_actions::GameAction;
pub use input_controller::{AccessibilitySettings, InputContext, InputController};
pub use input_events::{InputEvent, InputEventBuffer, KeyCode, Modifiers, MouseButton, TouchPhase};
pub use networking::NetworkingManager; pub use networking::NetworkingManager;
pub use persistence::PersistenceManager; pub use persistence::PersistenceManager;

View File

@@ -3,7 +3,7 @@
//! This module creates and manages the main window and event loop. //! This module creates and manages the main window and event loop.
//! It converts winit events to InputEvents and provides them to the engine. //! It converts winit events to InputEvents and provides them to the engine.
use super::winit_bridge; use super::input;
use winit::application::ApplicationHandler; use winit::application::ApplicationHandler;
use winit::event::WindowEvent; use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
@@ -46,7 +46,7 @@ impl ApplicationHandler for DesktopApp {
event: WindowEvent, event: WindowEvent,
) { ) {
// Forward all input events to the bridge first // Forward all input events to the bridge first
winit_bridge::push_window_event(&event); input::push_window_event(&event);
match event { match event {
WindowEvent::CloseRequested => { WindowEvent::CloseRequested => {

View File

@@ -1,7 +1,54 @@
//! Application executor - owns winit and drives Bevy ECS //! Application executor - owns winit and drives Bevy ECS
//! //!
//! The executor gives us full control over the event loop and allows //! The executor is the bridge between the platform (winit) and the engine (Bevy ECS),
//! both the window and ECS to run unbounded (maximum performance). //! giving us full control over the event loop for maximum performance.
//!
//! # Architecture
//!
//! Instead of using Bevy's WinitPlugin, we own the winit event loop directly:
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────┐
//! │ Executor │
//! │ (ApplicationHandler implementation) │
//! │ │
//! │ ┌────────────┐ ┌──────────────┐ │
//! │ │ Winit │ ────▶ │ Bevy ECS │ │
//! │ │ Event Loop │ │ app.update() │ │
//! │ └────────────┘ └──────────────┘ │
//! │ │ │
//! │ ▼ │
//! │ push_window_event() ──▶ RawWinitEvent ──▶ InputEvent │
//! │ push_device_event() ──▶ (channel) ──▶ (buffer) │
//! └─────────────────────────────────────────────────────────┘
//! ```
//!
//! ## Responsibilities
//!
//! - **Event Loop Ownership**: Runs winit's event loop in unbounded mode (`ControlFlow::Poll`)
//! - **Window Management**: Creates window entity with RawHandleWrapper for Bevy renderer
//! - **Event Forwarding**: Routes winit events to platform bridge for conversion
//! - **ECS Updates**: Drives Bevy's `app.update()` every frame
//! - **Lifecycle**: Handles initialization (resumed), suspension (mobile), and shutdown
//!
//! ## Feature Parity with Bevy's WinitPlugin
//!
//! This executor provides complete feature parity with bevy_winit's ApplicationHandler:
//! - ✅ Window creation before app.finish() for renderer initialization
//! - ✅ Window/device event handling (20+ event types)
//! - ✅ Application lifecycle (resumed, suspended, exiting)
//! - ✅ Scale factor and resize handling
//! - ✅ Input event buffering and processing
//! - ✅ Graceful shutdown with WindowClosing events
//!
//! ## Performance
//!
//! The executor runs in unbounded mode for maximum performance:
//! - No frame rate cap (runs as fast as possible)
//! - Continuous redraw requests via `about_to_wait()`
//! - Lock-free event buffering via crossbeam channel
//!
//! Note: Battery-aware adaptive frame limiting is planned for production use.
use bevy::prelude::*; use bevy::prelude::*;
use bevy::app::AppExit; use bevy::app::AppExit;
@@ -10,24 +57,27 @@ use bevy::input::{
mouse::MouseButton as BevyMouseButton, mouse::MouseButton as BevyMouseButton,
keyboard::KeyCode as BevyKeyCode, keyboard::KeyCode as BevyKeyCode,
touch::{Touches, TouchInput}, touch::{Touches, TouchInput},
gestures::*,
keyboard::KeyboardInput,
mouse::{MouseButtonInput, MouseMotion, MouseWheel},
}; };
use bevy::window::{ use bevy::window::{
PrimaryWindow, WindowCreated, WindowResized, WindowScaleFactorChanged, WindowClosing, PrimaryWindow, WindowCreated, WindowResized, WindowScaleFactorChanged, WindowClosing,
WindowResolution, WindowMode, WindowPosition, WindowEvent as BevyWindowEvent, WindowResolution, WindowMode, WindowPosition, WindowEvent as BevyWindowEvent,
RawHandleWrapper, WindowWrapper, RawHandleWrapper, WindowWrapper,
CursorMoved, CursorEntered, CursorLeft,
WindowFocused, WindowOccluded, WindowMoved, WindowThemeChanged, WindowDestroyed,
FileDragAndDrop, Ime, WindowCloseRequested,
}; };
use bevy::ecs::message::Messages; use bevy::ecs::message::Messages;
use libmarathon::engine::InputEvent; use crate::platform::input::{InputEvent, InputEventBuffer};
use libmarathon::platform::desktop; use super::{push_window_event, push_device_event, drain_as_input_events, set_scale_factor};
use std::sync::Arc; use std::sync::Arc;
use winit::application::ApplicationHandler; use winit::application::ApplicationHandler;
use winit::event::{Event as WinitEvent, WindowEvent as WinitWindowEvent}; use winit::event::{Event as WinitEvent, WindowEvent as WinitWindowEvent};
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy}; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
use winit::window::{Window as WinitWindow, WindowId, WindowAttributes}; use winit::window::{Window as WinitWindow, WindowId, WindowAttributes};
// Re-export InputEventBuffer from the input module
pub use crate::input::event_buffer::InputEventBuffer;
/// Application handler state machine /// Application handler state machine
enum AppHandler { enum AppHandler {
Initializing { app: Option<App> }, Initializing { app: Option<App> },
@@ -125,7 +175,7 @@ impl AppHandler {
let scale_factor = winit_window.scale_factor(); let scale_factor = winit_window.scale_factor();
// Set the scale factor in the input bridge so mouse coords are converted correctly // Set the scale factor in the input bridge so mouse coords are converted correctly
desktop::set_scale_factor(scale_factor); set_scale_factor(scale_factor);
// Create window entity with all required components (use logical size) // Create window entity with all required components (use logical size)
let mut window = bevy::window::Window { let mut window = bevy::window::Window {
@@ -180,7 +230,7 @@ impl AppHandler {
fn shutdown(&mut self, event_loop: &ActiveEventLoop) { fn shutdown(&mut self, event_loop: &ActiveEventLoop) {
if let AppHandler::Running { if let AppHandler::Running {
bevy_window_entity, bevy_window_entity,
ref mut bevy_app, bevy_app,
.. ..
} = self } = self
{ {
@@ -212,14 +262,6 @@ impl ApplicationHandler for AppHandler {
info!("App resumed"); info!("App resumed");
} }
// TODO(@siennathesane): Implement suspended() callback for mobile platforms.
// On iOS/Android, the app can be backgrounded (suspended). We should:
// - Stop requesting redraws to save battery
// - Potentially release GPU resources
// - Log the suspended state for debugging
// Note: RedrawRequested events won't fire while suspended anyway,
// so the unbounded loop naturally pauses.
fn window_event( fn window_event(
&mut self, &mut self,
event_loop: &ActiveEventLoop, event_loop: &ActiveEventLoop,
@@ -228,16 +270,16 @@ impl ApplicationHandler for AppHandler {
) { ) {
// Only handle events if we're in Running state // Only handle events if we're in Running state
let AppHandler::Running { let AppHandler::Running {
ref window, window,
bevy_window_entity, bevy_window_entity,
ref mut bevy_app, bevy_app,
} = self } = self
else { else {
return; return;
}; };
// Forward input events to platform bridge // Forward input events to platform bridge
desktop::push_window_event(&event); push_window_event(&event);
match event { match event {
WinitWindowEvent::CloseRequested => { WinitWindowEvent::CloseRequested => {
@@ -259,7 +301,7 @@ impl ApplicationHandler for AppHandler {
WinitWindowEvent::RedrawRequested => { WinitWindowEvent::RedrawRequested => {
// Collect input events from platform bridge // Collect input events from platform bridge
let input_events = desktop::drain_as_input_events(); let input_events = drain_as_input_events();
// Reuse buffer capacity instead of replacing (optimization) // Reuse buffer capacity instead of replacing (optimization)
{ {
@@ -305,11 +347,38 @@ impl ApplicationHandler for AppHandler {
} }
} }
fn device_event(
&mut self,
_event_loop: &ActiveEventLoop,
_device_id: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) {
// Forward device events to platform bridge
// The main one we care about is MouseMotion for raw mouse delta (FPS camera)
push_device_event(&event);
}
fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
// On iOS/Android, the app is being backgrounded
// RedrawRequested events won't fire while suspended, so the unbounded loop naturally pauses
info!("App suspended (backgrounded on mobile)");
// TODO(@siennathesane): Implement AppLifecycle resource to track app state
// Similar to Bevy's WinitPlugin, we should:
// 1. Set AppLifecycle::WillSuspend before suspending
// 2. Let the schedule run one last frame to react to suspension
// 3. Potentially release GPU resources to save memory
// 4. Stop requesting redraws to save battery
//
// For now, the unbounded loop will naturally pause since RedrawRequested
// events won't fire while suspended anyway.
}
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
// Ensure we keep rendering even if no window events arrive. // Ensure we keep rendering even if no window events arrive.
// This is needed for unbounded mode with ControlFlow::Poll to maintain // This is needed for unbounded mode with ControlFlow::Poll to maintain
// maximum frame rate by continuously requesting redraws. // maximum frame rate by continuously requesting redraws.
if let AppHandler::Running { ref window, .. } = self { if let AppHandler::Running { window, .. } = self {
window.request_redraw(); window.request_redraw();
} }
} }
@@ -348,13 +417,14 @@ impl ApplicationHandler for AppHandler {
/// ///
/// ```no_run /// ```no_run
/// use bevy::prelude::*; /// use bevy::prelude::*;
/// use libmarathon::platform::desktop;
/// ///
/// let app = App::new(); /// let app = App::new();
/// // ... configure app with plugins and systems ... /// // ... configure app with plugins and systems ...
/// ///
/// executor::run(app).expect("Failed to run executor"); /// desktop::run_executor(app).expect("Failed to run executor");
/// ``` /// ```
pub fn run(mut app: App) -> Result<(), Box<dyn std::error::Error>> { pub fn run_executor(app: App) -> Result<(), Box<dyn std::error::Error>> {
// Create event loop (using default type for now, WakeUp will be added when implementing battery mode) // Create event loop (using default type for now, WakeUp will be added when implementing battery mode)
let event_loop = EventLoop::new()?; let event_loop = EventLoop::new()?;

View File

@@ -0,0 +1,617 @@
//! Desktop winit event loop integration
//!
//! This module owns the winit event loop and window, converting winit events
//! to engine-agnostic InputEvents and Bevy window events.
//!
//! # Feature Parity with Bevy's WinitPlugin
//!
//! This implementation achieves comprehensive feature parity with bevy_winit,
//! handling all major input and window event types:
//!
//! ## Input Events (WindowEvent)
//! - **Mouse**: Buttons, cursor movement, wheel scrolling, enter/exit
//! - **Keyboard**: Full key event details with modifiers, logical keys, repeat detection
//! - **Touch**: Multi-touch with phase tracking (Started, Moved, Ended, Cancelled)
//! - **Apple Pencil**: Force/pressure detection via calibrated touch force, altitude angle for tilt
//! - **Gestures**: Pinch (zoom), rotation, pan, double-tap (trackpad/touch)
//! - **File Drag & Drop**: Dropped files, hovered files, cancelled hovers
//! - **IME**: International text input with preedit, commit, enabled/disabled states
//!
//! ## Window State Events
//! - **Focus**: Window focused/unfocused tracking
//! - **Occlusion**: Window visibility state (occluded/visible)
//! - **Theme**: System theme changes (light/dark mode)
//! - **Position**: Window moved events
//! - **Resize**: Window resized with physical dimensions
//! - **Scale Factor**: DPI/HiDPI scale factor changes
//!
//! ## Device Events
//! - **Mouse Motion**: Raw mouse delta for FPS camera controls (unbounded movement)
//!
//! ## Application Lifecycle
//! - **Suspended**: iOS/Android backgrounding support
//! - **Resumed**: App foreground/initialization
//! - **Exiting**: Clean shutdown
//!
//! ## Architecture
//!
//! Events flow through three layers:
//! 1. **Winit WindowEvent/DeviceEvent** → Raw OS events
//! 2. **RawWinitEvent** → Buffered events in lock-free channel
//! 3. **InputEvent** → Engine-agnostic input abstraction
//!
//! The executor (`super::executor`) drives the event loop and calls:
//! - `push_window_event()` for window events
//! - `push_device_event()` for device events
//! - `drain_as_input_events()` to consume buffered events each frame
use crate::platform::input::{InputEvent, KeyCode, Modifiers, MouseButton, TouchPhase};
use crossbeam_channel::{Receiver, Sender, unbounded};
use glam::Vec2;
use std::sync::{Mutex, OnceLock};
use std::path::PathBuf;
use winit::event::{
DeviceEvent, ElementState, MouseButton as WinitMouseButton, MouseScrollDelta, WindowEvent,
Touch as WinitTouch, Force as WinitForce, TouchPhase as WinitTouchPhase,
Ime as WinitIme,
};
use winit::keyboard::{PhysicalKey, Key as LogicalKey, NamedKey};
use winit::window::Theme as WinitTheme;
/// Raw winit input events before conversion
///
/// Comprehensive event types matching bevy_winit functionality
#[derive(Clone, Debug)]
pub enum RawWinitEvent {
// Mouse events
MouseButton {
button: MouseButton,
state: ElementState,
position: Vec2,
},
CursorMoved {
position: Vec2,
},
CursorEntered,
CursorLeft,
MouseWheel {
delta: Vec2,
position: Vec2,
},
/// Raw mouse motion delta (for FPS camera, etc.)
/// This is separate from CursorMoved and gives unbounded mouse delta
MouseMotion {
delta: Vec2,
},
// Keyboard events
Keyboard {
key: KeyCode,
state: ElementState,
modifiers: Modifiers,
logical_key: String, // Text representation for display
text: Option<String>, // Actual character input
repeat: bool,
},
// Touch events (CRITICAL for Apple Pencil!)
Touch {
phase: WinitTouchPhase,
position: Vec2,
force: Option<WinitForce>,
id: u64,
},
// Gesture events (trackpad/touch)
PinchGesture {
delta: f32,
},
RotationGesture {
delta: f32,
},
DoubleTapGesture,
PanGesture {
delta: Vec2,
},
// File drag & drop
DroppedFile {
path: PathBuf,
},
HoveredFile {
path: PathBuf,
},
HoveredFileCancelled,
// IME (international text input)
ImePreedit {
value: String,
cursor: Option<(usize, usize)>,
},
ImeCommit {
value: String,
},
ImeEnabled,
ImeDisabled,
// Window state events
Focused {
focused: bool,
},
Occluded {
occluded: bool,
},
ThemeChanged {
theme: WinitTheme,
},
Moved {
x: i32,
y: i32,
},
}
/// Lock-free channel for winit events
///
/// The winit event loop sends events here.
/// The engine receives them each frame.
static EVENT_CHANNEL: OnceLock<(Sender<RawWinitEvent>, Receiver<RawWinitEvent>)> = OnceLock::new();
fn get_event_channel() -> &'static (Sender<RawWinitEvent>, Receiver<RawWinitEvent>) {
EVENT_CHANNEL.get_or_init(|| unbounded())
}
/// Current scale factor (needed to convert physical to logical pixels)
static SCALE_FACTOR: Mutex<f64> = Mutex::new(1.0);
/// Set the window scale factor (call when window is created or scale changes)
pub fn set_scale_factor(scale_factor: f64) {
if let Ok(mut sf) = SCALE_FACTOR.lock() {
*sf = scale_factor;
}
}
/// Current input state for tracking drags and modifiers
static INPUT_STATE: Mutex<InputState> = Mutex::new(InputState {
left_pressed: false,
right_pressed: false,
middle_pressed: false,
last_position: Vec2::ZERO,
modifiers: Modifiers {
shift: false,
ctrl: false,
alt: false,
meta: false,
},
});
#[derive(Clone, Copy, Debug)]
struct InputState {
left_pressed: bool,
right_pressed: bool,
middle_pressed: bool,
last_position: Vec2,
modifiers: Modifiers,
}
/// Push a winit window event to the buffer
///
/// Call this from the winit event loop. Handles ALL WindowEvent variants
/// for complete feature parity with bevy_winit.
pub fn push_window_event(event: &WindowEvent) {
let (sender, _) = get_event_channel();
match event {
// === MOUSE EVENTS ===
WindowEvent::MouseInput { state, button, .. } => {
let mouse_button = match button {
WinitMouseButton::Left => MouseButton::Left,
WinitMouseButton::Right => MouseButton::Right,
WinitMouseButton::Middle => MouseButton::Middle,
_ => return, // Ignore other buttons
};
if let Ok(mut input_state) = INPUT_STATE.lock() {
let position = input_state.last_position;
// Update button state
match mouse_button {
MouseButton::Left => input_state.left_pressed = *state == ElementState::Pressed,
MouseButton::Right => input_state.right_pressed = *state == ElementState::Pressed,
MouseButton::Middle => input_state.middle_pressed = *state == ElementState::Pressed,
}
let _ = sender.send(RawWinitEvent::MouseButton {
button: mouse_button,
state: *state,
position,
});
}
}
WindowEvent::CursorMoved { position, .. } => {
// Convert from physical pixels to logical pixels
let scale_factor = SCALE_FACTOR.lock().map(|sf| *sf).unwrap_or(1.0);
let pos = Vec2::new(
(position.x / scale_factor) as f32,
(position.y / scale_factor) as f32,
);
if let Ok(mut input_state) = INPUT_STATE.lock() {
input_state.last_position = pos;
let _ = sender.send(RawWinitEvent::CursorMoved { position: pos });
}
}
WindowEvent::CursorEntered { .. } => {
let _ = sender.send(RawWinitEvent::CursorEntered);
}
WindowEvent::CursorLeft { .. } => {
let _ = sender.send(RawWinitEvent::CursorLeft);
}
WindowEvent::MouseWheel { delta, .. } => {
let scroll_delta = match delta {
MouseScrollDelta::LineDelta(x, y) => Vec2::new(*x, *y) * 20.0,
MouseScrollDelta::PixelDelta(pos) => Vec2::new(pos.x as f32, pos.y as f32),
};
if let Ok(input_state) = INPUT_STATE.lock() {
let _ = sender.send(RawWinitEvent::MouseWheel {
delta: scroll_delta,
position: input_state.last_position,
});
}
}
// === KEYBOARD EVENTS ===
WindowEvent::KeyboardInput { event: key_event, is_synthetic: false, .. } => {
// Skip synthetic key events (we handle focus ourselves)
if let PhysicalKey::Code(key_code) = key_event.physical_key {
if let Ok(input_state) = INPUT_STATE.lock() {
// Convert logical key to string for display
let logical_key_str = match &key_event.logical_key {
LogicalKey::Character(s) => s.to_string(),
LogicalKey::Named(named) => format!("{:?}", named),
_ => String::new(),
};
let _ = sender.send(RawWinitEvent::Keyboard {
key: key_code,
state: key_event.state,
modifiers: input_state.modifiers,
logical_key: logical_key_str,
text: key_event.text.as_ref().map(|s| s.to_string()),
repeat: key_event.repeat,
});
}
}
}
WindowEvent::ModifiersChanged(new_modifiers) => {
if let Ok(mut input_state) = INPUT_STATE.lock() {
input_state.modifiers = Modifiers {
shift: new_modifiers.state().shift_key(),
ctrl: new_modifiers.state().control_key(),
alt: new_modifiers.state().alt_key(),
meta: new_modifiers.state().super_key(),
};
}
}
// === TOUCH EVENTS (APPLE PENCIL!) ===
WindowEvent::Touch(touch) => {
let scale_factor = SCALE_FACTOR.lock().map(|sf| *sf).unwrap_or(1.0);
let position = Vec2::new(
(touch.location.x / scale_factor) as f32,
(touch.location.y / scale_factor) as f32,
);
let _ = sender.send(RawWinitEvent::Touch {
phase: touch.phase,
position,
force: touch.force,
id: touch.id,
});
}
// === GESTURE EVENTS ===
WindowEvent::PinchGesture { delta, .. } => {
let _ = sender.send(RawWinitEvent::PinchGesture {
delta: *delta as f32,
});
}
WindowEvent::RotationGesture { delta, .. } => {
let _ = sender.send(RawWinitEvent::RotationGesture {
delta: *delta,
});
}
WindowEvent::DoubleTapGesture { .. } => {
let _ = sender.send(RawWinitEvent::DoubleTapGesture);
}
WindowEvent::PanGesture { delta, .. } => {
let _ = sender.send(RawWinitEvent::PanGesture {
delta: Vec2::new(delta.x, delta.y),
});
}
// === FILE DRAG & DROP ===
WindowEvent::DroppedFile(path) => {
let _ = sender.send(RawWinitEvent::DroppedFile {
path: path.clone(),
});
}
WindowEvent::HoveredFile(path) => {
let _ = sender.send(RawWinitEvent::HoveredFile {
path: path.clone(),
});
}
WindowEvent::HoveredFileCancelled => {
let _ = sender.send(RawWinitEvent::HoveredFileCancelled);
}
// === IME (INTERNATIONAL TEXT INPUT) ===
WindowEvent::Ime(ime_event) => {
match ime_event {
WinitIme::Enabled => {
let _ = sender.send(RawWinitEvent::ImeEnabled);
}
WinitIme::Preedit(value, cursor) => {
let _ = sender.send(RawWinitEvent::ImePreedit {
value: value.clone(),
cursor: *cursor,
});
}
WinitIme::Commit(value) => {
let _ = sender.send(RawWinitEvent::ImeCommit {
value: value.clone(),
});
}
WinitIme::Disabled => {
let _ = sender.send(RawWinitEvent::ImeDisabled);
}
}
}
// === WINDOW STATE EVENTS ===
WindowEvent::Focused(focused) => {
let _ = sender.send(RawWinitEvent::Focused {
focused: *focused,
});
}
WindowEvent::Occluded(occluded) => {
let _ = sender.send(RawWinitEvent::Occluded {
occluded: *occluded,
});
}
WindowEvent::ThemeChanged(theme) => {
let _ = sender.send(RawWinitEvent::ThemeChanged {
theme: *theme,
});
}
WindowEvent::Moved(position) => {
let _ = sender.send(RawWinitEvent::Moved {
x: position.x,
y: position.y,
});
}
// === EVENTS WE DON'T PROPAGATE (handled at executor level) ===
WindowEvent::Resized(_) |
WindowEvent::ScaleFactorChanged { .. } |
WindowEvent::CloseRequested |
WindowEvent::Destroyed |
WindowEvent::RedrawRequested => {
// These are handled directly by the executor, not converted to InputEvents
}
// Catch-all for any future events
_ => {}
}
}
/// Push a device event into the event buffer
///
/// Device events are low-level input events that don't correspond to a specific window.
/// The most important one is MouseMotion, which gives raw mouse delta for FPS cameras.
pub fn push_device_event(event: &winit::event::DeviceEvent) {
let (sender, _) = get_event_channel();
match event {
winit::event::DeviceEvent::MouseMotion { delta: (x, y) } => {
let delta = Vec2::new(*x as f32, *y as f32);
let _ = sender.send(RawWinitEvent::MouseMotion { delta });
}
// Other device events (Added/Removed, Button, Key, etc.) are not needed
// for our use case. Mouse delta is the main one.
_ => {}
}
}
/// Drain all buffered winit events and convert to InputEvents
///
/// Call this from your engine's input processing to consume events.
/// This uses a lock-free channel so it never blocks and can't silently drop events.
pub fn drain_as_input_events() -> Vec<InputEvent> {
let (_, receiver) = get_event_channel();
// Drain all events from the channel
receiver
.try_iter()
.filter_map(raw_to_input_event)
.collect()
}
/// Convert a raw winit event to an engine InputEvent
///
/// Only input-related events are converted. Other events (gestures, file drop, IME, etc.)
/// return None and should be handled by the Bevy event system directly.
fn raw_to_input_event(event: RawWinitEvent) -> Option<InputEvent> {
match event {
// === MOUSE INPUT ===
RawWinitEvent::MouseButton { button, state, position } => {
let phase = match state {
ElementState::Pressed => TouchPhase::Started,
ElementState::Released => TouchPhase::Ended,
};
Some(InputEvent::Mouse {
pos: position,
button,
phase,
})
}
RawWinitEvent::CursorMoved { position } => {
// Check if any button is pressed
let input_state = INPUT_STATE.lock().ok()?;
if input_state.left_pressed {
Some(InputEvent::Mouse {
pos: position,
button: MouseButton::Left,
phase: TouchPhase::Moved,
})
} else if input_state.right_pressed {
Some(InputEvent::Mouse {
pos: position,
button: MouseButton::Right,
phase: TouchPhase::Moved,
})
} else if input_state.middle_pressed {
Some(InputEvent::Mouse {
pos: position,
button: MouseButton::Middle,
phase: TouchPhase::Moved,
})
} else {
// No button pressed - hover tracking
Some(InputEvent::MouseMove { pos: position })
}
}
RawWinitEvent::MouseWheel { delta, position } => {
Some(InputEvent::MouseWheel {
delta,
pos: position,
})
}
// === KEYBOARD INPUT ===
RawWinitEvent::Keyboard { key, state, modifiers, .. } => {
Some(InputEvent::Keyboard {
key,
pressed: state == ElementState::Pressed,
modifiers,
})
}
// === TOUCH INPUT (APPLE PENCIL!) ===
RawWinitEvent::Touch { phase, position, force, id } => {
// Convert winit TouchPhase to engine TouchPhase
let touch_phase = match phase {
WinitTouchPhase::Started => TouchPhase::Started,
WinitTouchPhase::Moved => TouchPhase::Moved,
WinitTouchPhase::Ended => TouchPhase::Ended,
WinitTouchPhase::Cancelled => TouchPhase::Cancelled,
};
// Check if this is a stylus (has force/pressure data)
match force {
Some(WinitForce::Calibrated { force, max_possible_force, altitude_angle }) => {
// This is Apple Pencil or similar stylus!
// Normalize pressure to 0-1 range (though Apple Pencil can exceed 1.0)
let pressure = if max_possible_force > 0.0 {
(force / max_possible_force) as f32
} else {
force as f32 // Fallback if max is not provided
};
// Tilt: altitude_angle is provided, azimuth would need device_id tracking
// For now, use altitude and assume azimuth=0
let tilt = Vec2::new(
altitude_angle.unwrap_or(0.0) as f32,
0.0, // Azimuth not provided by winit Force::Calibrated
);
Some(InputEvent::Stylus {
pos: position,
pressure,
tilt,
phase: touch_phase,
timestamp: 0.0, // TODO: Get actual timestamp from winit when available
})
}
Some(WinitForce::Normalized(pressure)) => {
// Normalized pressure (0.0-1.0), likely a stylus
Some(InputEvent::Stylus {
pos: position,
pressure: pressure as f32,
tilt: Vec2::ZERO, // No tilt data in normalized mode
phase: touch_phase,
timestamp: 0.0,
})
}
None => {
// No force data - regular touch (finger)
Some(InputEvent::Touch {
pos: position,
phase: touch_phase,
id,
})
}
}
}
// === GESTURE INPUT ===
RawWinitEvent::PinchGesture { delta } => {
Some(InputEvent::PinchGesture { delta })
}
RawWinitEvent::RotationGesture { delta } => {
Some(InputEvent::RotationGesture { delta })
}
RawWinitEvent::PanGesture { delta } => {
Some(InputEvent::PanGesture { delta })
}
RawWinitEvent::DoubleTapGesture => {
Some(InputEvent::DoubleTapGesture)
}
// === MOUSE MOTION (RAW DELTA) ===
RawWinitEvent::MouseMotion { delta } => {
Some(InputEvent::MouseMotion { delta })
}
// === NON-INPUT EVENTS ===
// These are window/system events, not user input
RawWinitEvent::CursorEntered |
RawWinitEvent::CursorLeft |
RawWinitEvent::DroppedFile { .. } |
RawWinitEvent::HoveredFile { .. } |
RawWinitEvent::HoveredFileCancelled |
RawWinitEvent::ImePreedit { .. } |
RawWinitEvent::ImeCommit { .. } |
RawWinitEvent::ImeEnabled |
RawWinitEvent::ImeDisabled |
RawWinitEvent::Focused { .. } |
RawWinitEvent::Occluded { .. } |
RawWinitEvent::ThemeChanged { .. } |
RawWinitEvent::Moved { .. } => {
// These are window/UI events, should be sent to Bevy messages
// (to be implemented when we add Bevy window event forwarding)
None
}
}
}

View File

@@ -1,9 +1,19 @@
//! Desktop platform integration //! Desktop platform implementation (winit-based)
//! //!
//! Owns the winit event loop and converts winit events to InputEvents. //! This module provides the concrete desktop implementation of the platform layer using winit.
//!
//! ## Modules
//!
//! - **executor**: Application executor that owns the winit event loop
//! - **input**: Winit event conversion to platform-agnostic InputEvents
//! - **event_loop**: Legacy event loop wrapper (to be removed)
mod event_loop; mod event_loop;
mod winit_bridge; mod executor;
mod input;
pub use event_loop::run; pub use event_loop::run;
pub use winit_bridge::{drain_as_input_events, push_window_event, set_scale_factor}; pub use executor::run_executor;
pub use input::{
drain_as_input_events, push_device_event, push_window_event, set_scale_factor,
};

View File

@@ -1,248 +0,0 @@
//! Desktop winit event loop integration
//!
//! This module owns the winit event loop and window, converting winit events
//! to engine-agnostic InputEvents.
use crate::engine::{InputEvent, KeyCode, Modifiers, MouseButton, TouchPhase};
use crossbeam_channel::{Receiver, Sender, unbounded};
use glam::Vec2;
use std::sync::{Mutex, OnceLock};
use winit::event::{ElementState, MouseButton as WinitMouseButton, MouseScrollDelta, WindowEvent};
use winit::keyboard::PhysicalKey;
/// Raw winit input events before conversion
#[derive(Clone, Debug)]
pub enum RawWinitEvent {
MouseButton {
button: MouseButton,
state: ElementState,
position: Vec2,
},
CursorMoved {
position: Vec2,
},
Keyboard {
key: KeyCode,
state: ElementState,
modifiers: Modifiers,
},
MouseWheel {
delta: Vec2,
position: Vec2,
},
}
/// Lock-free channel for winit events
///
/// The winit event loop sends events here.
/// The engine receives them each frame.
static EVENT_CHANNEL: OnceLock<(Sender<RawWinitEvent>, Receiver<RawWinitEvent>)> = OnceLock::new();
fn get_event_channel() -> &'static (Sender<RawWinitEvent>, Receiver<RawWinitEvent>) {
EVENT_CHANNEL.get_or_init(|| unbounded())
}
/// Current scale factor (needed to convert physical to logical pixels)
static SCALE_FACTOR: Mutex<f64> = Mutex::new(1.0);
/// Set the window scale factor (call when window is created or scale changes)
pub fn set_scale_factor(scale_factor: f64) {
if let Ok(mut sf) = SCALE_FACTOR.lock() {
*sf = scale_factor;
}
}
/// Current input state for tracking drags and modifiers
static INPUT_STATE: Mutex<InputState> = Mutex::new(InputState {
left_pressed: false,
right_pressed: false,
middle_pressed: false,
last_position: Vec2::ZERO,
modifiers: Modifiers {
shift: false,
ctrl: false,
alt: false,
meta: false,
},
});
#[derive(Clone, Copy, Debug)]
struct InputState {
left_pressed: bool,
right_pressed: bool,
middle_pressed: bool,
last_position: Vec2,
modifiers: Modifiers,
}
/// Push a winit window event to the buffer
///
/// Call this from the winit event loop
pub fn push_window_event(event: &WindowEvent) {
match event {
WindowEvent::MouseInput { state, button, .. } => {
let mouse_button = match button {
WinitMouseButton::Left => MouseButton::Left,
WinitMouseButton::Right => MouseButton::Right,
WinitMouseButton::Middle => MouseButton::Middle,
_ => return, // Ignore other buttons
};
if let Ok(mut input_state) = INPUT_STATE.lock() {
let position = input_state.last_position;
// Update button state
match mouse_button {
MouseButton::Left => input_state.left_pressed = *state == ElementState::Pressed,
MouseButton::Right => input_state.right_pressed = *state == ElementState::Pressed,
MouseButton::Middle => input_state.middle_pressed = *state == ElementState::Pressed,
}
// Send to lock-free channel (never blocks or fails)
let (sender, _) = get_event_channel();
let _ = sender.send(RawWinitEvent::MouseButton {
button: mouse_button,
state: *state,
position,
});
}
}
WindowEvent::CursorMoved { position, .. } => {
// Convert from physical pixels to logical pixels
let scale_factor = SCALE_FACTOR.lock().map(|sf| *sf).unwrap_or(1.0);
let pos = Vec2::new(
(position.x / scale_factor) as f32,
(position.y / scale_factor) as f32,
);
if let Ok(mut input_state) = INPUT_STATE.lock() {
input_state.last_position = pos;
// ALWAYS send cursor movement for hover tracking (egui needs this!)
let (sender, _) = get_event_channel();
let _ = sender.send(RawWinitEvent::CursorMoved { position: pos });
}
}
WindowEvent::KeyboardInput { event: key_event, .. } => {
// Only handle physical keys
if let PhysicalKey::Code(key_code) = key_event.physical_key {
if let Ok(input_state) = INPUT_STATE.lock() {
let (sender, _) = get_event_channel();
let _ = sender.send(RawWinitEvent::Keyboard {
key: key_code,
state: key_event.state,
modifiers: input_state.modifiers,
});
}
}
}
WindowEvent::ModifiersChanged(new_modifiers) => {
if let Ok(mut input_state) = INPUT_STATE.lock() {
input_state.modifiers = Modifiers {
shift: new_modifiers.state().shift_key(),
ctrl: new_modifiers.state().control_key(),
alt: new_modifiers.state().alt_key(),
meta: new_modifiers.state().super_key(),
};
}
}
WindowEvent::MouseWheel { delta, .. } => {
let scroll_delta = match delta {
MouseScrollDelta::LineDelta(x, y) => Vec2::new(*x, *y) * 20.0, // Scale line deltas
MouseScrollDelta::PixelDelta(pos) => Vec2::new(pos.x as f32, pos.y as f32),
};
if let Ok(input_state) = INPUT_STATE.lock() {
let (sender, _) = get_event_channel();
let _ = sender.send(RawWinitEvent::MouseWheel {
delta: scroll_delta,
position: input_state.last_position,
});
}
}
_ => {}
}
}
/// Drain all buffered winit events and convert to InputEvents
///
/// Call this from your engine's input processing to consume events.
/// This uses a lock-free channel so it never blocks and can't silently drop events.
pub fn drain_as_input_events() -> Vec<InputEvent> {
let (_, receiver) = get_event_channel();
// Drain all events from the channel
receiver
.try_iter()
.filter_map(raw_to_input_event)
.collect()
}
/// Convert a raw winit event to an engine InputEvent
fn raw_to_input_event(event: RawWinitEvent) -> Option<InputEvent> {
match event {
RawWinitEvent::MouseButton { button, state, position } => {
let phase = match state {
ElementState::Pressed => TouchPhase::Started,
ElementState::Released => TouchPhase::Ended,
};
Some(InputEvent::Mouse {
pos: position,
button,
phase,
})
}
RawWinitEvent::CursorMoved { position } => {
// Check if any button is pressed
let input_state = INPUT_STATE.lock().ok()?;
if input_state.left_pressed {
// Drag with left button
Some(InputEvent::Mouse {
pos: position,
button: MouseButton::Left,
phase: TouchPhase::Moved,
})
} else if input_state.right_pressed {
// Drag with right button
Some(InputEvent::Mouse {
pos: position,
button: MouseButton::Right,
phase: TouchPhase::Moved,
})
} else if input_state.middle_pressed {
// Drag with middle button
Some(InputEvent::Mouse {
pos: position,
button: MouseButton::Middle,
phase: TouchPhase::Moved,
})
} else {
// No button pressed - hover tracking
Some(InputEvent::MouseMove { pos: position })
}
}
RawWinitEvent::Keyboard { key, state, modifiers } => {
Some(InputEvent::Keyboard {
key,
pressed: state == ElementState::Pressed,
modifiers,
})
}
RawWinitEvent::MouseWheel { delta, position } => {
Some(InputEvent::MouseWheel {
delta,
pos: position,
})
}
}
}

View File

@@ -5,8 +5,8 @@
//! - Accessibility (alternative input methods) //! - Accessibility (alternative input methods)
//! - Context-aware bindings (different actions in different modes) //! - Context-aware bindings (different actions in different modes)
use super::game_actions::GameAction; use crate::engine::GameAction;
use super::input_events::{InputEvent, KeyCode, MouseButton, TouchPhase}; use super::events::{InputEvent, KeyCode, MouseButton, TouchPhase};
use glam::Vec2; use glam::Vec2;
use std::collections::HashMap; use std::collections::HashMap;
@@ -196,6 +196,55 @@ impl InputController {
InputEvent::Touch { pos, phase, id: _ } => { InputEvent::Touch { pos, phase, id: _ } => {
self.process_touch(*pos, *phase, &mut actions); self.process_touch(*pos, *phase, &mut actions);
} }
InputEvent::PinchGesture { delta } => {
// Pinch gesture - use for zoom/scale
// Positive delta = pinch out (zoom in), negative = pinch in (zoom out)
let adjusted_delta = delta * self.accessibility.scroll_sensitivity;
actions.push(GameAction::MoveEntityDepth { delta: adjusted_delta });
}
InputEvent::RotationGesture { delta } => {
// Rotation gesture - use for rotating entities or camera
let adjusted_delta = if self.accessibility.invert_y {
-*delta
} else {
*delta
};
// Convert rotation delta to 2D delta for rotation action
let delta_vec = Vec2::new(adjusted_delta, 0.0);
actions.push(GameAction::RotateEntity { delta: delta_vec });
}
InputEvent::PanGesture { delta } => {
// Pan gesture - use for camera movement or entity translation
let adjusted_delta = *delta * self.accessibility.mouse_sensitivity;
match self.current_context {
InputContext::CameraControl => {
actions.push(GameAction::MoveCamera { delta: adjusted_delta });
}
InputContext::EntityManipulation => {
actions.push(GameAction::MoveEntity { delta: adjusted_delta });
}
_ => {}
}
}
InputEvent::DoubleTapGesture => {
// Double-tap gesture - quick reset/center action
actions.push(GameAction::ResetEntity);
}
InputEvent::MouseMotion { delta } => {
// Raw mouse motion delta - only used in CameraControl mode for FPS-style camera
// This is unbounded mouse movement (different from cursor position)
// and would conflict with normal cursor-based dragging if used elsewhere
if self.current_context == InputContext::CameraControl {
let adjusted_delta = *delta * self.accessibility.mouse_sensitivity;
actions.push(GameAction::MoveCamera { delta: adjusted_delta });
}
// In other contexts, ignore MouseMotion to avoid conflicts with cursor-based input
}
} }
actions actions

View File

@@ -115,6 +115,34 @@ pub enum InputEvent {
/// Current mouse position /// Current mouse position
pos: Vec2, pos: Vec2,
}, },
/// Raw mouse motion delta (for FPS camera, etc.)
/// This is unbounded mouse movement, separate from cursor position
MouseMotion {
/// Raw mouse delta
delta: Vec2,
},
/// Pinch gesture (trackpad/touch - zoom in/out)
PinchGesture {
/// Delta amount (positive = zoom in, negative = zoom out)
delta: f32,
},
/// Rotation gesture (trackpad/touch - rotate with two fingers)
RotationGesture {
/// Rotation delta in radians
delta: f32,
},
/// Pan gesture (trackpad/touch - swipe with two fingers)
PanGesture {
/// Pan delta
delta: Vec2,
},
/// Double-tap gesture (trackpad/touch)
DoubleTapGesture,
} }
impl InputEvent { impl InputEvent {
@@ -126,7 +154,12 @@ impl InputEvent {
InputEvent::MouseMove { pos } => Some(*pos), InputEvent::MouseMove { pos } => Some(*pos),
InputEvent::Touch { pos, .. } => Some(*pos), InputEvent::Touch { pos, .. } => Some(*pos),
InputEvent::MouseWheel { pos, .. } => Some(*pos), InputEvent::MouseWheel { pos, .. } => Some(*pos),
InputEvent::Keyboard { .. } => None, InputEvent::Keyboard { .. } |
InputEvent::MouseMotion { .. } |
InputEvent::PinchGesture { .. } |
InputEvent::RotationGesture { .. } |
InputEvent::PanGesture { .. } |
InputEvent::DoubleTapGesture => None,
} }
} }
@@ -136,7 +169,14 @@ impl InputEvent {
InputEvent::Stylus { phase, .. } => Some(*phase), InputEvent::Stylus { phase, .. } => Some(*phase),
InputEvent::Mouse { phase, .. } => Some(*phase), InputEvent::Mouse { phase, .. } => Some(*phase),
InputEvent::Touch { phase, .. } => Some(*phase), InputEvent::Touch { phase, .. } => Some(*phase),
InputEvent::Keyboard { .. } | InputEvent::MouseWheel { .. } | InputEvent::MouseMove { .. } => None, InputEvent::Keyboard { .. } |
InputEvent::MouseWheel { .. } |
InputEvent::MouseMove { .. } |
InputEvent::MouseMotion { .. } |
InputEvent::PinchGesture { .. } |
InputEvent::RotationGesture { .. } |
InputEvent::PanGesture { .. } |
InputEvent::DoubleTapGesture => None,
} }
} }
@@ -144,7 +184,7 @@ impl InputEvent {
pub fn is_active(&self) -> bool { pub fn is_active(&self) -> bool {
match self.phase() { match self.phase() {
Some(phase) => !matches!(phase, TouchPhase::Ended | TouchPhase::Cancelled), Some(phase) => !matches!(phase, TouchPhase::Ended | TouchPhase::Cancelled),
None => true, // Keyboard and wheel events are considered instantaneous None => true, // Gestures, keyboard, and wheel events are instantaneous
} }
} }
} }

View File

@@ -0,0 +1,17 @@
//! Platform input abstraction layer
//!
//! This module provides a platform-agnostic input event system that can be
//! implemented by different platforms (desktop/winit, iOS, Android, web).
//!
//! ## Architecture
//!
//! - **events.rs**: Platform-agnostic input event types (InputEvent, TouchPhase, etc.)
//! - **controller.rs**: Maps InputEvents to game-specific GameActions
//!
//! Platform implementations (like desktop/input.rs) convert native input to InputEvents.
mod controller;
mod events;
pub use controller::{AccessibilitySettings, InputContext, InputController};
pub use events::{InputEvent, InputEventBuffer, KeyCode, Modifiers, MouseButton, TouchPhase};

View File

@@ -1,7 +1,11 @@
//! Platform-specific input bridges //! Platform abstraction layer
//! //!
//! This module contains platform-specific code for capturing input //! This module provides platform-agnostic interfaces for OS/hardware interaction:
//! and converting it to engine-agnostic InputEvents. //! - **input**: Abstract input events (keyboard, mouse, touch, gestures)
//! - **desktop**: Concrete winit-based implementation for desktop platforms
//! - **ios**: Concrete UIKit-based implementation for iOS
pub mod input;
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
pub mod ios; pub mod ios;