initial arhitectural overhaul

Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
2025-12-13 22:22:05 +00:00
parent 9d4e603db3
commit bc5b013582
99 changed files with 4137 additions and 311 deletions

View 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(())
}

View 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};

View 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,
})
}
}
}

View 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,
};

View 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
}

View 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

View File

@@ -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
))
}
}
}
}

View 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;