moved some things around.
Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
@@ -7,7 +7,7 @@ use bevy::prelude::*;
|
||||
use bevy::input::keyboard::KeyboardInput;
|
||||
use bevy::input::mouse::{MouseButtonInput, MouseWheel};
|
||||
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
|
||||
///
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
//!
|
||||
//! InputEventBuffer is now defined in libmarathon::engine
|
||||
|
||||
pub use libmarathon::engine::InputEventBuffer;
|
||||
pub use libmarathon::platform::input::InputEventBuffer;
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
|
||||
use bevy::prelude::*;
|
||||
use libmarathon::{
|
||||
engine::{GameAction, InputController},
|
||||
engine::GameAction,
|
||||
platform::input::InputController,
|
||||
networking::{EntityLockRegistry, NetworkedEntity, NetworkedSelection, NodeVectorClock},
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! This module integrates the platform-agnostic pencil bridge with Bevy.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use libmarathon::{engine::InputEvent, platform::ios};
|
||||
use libmarathon::{platform::input::InputEvent, platform::ios};
|
||||
|
||||
pub struct PencilInputPlugin;
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ use std::path::PathBuf;
|
||||
mod camera;
|
||||
mod cube;
|
||||
mod debug_ui;
|
||||
mod executor;
|
||||
mod engine_bridge;
|
||||
mod rendering;
|
||||
mod selection;
|
||||
@@ -101,5 +100,5 @@ fn main() {
|
||||
app.add_systems(Startup, initialize_offline_resources);
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ use bevy::window::{CursorMoved, FileDragAndDrop, Ime, Window};
|
||||
use egui::Modifiers;
|
||||
|
||||
// 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.
|
||||
#[derive(Component, Default)]
|
||||
|
||||
@@ -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 commands;
|
||||
mod core;
|
||||
mod events;
|
||||
mod game_actions;
|
||||
mod input_controller;
|
||||
mod input_events;
|
||||
mod networking;
|
||||
mod persistence;
|
||||
|
||||
@@ -15,7 +21,5 @@ pub use commands::EngineCommand;
|
||||
pub use core::EngineCore;
|
||||
pub use events::EngineEvent;
|
||||
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 persistence::PersistenceManager;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! This module creates and manages the main window and event loop.
|
||||
//! 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::event::WindowEvent;
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
@@ -46,7 +46,7 @@ impl ApplicationHandler for DesktopApp {
|
||||
event: WindowEvent,
|
||||
) {
|
||||
// Forward all input events to the bridge first
|
||||
winit_bridge::push_window_event(&event);
|
||||
input::push_window_event(&event);
|
||||
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
|
||||
@@ -1,7 +1,54 @@
|
||||
//! Application executor - owns winit and drives Bevy ECS
|
||||
//!
|
||||
//! The executor gives us full control over the event loop and allows
|
||||
//! both the window and ECS to run unbounded (maximum performance).
|
||||
//! The executor is the bridge between the platform (winit) and the engine (Bevy ECS),
|
||||
//! 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::app::AppExit;
|
||||
@@ -10,24 +57,27 @@ use bevy::input::{
|
||||
mouse::MouseButton as BevyMouseButton,
|
||||
keyboard::KeyCode as BevyKeyCode,
|
||||
touch::{Touches, TouchInput},
|
||||
gestures::*,
|
||||
keyboard::KeyboardInput,
|
||||
mouse::{MouseButtonInput, MouseMotion, MouseWheel},
|
||||
};
|
||||
use bevy::window::{
|
||||
PrimaryWindow, WindowCreated, WindowResized, WindowScaleFactorChanged, WindowClosing,
|
||||
WindowResolution, WindowMode, WindowPosition, WindowEvent as BevyWindowEvent,
|
||||
RawHandleWrapper, WindowWrapper,
|
||||
CursorMoved, CursorEntered, CursorLeft,
|
||||
WindowFocused, WindowOccluded, WindowMoved, WindowThemeChanged, WindowDestroyed,
|
||||
FileDragAndDrop, Ime, WindowCloseRequested,
|
||||
};
|
||||
use bevy::ecs::message::Messages;
|
||||
use libmarathon::engine::InputEvent;
|
||||
use libmarathon::platform::desktop;
|
||||
use crate::platform::input::{InputEvent, InputEventBuffer};
|
||||
use super::{push_window_event, push_device_event, drain_as_input_events, set_scale_factor};
|
||||
use std::sync::Arc;
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::event::{Event as WinitEvent, WindowEvent as WinitWindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
|
||||
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
|
||||
enum AppHandler {
|
||||
Initializing { app: Option<App> },
|
||||
@@ -125,7 +175,7 @@ impl AppHandler {
|
||||
let scale_factor = winit_window.scale_factor();
|
||||
|
||||
// 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)
|
||||
let mut window = bevy::window::Window {
|
||||
@@ -180,7 +230,7 @@ impl AppHandler {
|
||||
fn shutdown(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if let AppHandler::Running {
|
||||
bevy_window_entity,
|
||||
ref mut bevy_app,
|
||||
bevy_app,
|
||||
..
|
||||
} = self
|
||||
{
|
||||
@@ -212,14 +262,6 @@ impl ApplicationHandler for AppHandler {
|
||||
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(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
@@ -228,16 +270,16 @@ impl ApplicationHandler for AppHandler {
|
||||
) {
|
||||
// Only handle events if we're in Running state
|
||||
let AppHandler::Running {
|
||||
ref window,
|
||||
window,
|
||||
bevy_window_entity,
|
||||
ref mut bevy_app,
|
||||
bevy_app,
|
||||
} = self
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Forward input events to platform bridge
|
||||
desktop::push_window_event(&event);
|
||||
push_window_event(&event);
|
||||
|
||||
match event {
|
||||
WinitWindowEvent::CloseRequested => {
|
||||
@@ -259,7 +301,7 @@ impl ApplicationHandler for AppHandler {
|
||||
|
||||
WinitWindowEvent::RedrawRequested => {
|
||||
// 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)
|
||||
{
|
||||
@@ -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) {
|
||||
// Ensure we keep rendering even if no window events arrive.
|
||||
// This is needed for unbounded mode with ControlFlow::Poll to maintain
|
||||
// maximum frame rate by continuously requesting redraws.
|
||||
if let AppHandler::Running { ref window, .. } = self {
|
||||
if let AppHandler::Running { window, .. } = self {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
@@ -348,13 +417,14 @@ impl ApplicationHandler for AppHandler {
|
||||
///
|
||||
/// ```no_run
|
||||
/// use bevy::prelude::*;
|
||||
/// use libmarathon::platform::desktop;
|
||||
///
|
||||
/// let app = App::new();
|
||||
/// // ... 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)
|
||||
let event_loop = EventLoop::new()?;
|
||||
|
||||
617
crates/libmarathon/src/platform/desktop/input.rs
Normal file
617
crates/libmarathon/src/platform/desktop/input.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 winit_bridge;
|
||||
mod executor;
|
||||
mod input;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
//! - Accessibility (alternative input methods)
|
||||
//! - Context-aware bindings (different actions in different modes)
|
||||
|
||||
use super::game_actions::GameAction;
|
||||
use super::input_events::{InputEvent, KeyCode, MouseButton, TouchPhase};
|
||||
use crate::engine::GameAction;
|
||||
use super::events::{InputEvent, KeyCode, MouseButton, TouchPhase};
|
||||
use glam::Vec2;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -196,6 +196,55 @@ impl InputController {
|
||||
InputEvent::Touch { pos, phase, id: _ } => {
|
||||
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
|
||||
@@ -115,6 +115,34 @@ pub enum InputEvent {
|
||||
/// Current mouse position
|
||||
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 {
|
||||
@@ -126,7 +154,12 @@ impl InputEvent {
|
||||
InputEvent::MouseMove { pos } => Some(*pos),
|
||||
InputEvent::Touch { 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::Mouse { 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 {
|
||||
match self.phase() {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
17
crates/libmarathon/src/platform/input/mod.rs
Normal file
17
crates/libmarathon/src/platform/input/mod.rs
Normal 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};
|
||||
@@ -1,7 +1,11 @@
|
||||
//! Platform-specific input bridges
|
||||
//! Platform abstraction layer
|
||||
//!
|
||||
//! This module contains platform-specific code for capturing input
|
||||
//! and converting it to engine-agnostic InputEvents.
|
||||
//! This module provides platform-agnostic interfaces for OS/hardware interaction:
|
||||
//! - **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")]
|
||||
pub mod ios;
|
||||
|
||||
Reference in New Issue
Block a user