initial arhitectural overhaul
Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
90
crates/libmarathon/src/platform/desktop/event_loop.rs
Normal file
90
crates/libmarathon/src/platform/desktop/event_loop.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
//! Desktop event loop - owns winit window and event handling
|
||||
//!
|
||||
//! 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 winit::application::ApplicationHandler;
|
||||
use winit::event::WindowEvent;
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
use winit::window::{Window, WindowId};
|
||||
|
||||
/// Main event loop runner for desktop platforms
|
||||
pub struct DesktopApp {
|
||||
window: Option<Window>,
|
||||
}
|
||||
|
||||
impl DesktopApp {
|
||||
pub fn new() -> Self {
|
||||
Self { window: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for DesktopApp {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.window.is_none() {
|
||||
let window_attributes = Window::default_attributes()
|
||||
.with_title("Marathon")
|
||||
.with_inner_size(winit::dpi::LogicalSize::new(1280, 720));
|
||||
|
||||
match event_loop.create_window(window_attributes) {
|
||||
Ok(window) => {
|
||||
tracing::info!("Created winit window");
|
||||
self.window = Some(window);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to create window: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
_window_id: WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
// Forward all input events to the bridge first
|
||||
winit_bridge::push_window_event(&event);
|
||||
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
tracing::info!("Window close requested");
|
||||
event_loop.exit();
|
||||
}
|
||||
|
||||
WindowEvent::RedrawRequested => {
|
||||
// Rendering happens via Bevy
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
// Request redraw for next frame
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the desktop application with the provided game update function
|
||||
///
|
||||
/// This takes ownership of the main thread and runs the winit event loop.
|
||||
/// The update_fn is called each frame to update game logic.
|
||||
pub fn run(mut update_fn: impl FnMut() + 'static) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let event_loop = EventLoop::new()?;
|
||||
event_loop.set_control_flow(ControlFlow::Poll); // Run as fast as possible
|
||||
|
||||
let mut app = DesktopApp::new();
|
||||
|
||||
// Run the event loop, calling update_fn each frame
|
||||
event_loop.run_app(&mut app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
9
crates/libmarathon/src/platform/desktop/mod.rs
Normal file
9
crates/libmarathon/src/platform/desktop/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
//! Desktop platform integration
|
||||
//!
|
||||
//! Owns the winit event loop and converts winit events to InputEvents.
|
||||
|
||||
mod event_loop;
|
||||
mod winit_bridge;
|
||||
|
||||
pub use event_loop::run;
|
||||
pub use winit_bridge::{drain_as_input_events, push_window_event};
|
||||
225
crates/libmarathon/src/platform/desktop/winit_bridge.rs
Normal file
225
crates/libmarathon/src/platform/desktop/winit_bridge.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
//! 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 glam::Vec2;
|
||||
use std::sync::Mutex;
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
/// Thread-safe buffer for winit events
|
||||
///
|
||||
/// The winit event loop pushes events here.
|
||||
/// The engine drains them each frame.
|
||||
static BUFFER: Mutex<Vec<RawWinitEvent>> = Mutex::new(Vec::new());
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
if let Ok(mut buf) = BUFFER.lock() {
|
||||
buf.push(RawWinitEvent::MouseButton {
|
||||
button: mouse_button,
|
||||
state: *state,
|
||||
position,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
let pos = Vec2::new(position.x as f32, position.y as f32);
|
||||
|
||||
if let Ok(mut input_state) = INPUT_STATE.lock() {
|
||||
input_state.last_position = pos;
|
||||
|
||||
// Generate drag events for any pressed buttons
|
||||
if input_state.left_pressed || input_state.right_pressed || input_state.middle_pressed {
|
||||
if let Ok(mut buf) = BUFFER.lock() {
|
||||
buf.push(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() {
|
||||
if let Ok(mut buf) = BUFFER.lock() {
|
||||
buf.push(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() {
|
||||
if let Ok(mut buf) = BUFFER.lock() {
|
||||
buf.push(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.
|
||||
pub fn drain_as_input_events() -> Vec<InputEvent> {
|
||||
BUFFER
|
||||
.lock()
|
||||
.ok()
|
||||
.map(|mut b| {
|
||||
std::mem::take(&mut *b)
|
||||
.into_iter()
|
||||
.filter_map(raw_to_input_event)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 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 } => {
|
||||
// Determine which button is pressed for drag events
|
||||
let input_state = INPUT_STATE.lock().ok()?;
|
||||
|
||||
let button = if input_state.left_pressed {
|
||||
MouseButton::Left
|
||||
} else if input_state.right_pressed {
|
||||
MouseButton::Right
|
||||
} else if input_state.middle_pressed {
|
||||
MouseButton::Middle
|
||||
} else {
|
||||
return None; // No button pressed, ignore
|
||||
};
|
||||
|
||||
Some(InputEvent::Mouse {
|
||||
pos: position,
|
||||
button,
|
||||
phase: TouchPhase::Moved,
|
||||
})
|
||||
}
|
||||
|
||||
RawWinitEvent::Keyboard { key, state, modifiers } => {
|
||||
Some(InputEvent::Keyboard {
|
||||
key,
|
||||
pressed: state == ElementState::Pressed,
|
||||
modifiers,
|
||||
})
|
||||
}
|
||||
|
||||
RawWinitEvent::MouseWheel { delta, position } => {
|
||||
Some(InputEvent::MouseWheel {
|
||||
delta,
|
||||
pos: position,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
10
crates/libmarathon/src/platform/ios/mod.rs
Normal file
10
crates/libmarathon/src/platform/ios/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! iOS platform support
|
||||
//!
|
||||
//! This module contains iOS-specific input capture code.
|
||||
|
||||
pub mod pencil_bridge;
|
||||
|
||||
pub use pencil_bridge::{
|
||||
drain_as_input_events, drain_raw, pencil_point_received, swift_attach_pencil_capture,
|
||||
RawPencilPoint,
|
||||
};
|
||||
103
crates/libmarathon/src/platform/ios/pencil_bridge.rs
Normal file
103
crates/libmarathon/src/platform/ios/pencil_bridge.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
//! Apple Pencil input bridge for iOS
|
||||
//!
|
||||
//! This module captures raw Apple Pencil input via Swift/UIKit and converts
|
||||
//! it to engine-agnostic InputEvents.
|
||||
|
||||
use crate::engine::input_events::{InputEvent, TouchPhase};
|
||||
use glam::Vec2;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Raw pencil point data from Swift UITouch
|
||||
///
|
||||
/// This matches the C struct defined in PencilBridge.h
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[repr(C)] // Use C memory layout so Swift can interop
|
||||
pub struct RawPencilPoint {
|
||||
/// Screen X coordinate in points (not pixels)
|
||||
pub x: f32,
|
||||
/// Screen Y coordinate in points (not pixels)
|
||||
pub y: f32,
|
||||
/// Force/pressure (0.0 - 4.0 on Apple Pencil)
|
||||
pub force: f32,
|
||||
/// Altitude angle in radians (0 = flat, π/2 = perpendicular)
|
||||
pub altitude: f32,
|
||||
/// Azimuth angle in radians (rotation around vertical)
|
||||
pub azimuth: f32,
|
||||
/// iOS timestamp (seconds since system boot)
|
||||
pub timestamp: f64,
|
||||
/// Touch phase: 0=began, 1=moved, 2=ended
|
||||
pub phase: u8,
|
||||
}
|
||||
|
||||
/// Thread-safe buffer for pencil points
|
||||
///
|
||||
/// Swift's main thread pushes points here via C FFI.
|
||||
/// Bevy's Update schedule drains them each frame.
|
||||
static BUFFER: Mutex<Vec<RawPencilPoint>> = Mutex::new(Vec::new());
|
||||
|
||||
/// FFI function called from Swift when a pencil point is received
|
||||
///
|
||||
/// This is exposed as a C function so Swift can call it.
|
||||
/// The `#[no_mangle]` prevents Rust from changing the function name.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pencil_point_received(point: RawPencilPoint) {
|
||||
if let Ok(mut buf) = BUFFER.lock() {
|
||||
buf.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain all buffered pencil points and convert to InputEvents
|
||||
///
|
||||
/// Call this from your Bevy Update system to consume input.
|
||||
pub fn drain_as_input_events() -> Vec<InputEvent> {
|
||||
BUFFER
|
||||
.lock()
|
||||
.ok()
|
||||
.map(|mut b| {
|
||||
std::mem::take(&mut *b)
|
||||
.into_iter()
|
||||
.map(raw_to_input_event)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Drain raw pencil points without conversion
|
||||
///
|
||||
/// Useful for debugging or custom processing.
|
||||
pub fn drain_raw() -> Vec<RawPencilPoint> {
|
||||
BUFFER
|
||||
.lock()
|
||||
.ok()
|
||||
.map(|mut b| std::mem::take(&mut *b))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Convert a raw pencil point to an engine InputEvent
|
||||
fn raw_to_input_event(p: RawPencilPoint) -> InputEvent {
|
||||
InputEvent::Stylus {
|
||||
pos: Vec2::new(p.x, p.y),
|
||||
pressure: p.force,
|
||||
tilt: Vec2::new(p.altitude, p.azimuth),
|
||||
phase: match p.phase {
|
||||
0 => TouchPhase::Started,
|
||||
1 => TouchPhase::Moved,
|
||||
2 => TouchPhase::Ended,
|
||||
_ => TouchPhase::Cancelled,
|
||||
},
|
||||
timestamp: p.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach the pencil capture system to a UIView
|
||||
///
|
||||
/// This is only available on iOS. On other platforms, it's a no-op.
|
||||
#[cfg(target_os = "ios")]
|
||||
extern "C" {
|
||||
pub fn swift_attach_pencil_capture(view: *mut std::ffi::c_void);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub unsafe fn swift_attach_pencil_capture(_: *mut std::ffi::c_void) {
|
||||
// No-op on non-iOS platforms
|
||||
}
|
||||
43
crates/libmarathon/src/platform/ios/swift/PencilBridge.h
Normal file
43
crates/libmarathon/src/platform/ios/swift/PencilBridge.h
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* C header for Rust-Swift interop
|
||||
*
|
||||
* This defines the interface between Rust and Swift.
|
||||
* Both sides include this header to ensure they agree on data types.
|
||||
*/
|
||||
|
||||
#ifndef PENCIL_BRIDGE_H
|
||||
#define PENCIL_BRIDGE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/**
|
||||
* Raw pencil data from iOS UITouch
|
||||
*
|
||||
* This struct uses C types that both Rust and Swift understand.
|
||||
* The memory layout must match exactly on both sides.
|
||||
*/
|
||||
typedef struct {
|
||||
float x; // Screen X in points
|
||||
float y; // Screen Y in points
|
||||
float force; // Pressure (0.0 - 4.0)
|
||||
float altitude; // Angle from screen (radians)
|
||||
float azimuth; // Rotation angle (radians)
|
||||
double timestamp; // iOS system timestamp
|
||||
uint8_t phase; // 0=began, 1=moved, 2=ended
|
||||
} RawPencilPoint;
|
||||
|
||||
/**
|
||||
* Called from Swift when a pencil point is captured
|
||||
*
|
||||
* This is implemented in Rust (pencil_bridge.rs)
|
||||
*/
|
||||
void pencil_point_received(RawPencilPoint point);
|
||||
|
||||
/**
|
||||
* Attach pencil capture to a UIView
|
||||
*
|
||||
* This is implemented in Swift (PencilCapture.swift)
|
||||
*/
|
||||
void swift_attach_pencil_capture(void* view);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,52 @@
|
||||
import UIKit
|
||||
|
||||
@_cdecl("swift_attach_pencil_capture")
|
||||
func swiftAttachPencilCapture(_ viewPtr: UnsafeMutableRawPointer) {
|
||||
DispatchQueue.main.async {
|
||||
let view = Unmanaged<UIView>.fromOpaque(viewPtr).takeUnretainedValue()
|
||||
let recognizer = PencilGestureRecognizer()
|
||||
recognizer.cancelsTouchesInView = false
|
||||
recognizer.delaysTouchesEnded = false
|
||||
view.addGestureRecognizer(recognizer)
|
||||
print("[Swift] Pencil capture attached")
|
||||
}
|
||||
}
|
||||
|
||||
class PencilGestureRecognizer: UIGestureRecognizer {
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
state = .began
|
||||
send(touches, event: event, phase: 0)
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
state = .changed
|
||||
send(touches, event: event, phase: 1)
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
state = .ended
|
||||
send(touches, event: event, phase: 2)
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
state = .cancelled
|
||||
send(touches, event: event, phase: 2)
|
||||
}
|
||||
|
||||
private func send(_ touches: Set<UITouch>, event: UIEvent?, phase: UInt8) {
|
||||
for touch in touches where touch.type == .pencil {
|
||||
for t in event?.coalescedTouches(for: touch) ?? [touch] {
|
||||
let loc = t.preciseLocation(in: view)
|
||||
pencil_point_received(RawPencilPoint(
|
||||
x: Float(loc.x),
|
||||
y: Float(loc.y),
|
||||
force: Float(t.force),
|
||||
altitude: Float(t.altitudeAngle),
|
||||
azimuth: Float(t.azimuthAngle(in: view)),
|
||||
timestamp: t.timestamp,
|
||||
phase: phase
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
crates/libmarathon/src/platform/mod.rs
Normal file
10
crates/libmarathon/src/platform/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! Platform-specific input bridges
|
||||
//!
|
||||
//! This module contains platform-specific code for capturing input
|
||||
//! and converting it to engine-agnostic InputEvents.
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
pub mod ios;
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub mod desktop;
|
||||
Reference in New Issue
Block a user