first successful ipad build
Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
@@ -13,19 +13,8 @@ impl PersistenceManager {
|
||||
pub fn new(db_path: &str) -> Self {
|
||||
let conn = Connection::open(db_path).expect("Failed to open database");
|
||||
|
||||
// Initialize schema (Phase 1 stub - will load from file in Phase 4)
|
||||
let schema = "
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
state TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
last_active_at INTEGER NOT NULL
|
||||
);
|
||||
";
|
||||
|
||||
if let Err(e) = conn.execute_batch(schema) {
|
||||
tracing::warn!("Failed to initialize schema: {}", e);
|
||||
}
|
||||
// Schema is handled by the migration system in persistence/migrations
|
||||
// No hardcoded schema creation here
|
||||
|
||||
Self {
|
||||
conn: Arc::new(Mutex::new(conn)),
|
||||
@@ -35,14 +24,17 @@ impl PersistenceManager {
|
||||
pub fn save_session(&self, session: &Session) -> anyhow::Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
// Schema from migration 004: id (BLOB), code, name, created_at, last_active, entity_count, state, secret
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO sessions (id, state, created_at, last_active_at)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
"INSERT OR REPLACE INTO sessions (id, code, state, created_at, last_active, entity_count)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
(
|
||||
session.id.to_code(),
|
||||
session.id.as_uuid().as_bytes(), // id is BLOB in migration 004
|
||||
session.id.to_code(), // code is the text representation
|
||||
format!("{:?}", session.state),
|
||||
session.created_at,
|
||||
session.last_active,
|
||||
0, // entity_count default
|
||||
),
|
||||
)?;
|
||||
|
||||
@@ -53,21 +45,22 @@ impl PersistenceManager {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
|
||||
// Query for the most recently active session
|
||||
// Schema from migration 004: uses last_active (not last_active_at)
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, state, created_at, last_active_at
|
||||
"SELECT code, state, created_at, last_active
|
||||
FROM sessions
|
||||
ORDER BY last_active_at DESC
|
||||
ORDER BY last_active DESC
|
||||
LIMIT 1"
|
||||
)?;
|
||||
|
||||
let session = stmt.query_row([], |row| {
|
||||
let id_code: String = row.get(0)?;
|
||||
let code: String = row.get(0)?;
|
||||
let _state: String = row.get(1)?;
|
||||
let _created_at: String = row.get(2)?;
|
||||
let _last_active_at: String = row.get(3)?;
|
||||
let _created_at: i64 = row.get(2)?;
|
||||
let _last_active: i64 = row.get(3)?;
|
||||
|
||||
// Parse session ID from code
|
||||
if let Ok(session_id) = SessionId::from_code(&id_code) {
|
||||
if let Ok(session_id) = SessionId::from_code(&code) {
|
||||
Ok(Some(Session::new(session_id)))
|
||||
} else {
|
||||
Ok(None)
|
||||
|
||||
@@ -40,6 +40,8 @@ pub mod sync;
|
||||
/// For simple integration, just add this single plugin to your Bevy app.
|
||||
/// Note: You'll still need to add your app-specific bridge/event handling.
|
||||
pub struct MarathonPlugin {
|
||||
/// Application name (used for database filename)
|
||||
pub app_name: String,
|
||||
/// Path to the persistence database
|
||||
pub db_path: std::path::PathBuf,
|
||||
/// Persistence configuration
|
||||
@@ -47,9 +49,26 @@ pub struct MarathonPlugin {
|
||||
}
|
||||
|
||||
impl MarathonPlugin {
|
||||
/// Create a new MarathonPlugin with custom database path and config
|
||||
pub fn new(db_path: impl Into<std::path::PathBuf>, config: persistence::PersistenceConfig) -> Self {
|
||||
/// Create a new MarathonPlugin with app name and config
|
||||
///
|
||||
/// The database will be created in the platform-appropriate location:
|
||||
/// - iOS: app's Documents directory
|
||||
/// - Desktop: current directory
|
||||
pub fn new(app_name: impl Into<String>, config: persistence::PersistenceConfig) -> Self {
|
||||
let app_name = app_name.into();
|
||||
let db_path = platform::get_database_path(&app_name);
|
||||
|
||||
Self {
|
||||
app_name,
|
||||
db_path,
|
||||
persistence_config: config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new MarathonPlugin with custom database path and config
|
||||
pub fn with_custom_db(app_name: impl Into<String>, db_path: impl Into<std::path::PathBuf>, config: persistence::PersistenceConfig) -> Self {
|
||||
Self {
|
||||
app_name: app_name.into(),
|
||||
db_path: db_path.into(),
|
||||
persistence_config: config,
|
||||
}
|
||||
@@ -58,6 +77,7 @@ impl MarathonPlugin {
|
||||
/// Create with default settings (database in current directory)
|
||||
pub fn with_default_db() -> Self {
|
||||
Self {
|
||||
app_name: "marathon".to_string(),
|
||||
db_path: "marathon.db".into(),
|
||||
persistence_config: Default::default(),
|
||||
}
|
||||
|
||||
24
crates/libmarathon/src/platform/ios/PencilBridge.h
Normal file
24
crates/libmarathon/src/platform/ios/PencilBridge.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#ifndef PENCIL_BRIDGE_H
|
||||
#define PENCIL_BRIDGE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Raw Apple Pencil data point
|
||||
typedef struct {
|
||||
float x; // Screen x coordinate
|
||||
float y; // Screen y coordinate
|
||||
float force; // Pressure (0.0 - 1.0)
|
||||
float altitude; // Altitude angle in radians
|
||||
float azimuth; // Azimuth angle in radians (relative to screen)
|
||||
double timestamp; // Event timestamp
|
||||
uint8_t phase; // 0 = began, 1 = moved, 2 = ended, 3 = cancelled
|
||||
} RawPencilPoint;
|
||||
|
||||
// Swift-implemented functions
|
||||
void swift_attach_pencil_capture(void* ui_view);
|
||||
void swift_detach_pencil_capture(void* ui_view);
|
||||
|
||||
// Rust-implemented function (called from Swift)
|
||||
void rust_push_pencil_point(RawPencilPoint point);
|
||||
|
||||
#endif // PENCIL_BRIDGE_H
|
||||
105
crates/libmarathon/src/platform/ios/PencilCapture.swift
Normal file
105
crates/libmarathon/src/platform/ios/PencilCapture.swift
Normal file
@@ -0,0 +1,105 @@
|
||||
import UIKit
|
||||
|
||||
// Import the C bridge header
|
||||
// This declares rust_push_pencil_point() which we'll call
|
||||
// Note: In build.rs we'll use -import-objc-header to make this available
|
||||
|
||||
/// Custom gesture recognizer that captures Apple Pencil input
|
||||
class PencilGestureRecognizer: UIGestureRecognizer {
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
for touch in touches {
|
||||
if touch.type == .pencil || touch.type == .stylus {
|
||||
handlePencilTouch(touch, phase: 0) // began
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
for touch in touches {
|
||||
if touch.type == .pencil || touch.type == .stylus {
|
||||
handlePencilTouch(touch, phase: 1) // moved
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
for touch in touches {
|
||||
if touch.type == .pencil || touch.type == .stylus {
|
||||
handlePencilTouch(touch, phase: 2) // ended
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
for touch in touches {
|
||||
if touch.type == .pencil || touch.type == .stylus {
|
||||
handlePencilTouch(touch, phase: 3) // cancelled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handlePencilTouch(_ touch: UITouch, phase: UInt8) {
|
||||
let location = touch.location(in: view)
|
||||
|
||||
// Construct the raw pencil point
|
||||
var point = RawPencilPoint(
|
||||
x: Float(location.x),
|
||||
y: Float(location.y),
|
||||
force: Float(touch.force / touch.maximumPossibleForce),
|
||||
altitude: Float(touch.altitudeAngle),
|
||||
azimuth: Float(touch.azimuthAngle(in: view)),
|
||||
timestamp: touch.timestamp,
|
||||
phase: phase
|
||||
)
|
||||
|
||||
// Call into Rust FFI
|
||||
rust_push_pencil_point(point)
|
||||
}
|
||||
|
||||
// Allow simultaneous recognition with other gestures
|
||||
override func canPrevent(_ preventedGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override func canBePrevented(by preventingGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Static reference to the gesture recognizer (so we can detach later)
|
||||
private var pencilGestureRecognizer: PencilGestureRecognizer?
|
||||
|
||||
/// Attach pencil capture to a UIView
|
||||
@_cdecl("swift_attach_pencil_capture")
|
||||
public func swiftAttachPencilCapture(_ uiView: UnsafeMutableRawPointer) {
|
||||
guard let view = Unmanaged<UIView>.fromOpaque(uiView).takeUnretainedValue() as UIView? else {
|
||||
print("⚠️ Failed to get UIView from pointer")
|
||||
return
|
||||
}
|
||||
|
||||
// Create and attach the gesture recognizer
|
||||
let recognizer = PencilGestureRecognizer()
|
||||
recognizer.allowedTouchTypes = [NSNumber(value: UITouch.TouchType.pencil.rawValue)]
|
||||
recognizer.cancelsTouchesInView = false
|
||||
recognizer.delaysTouchesBegan = false
|
||||
recognizer.delaysTouchesEnded = false
|
||||
|
||||
view.addGestureRecognizer(recognizer)
|
||||
pencilGestureRecognizer = recognizer
|
||||
|
||||
print("✏️ Apple Pencil capture attached to view")
|
||||
}
|
||||
|
||||
/// Detach pencil capture from a UIView
|
||||
@_cdecl("swift_detach_pencil_capture")
|
||||
public func swiftDetachPencilCapture(_ uiView: UnsafeMutableRawPointer) {
|
||||
guard let view = Unmanaged<UIView>.fromOpaque(uiView).takeUnretainedValue() as UIView? else {
|
||||
return
|
||||
}
|
||||
|
||||
if let recognizer = pencilGestureRecognizer {
|
||||
view.removeGestureRecognizer(recognizer)
|
||||
pencilGestureRecognizer = nil
|
||||
print("✏️ Apple Pencil capture detached")
|
||||
}
|
||||
}
|
||||
339
crates/libmarathon/src/platform/ios/executor.rs
Normal file
339
crates/libmarathon/src/platform/ios/executor.rs
Normal file
@@ -0,0 +1,339 @@
|
||||
//! iOS application executor - owns winit and drives Bevy ECS
|
||||
//!
|
||||
//! iOS-specific implementation of the executor pattern, adapted for UIKit integration.
|
||||
//! See platform/desktop/executor.rs for detailed architecture documentation.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy::app::AppExit;
|
||||
use bevy::input::{
|
||||
ButtonInput,
|
||||
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 crate::platform::input::{InputEvent, InputEventBuffer};
|
||||
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};
|
||||
|
||||
/// Application handler state machine
|
||||
enum AppHandler {
|
||||
Initializing { app: Option<App> },
|
||||
Running {
|
||||
window: Arc<WinitWindow>,
|
||||
bevy_window_entity: Entity,
|
||||
bevy_app: App,
|
||||
},
|
||||
}
|
||||
|
||||
// Helper functions to reduce duplication
|
||||
fn send_window_created(app: &mut App, window: Entity) {
|
||||
app.world_mut()
|
||||
.resource_mut::<Messages<WindowCreated>>()
|
||||
.write(WindowCreated { window });
|
||||
}
|
||||
|
||||
fn send_window_resized(
|
||||
app: &mut App,
|
||||
window: Entity,
|
||||
physical_size: winit::dpi::PhysicalSize<u32>,
|
||||
scale_factor: f64,
|
||||
) {
|
||||
app.world_mut()
|
||||
.resource_mut::<Messages<WindowResized>>()
|
||||
.write(WindowResized {
|
||||
window,
|
||||
width: physical_size.width as f32 / scale_factor as f32,
|
||||
height: physical_size.height as f32 / scale_factor as f32,
|
||||
});
|
||||
}
|
||||
|
||||
fn send_scale_factor_changed(app: &mut App, window: Entity, scale_factor: f64) {
|
||||
app.world_mut()
|
||||
.resource_mut::<Messages<WindowScaleFactorChanged>>()
|
||||
.write(WindowScaleFactorChanged {
|
||||
window,
|
||||
scale_factor,
|
||||
});
|
||||
}
|
||||
|
||||
fn send_window_closing(app: &mut App, window: Entity) {
|
||||
app.world_mut()
|
||||
.resource_mut::<Messages<WindowClosing>>()
|
||||
.write(WindowClosing { window });
|
||||
}
|
||||
|
||||
impl AppHandler {
|
||||
/// Initialize the window and transition to Running state.
|
||||
fn initialize(&mut self, event_loop: &ActiveEventLoop) -> Result<(), String> {
|
||||
let AppHandler::Initializing { app: app_opt } = self else {
|
||||
warn!("initialize() called on non-Initializing state, ignoring");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(mut bevy_app) = app_opt.take() else {
|
||||
warn!("initialize() called twice, ignoring");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Insert InputEventBuffer resource
|
||||
bevy_app.insert_resource(InputEventBuffer::default());
|
||||
|
||||
// Initialize window message channels
|
||||
bevy_app.init_resource::<Messages<WindowCreated>>();
|
||||
bevy_app.init_resource::<Messages<WindowResized>>();
|
||||
bevy_app.init_resource::<Messages<WindowScaleFactorChanged>>();
|
||||
bevy_app.init_resource::<Messages<WindowClosing>>();
|
||||
bevy_app.init_resource::<Messages<BevyWindowEvent>>();
|
||||
|
||||
// Initialize input resources that Bevy UI and picking expect
|
||||
bevy_app.init_resource::<ButtonInput<BevyMouseButton>>();
|
||||
bevy_app.init_resource::<ButtonInput<BevyKeyCode>>();
|
||||
bevy_app.init_resource::<Touches>();
|
||||
bevy_app.init_resource::<Messages<TouchInput>>();
|
||||
|
||||
// Create the winit window BEFORE finishing the app
|
||||
let window_attributes = WindowAttributes::default()
|
||||
.with_title("Marathon")
|
||||
.with_inner_size(winit::dpi::LogicalSize::new(1280, 720));
|
||||
|
||||
let winit_window = event_loop.create_window(window_attributes)
|
||||
.map_err(|e| format!("Failed to create window: {}", e))?;
|
||||
let winit_window = Arc::new(winit_window);
|
||||
info!("Created iOS window before app.finish()");
|
||||
|
||||
let physical_size = winit_window.inner_size();
|
||||
let scale_factor = winit_window.scale_factor();
|
||||
|
||||
// iOS-specific: High DPI screens (Retina)
|
||||
// iPad Pro has scale factors of 2.0, some models 3.0
|
||||
info!("iOS scale factor: {}", scale_factor);
|
||||
|
||||
// Create window entity with all required components
|
||||
let mut window = bevy::window::Window {
|
||||
title: "Marathon".to_string(),
|
||||
resolution: WindowResolution::new(
|
||||
physical_size.width / scale_factor as u32,
|
||||
physical_size.height / scale_factor as u32,
|
||||
),
|
||||
mode: WindowMode::Windowed,
|
||||
position: WindowPosition::Automatic,
|
||||
focused: true,
|
||||
..Default::default()
|
||||
};
|
||||
window
|
||||
.resolution
|
||||
.set_scale_factor_and_apply_to_physical_size(scale_factor as f32);
|
||||
|
||||
// Create WindowWrapper and RawHandleWrapper for renderer
|
||||
let window_wrapper = WindowWrapper::new(winit_window.clone());
|
||||
let raw_handle_wrapper = RawHandleWrapper::new(&window_wrapper)
|
||||
.map_err(|e| format!("Failed to create RawHandleWrapper: {}", e))?;
|
||||
|
||||
let window_entity = bevy_app.world_mut().spawn((
|
||||
window,
|
||||
PrimaryWindow,
|
||||
raw_handle_wrapper,
|
||||
)).id();
|
||||
info!("Created window entity {}", window_entity);
|
||||
|
||||
// Send initialization event
|
||||
send_window_created(&mut bevy_app, window_entity);
|
||||
|
||||
// Now finish the app - the renderer will initialize with the window
|
||||
bevy_app.finish();
|
||||
bevy_app.cleanup();
|
||||
info!("iOS app finished and cleaned up");
|
||||
|
||||
// Transition to Running state
|
||||
*self = AppHandler::Running {
|
||||
window: winit_window,
|
||||
bevy_window_entity: window_entity,
|
||||
bevy_app,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clean shutdown of the app and window
|
||||
fn shutdown(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if let AppHandler::Running {
|
||||
bevy_window_entity,
|
||||
bevy_app,
|
||||
..
|
||||
} = self
|
||||
{
|
||||
info!("Shutting down iOS app gracefully");
|
||||
|
||||
// Send WindowClosing event
|
||||
send_window_closing(bevy_app, *bevy_window_entity);
|
||||
|
||||
// Run one final update to process close event
|
||||
bevy_app.update();
|
||||
}
|
||||
|
||||
event_loop.exit();
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for AppHandler {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
// Initialize on first resumed() call
|
||||
if let Err(e) = self.initialize(event_loop) {
|
||||
error!("Failed to initialize iOS app: {}", e);
|
||||
event_loop.exit();
|
||||
return;
|
||||
}
|
||||
info!("iOS app resumed");
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
_window_id: WindowId,
|
||||
event: WinitWindowEvent,
|
||||
) {
|
||||
// Only handle events if we're in Running state
|
||||
let AppHandler::Running {
|
||||
window,
|
||||
bevy_window_entity,
|
||||
bevy_app,
|
||||
} = self
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
match event {
|
||||
WinitWindowEvent::CloseRequested => {
|
||||
self.shutdown(event_loop);
|
||||
}
|
||||
|
||||
WinitWindowEvent::Resized(physical_size) => {
|
||||
// Update the Bevy Window component's physical resolution
|
||||
if let Some(mut window_component) = bevy_app.world_mut().get_mut::<Window>(*bevy_window_entity) {
|
||||
window_component
|
||||
.resolution
|
||||
.set_physical_resolution(physical_size.width, physical_size.height);
|
||||
}
|
||||
|
||||
// Notify Bevy systems of window resize
|
||||
let scale_factor = window.scale_factor();
|
||||
send_window_resized(bevy_app, *bevy_window_entity, physical_size, scale_factor);
|
||||
}
|
||||
|
||||
WinitWindowEvent::RedrawRequested => {
|
||||
// iOS-specific: Get pencil input from the bridge
|
||||
#[cfg(target_os = "ios")]
|
||||
let pencil_events = super::drain_as_input_events();
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
let pencil_events = vec![];
|
||||
|
||||
// Reuse buffer capacity instead of replacing (optimization)
|
||||
{
|
||||
let mut buffer = bevy_app.world_mut().resource_mut::<InputEventBuffer>();
|
||||
buffer.events.clear();
|
||||
buffer.events.extend(pencil_events);
|
||||
}
|
||||
|
||||
// Run one Bevy ECS update (unbounded)
|
||||
bevy_app.update();
|
||||
|
||||
// Check if app should exit
|
||||
if bevy_app.should_exit().is_some() {
|
||||
self.shutdown(event_loop);
|
||||
return;
|
||||
}
|
||||
|
||||
// Request next frame immediately (unbounded loop)
|
||||
window.request_redraw();
|
||||
}
|
||||
|
||||
WinitWindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
||||
// Update the Bevy Window component's scale factor
|
||||
if let Some(mut window_component) = bevy_app.world_mut().get_mut::<Window>(*bevy_window_entity) {
|
||||
let prior_factor = window_component.resolution.scale_factor();
|
||||
|
||||
window_component
|
||||
.resolution
|
||||
.set_scale_factor_and_apply_to_physical_size(scale_factor as f32);
|
||||
|
||||
send_scale_factor_changed(bevy_app, *bevy_window_entity, scale_factor);
|
||||
|
||||
info!(
|
||||
"iOS scale factor changed from {} to {} for window {:?}",
|
||||
prior_factor, scale_factor, bevy_window_entity
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
// On iOS, the app is being backgrounded
|
||||
info!("iOS app suspended (backgrounded)");
|
||||
|
||||
// TODO: Implement AppLifecycle resource to track app state
|
||||
// iOS-specific considerations:
|
||||
// 1. Release GPU resources to avoid being killed by iOS
|
||||
// 2. Stop requesting redraws to save battery
|
||||
// 3. Save any persistent state
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
// Ensure we keep rendering even if no window events arrive
|
||||
if let AppHandler::Running { window, .. } = self {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the iOS application executor.
|
||||
///
|
||||
/// This is the iOS equivalent of desktop::run_executor().
|
||||
/// See desktop/executor.rs for detailed documentation.
|
||||
///
|
||||
/// # iOS-Specific Notes
|
||||
///
|
||||
/// - Uses winit's iOS backend (backed by UIKit)
|
||||
/// - Supports Apple Pencil input via the pencil_bridge
|
||||
/// - Handles iOS lifecycle (suspended/resumed) for backgrounding
|
||||
/// - Uses Retina display scale factors (2.0-3.0)
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The event loop cannot be created
|
||||
/// - Window creation fails during initialization
|
||||
/// - The event loop encounters a fatal error
|
||||
pub fn run_executor(app: App) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let event_loop = EventLoop::new()?;
|
||||
|
||||
// Run as fast as possible (unbounded)
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
|
||||
info!("Starting iOS executor (unbounded mode)");
|
||||
|
||||
// Create handler in Initializing state with the app
|
||||
let mut handler = AppHandler::Initializing { app: Some(app) };
|
||||
|
||||
event_loop.run_app(&mut handler)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
//! iOS platform support
|
||||
//!
|
||||
//! This module contains iOS-specific input capture code.
|
||||
//! This module contains iOS-specific implementations:
|
||||
//! - Apple Pencil input capture
|
||||
//! - iOS executor (winit + UIKit integration)
|
||||
|
||||
pub mod executor;
|
||||
pub mod pencil_bridge;
|
||||
|
||||
pub use executor::run_executor;
|
||||
pub use pencil_bridge::{
|
||||
drain_as_input_events, drain_raw, pencil_point_received, swift_attach_pencil_capture,
|
||||
drain_as_input_events, drain_raw,
|
||||
pencil_point_received, rust_push_pencil_point,
|
||||
swift_attach_pencil_capture, swift_detach_pencil_capture,
|
||||
RawPencilPoint,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! 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 crate::platform::input::{InputEvent, TouchPhase};
|
||||
use glam::Vec2;
|
||||
use std::sync::Mutex;
|
||||
|
||||
@@ -39,13 +39,19 @@ static BUFFER: Mutex<Vec<RawPencilPoint>> = Mutex::new(Vec::new());
|
||||
///
|
||||
/// 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) {
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn rust_push_pencil_point(point: RawPencilPoint) {
|
||||
if let Ok(mut buf) = BUFFER.lock() {
|
||||
buf.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy alias for compatibility
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn pencil_point_received(point: RawPencilPoint) {
|
||||
rust_push_pencil_point(point);
|
||||
}
|
||||
|
||||
/// Drain all buffered pencil points and convert to InputEvents
|
||||
///
|
||||
/// Call this from your Bevy Update system to consume input.
|
||||
@@ -89,15 +95,20 @@ fn raw_to_input_event(p: RawPencilPoint) -> InputEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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" {
|
||||
unsafe extern "C" {
|
||||
/// Attach the pencil capture system to a UIView
|
||||
pub fn swift_attach_pencil_capture(view: *mut std::ffi::c_void);
|
||||
/// Detach the pencil capture system from a UIView
|
||||
pub fn swift_detach_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
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub unsafe fn swift_detach_pencil_capture(_: *mut std::ffi::c_void) {
|
||||
// No-op on non-iOS platforms
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
//! - **input**: Abstract input events (keyboard, mouse, touch, gestures)
|
||||
//! - **desktop**: Concrete winit-based implementation for desktop platforms
|
||||
//! - **ios**: Concrete UIKit-based implementation for iOS
|
||||
//!
|
||||
//! The `run_executor()` function is the main entry point and automatically
|
||||
//! selects the correct platform-specific executor based on compilation target.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub mod input;
|
||||
|
||||
@@ -12,3 +17,57 @@ pub mod ios;
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub mod desktop;
|
||||
|
||||
// Re-export the appropriate executor based on target platform
|
||||
#[cfg(target_os = "ios")]
|
||||
pub use ios::run_executor;
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub use desktop::run_executor;
|
||||
|
||||
/// Sanitize app name for safe filesystem usage
|
||||
///
|
||||
/// Removes whitespace and converts to lowercase.
|
||||
/// Example: "My App" -> "myapp"
|
||||
pub fn sanitize_app_name(app_name: &str) -> String {
|
||||
app_name.replace(char::is_whitespace, "").to_lowercase()
|
||||
}
|
||||
|
||||
/// Get the database filename for an application
|
||||
///
|
||||
/// Returns sanitized app name with .db extension.
|
||||
/// Example: "My App" -> "myapp.db"
|
||||
pub fn get_database_filename(app_name: &str) -> String {
|
||||
format!("{}.db", sanitize_app_name(app_name))
|
||||
}
|
||||
|
||||
/// Get the full database path for an application
|
||||
///
|
||||
/// Combines platform-appropriate directory with sanitized database filename.
|
||||
/// Example on iOS: "/path/to/Documents/myapp.db"
|
||||
/// Example on desktop: "./myapp.db"
|
||||
pub fn get_database_path(app_name: &str) -> PathBuf {
|
||||
get_data_directory().join(get_database_filename(app_name))
|
||||
}
|
||||
|
||||
/// Get the platform-appropriate data directory
|
||||
///
|
||||
/// - iOS: Returns the app's Documents directory (via dirs crate)
|
||||
/// - Desktop: Returns the current directory
|
||||
pub fn get_data_directory() -> PathBuf {
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
// Use dirs crate for proper iOS document directory access
|
||||
if let Some(doc_dir) = dirs::document_dir() {
|
||||
doc_dir
|
||||
} else {
|
||||
tracing::warn!("Failed to get iOS document directory, using current directory");
|
||||
PathBuf::from(".")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
{
|
||||
PathBuf::from(".")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user