first successful ipad build
Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
39
Cargo.lock
generated
39
Cargo.lock
generated
@@ -2595,6 +2595,27 @@ dependencies = [
|
|||||||
"crypto-common 0.2.0-rc.4",
|
"crypto-common 0.2.0-rc.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "5.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch"
|
name = "dispatch"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -4499,6 +4520,7 @@ dependencies = [
|
|||||||
"crdts",
|
"crdts",
|
||||||
"criterion",
|
"criterion",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
|
"dirs",
|
||||||
"egui",
|
"egui",
|
||||||
"encase 0.10.0",
|
"encase 0.10.0",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
@@ -5527,6 +5549,12 @@ version = "0.1.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "orbclient"
|
name = "orbclient"
|
||||||
version = "0.3.49"
|
version = "0.3.49"
|
||||||
@@ -6201,6 +6229,17 @@ dependencies = [
|
|||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.16",
|
||||||
|
"libredox",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.12.2"
|
version = "1.12.2"
|
||||||
|
|||||||
@@ -1,217 +0,0 @@
|
|||||||
//! Bridge Bevy's input to the engine's InputEvent system
|
|
||||||
//!
|
|
||||||
//! This temporarily reads Bevy's input and converts to InputEvents.
|
|
||||||
//! Later, we'll replace this with direct winit ownership.
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use bevy::input::keyboard::KeyboardInput;
|
|
||||||
use bevy::input::mouse::{MouseButtonInput, MouseWheel};
|
|
||||||
use bevy::window::CursorMoved;
|
|
||||||
use libmarathon::platform::input::{InputEvent, InputEventBuffer, KeyCode as EngineKeyCode, MouseButton as EngineMouseButton, TouchPhase, Modifiers};
|
|
||||||
|
|
||||||
/// Convert Bevy's Vec2 to glam::Vec2
|
|
||||||
///
|
|
||||||
/// Bevy re-exports glam types, so they're the same layout.
|
|
||||||
/// We just construct a new one to be safe.
|
|
||||||
#[inline]
|
|
||||||
fn to_glam_vec2(v: bevy::math::Vec2) -> glam::Vec2 {
|
|
||||||
glam::Vec2::new(v.x, v.y)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert Bevy's KeyCode to engine's KeyCode (winit::keyboard::KeyCode)
|
|
||||||
///
|
|
||||||
/// Bevy re-exports winit's KeyCode but wraps it, so we need to extract it.
|
|
||||||
/// For now, we'll just match the common keys. TODO: Complete mapping.
|
|
||||||
fn bevy_to_engine_keycode(bevy_key: KeyCode) -> Option<EngineKeyCode> {
|
|
||||||
// In Bevy 0.17, KeyCode variants match winit directly
|
|
||||||
// We can use format matching as a temporary solution
|
|
||||||
use EngineKeyCode as E;
|
|
||||||
|
|
||||||
Some(match bevy_key {
|
|
||||||
KeyCode::KeyA => E::KeyA,
|
|
||||||
KeyCode::KeyB => E::KeyB,
|
|
||||||
KeyCode::KeyC => E::KeyC,
|
|
||||||
KeyCode::KeyD => E::KeyD,
|
|
||||||
KeyCode::KeyE => E::KeyE,
|
|
||||||
KeyCode::KeyF => E::KeyF,
|
|
||||||
KeyCode::KeyG => E::KeyG,
|
|
||||||
KeyCode::KeyH => E::KeyH,
|
|
||||||
KeyCode::KeyI => E::KeyI,
|
|
||||||
KeyCode::KeyJ => E::KeyJ,
|
|
||||||
KeyCode::KeyK => E::KeyK,
|
|
||||||
KeyCode::KeyL => E::KeyL,
|
|
||||||
KeyCode::KeyM => E::KeyM,
|
|
||||||
KeyCode::KeyN => E::KeyN,
|
|
||||||
KeyCode::KeyO => E::KeyO,
|
|
||||||
KeyCode::KeyP => E::KeyP,
|
|
||||||
KeyCode::KeyQ => E::KeyQ,
|
|
||||||
KeyCode::KeyR => E::KeyR,
|
|
||||||
KeyCode::KeyS => E::KeyS,
|
|
||||||
KeyCode::KeyT => E::KeyT,
|
|
||||||
KeyCode::KeyU => E::KeyU,
|
|
||||||
KeyCode::KeyV => E::KeyV,
|
|
||||||
KeyCode::KeyW => E::KeyW,
|
|
||||||
KeyCode::KeyX => E::KeyX,
|
|
||||||
KeyCode::KeyY => E::KeyY,
|
|
||||||
KeyCode::KeyZ => E::KeyZ,
|
|
||||||
KeyCode::Digit1 => E::Digit1,
|
|
||||||
KeyCode::Digit2 => E::Digit2,
|
|
||||||
KeyCode::Digit3 => E::Digit3,
|
|
||||||
KeyCode::Digit4 => E::Digit4,
|
|
||||||
KeyCode::Digit5 => E::Digit5,
|
|
||||||
KeyCode::Digit6 => E::Digit6,
|
|
||||||
KeyCode::Digit7 => E::Digit7,
|
|
||||||
KeyCode::Digit8 => E::Digit8,
|
|
||||||
KeyCode::Digit9 => E::Digit9,
|
|
||||||
KeyCode::Digit0 => E::Digit0,
|
|
||||||
KeyCode::Space => E::Space,
|
|
||||||
KeyCode::Enter => E::Enter,
|
|
||||||
KeyCode::Escape => E::Escape,
|
|
||||||
KeyCode::Backspace => E::Backspace,
|
|
||||||
KeyCode::Tab => E::Tab,
|
|
||||||
KeyCode::ShiftLeft => E::ShiftLeft,
|
|
||||||
KeyCode::ShiftRight => E::ShiftRight,
|
|
||||||
KeyCode::ControlLeft => E::ControlLeft,
|
|
||||||
KeyCode::ControlRight => E::ControlRight,
|
|
||||||
KeyCode::AltLeft => E::AltLeft,
|
|
||||||
KeyCode::AltRight => E::AltRight,
|
|
||||||
KeyCode::SuperLeft => E::SuperLeft,
|
|
||||||
KeyCode::SuperRight => E::SuperRight,
|
|
||||||
KeyCode::ArrowUp => E::ArrowUp,
|
|
||||||
KeyCode::ArrowDown => E::ArrowDown,
|
|
||||||
KeyCode::ArrowLeft => E::ArrowLeft,
|
|
||||||
KeyCode::ArrowRight => E::ArrowRight,
|
|
||||||
_ => return None, // Unmapped keys
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DesktopInputBridgePlugin;
|
|
||||||
|
|
||||||
impl Plugin for DesktopInputBridgePlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.init_resource::<InputEventBuffer>()
|
|
||||||
.add_systems(PreUpdate, (
|
|
||||||
clear_buffer,
|
|
||||||
collect_mouse_buttons,
|
|
||||||
collect_mouse_motion,
|
|
||||||
collect_mouse_wheel,
|
|
||||||
collect_keyboard,
|
|
||||||
).chain());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear the buffer at the start of each frame
|
|
||||||
fn clear_buffer(mut buffer: ResMut<InputEventBuffer>) {
|
|
||||||
buffer.events.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collect mouse button events
|
|
||||||
fn collect_mouse_buttons(
|
|
||||||
mut buffer: ResMut<InputEventBuffer>,
|
|
||||||
mut mouse_button_events: MessageReader<MouseButtonInput>,
|
|
||||||
windows: Query<&Window>,
|
|
||||||
) {
|
|
||||||
let cursor_pos = windows
|
|
||||||
.single()
|
|
||||||
.ok()
|
|
||||||
.and_then(|w| w.cursor_position())
|
|
||||||
.unwrap_or(Vec2::ZERO);
|
|
||||||
|
|
||||||
for event in mouse_button_events.read() {
|
|
||||||
let button = match event.button {
|
|
||||||
MouseButton::Left => EngineMouseButton::Left,
|
|
||||||
MouseButton::Right => EngineMouseButton::Right,
|
|
||||||
MouseButton::Middle => EngineMouseButton::Middle,
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let phase = if event.state.is_pressed() {
|
|
||||||
TouchPhase::Started
|
|
||||||
} else {
|
|
||||||
TouchPhase::Ended
|
|
||||||
};
|
|
||||||
|
|
||||||
buffer.events.push(InputEvent::Mouse {
|
|
||||||
pos: to_glam_vec2(cursor_pos),
|
|
||||||
button,
|
|
||||||
phase,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collect mouse motion events (for hover and drag tracking)
|
|
||||||
fn collect_mouse_motion(
|
|
||||||
mut buffer: ResMut<InputEventBuffer>,
|
|
||||||
mut cursor_moved: MessageReader<CursorMoved>,
|
|
||||||
mouse_buttons: Res<ButtonInput<MouseButton>>,
|
|
||||||
) {
|
|
||||||
for event in cursor_moved.read() {
|
|
||||||
let cursor_pos = event.position;
|
|
||||||
|
|
||||||
// ALWAYS send MouseMove for cursor tracking (hover, tooltips, etc.)
|
|
||||||
buffer.events.push(InputEvent::MouseMove {
|
|
||||||
pos: to_glam_vec2(cursor_pos),
|
|
||||||
});
|
|
||||||
|
|
||||||
// ALSO generate drag events for currently pressed buttons
|
|
||||||
if mouse_buttons.pressed(MouseButton::Left) {
|
|
||||||
buffer.events.push(InputEvent::Mouse {
|
|
||||||
pos: to_glam_vec2(cursor_pos),
|
|
||||||
button: EngineMouseButton::Left,
|
|
||||||
phase: TouchPhase::Moved,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if mouse_buttons.pressed(MouseButton::Right) {
|
|
||||||
buffer.events.push(InputEvent::Mouse {
|
|
||||||
pos: to_glam_vec2(cursor_pos),
|
|
||||||
button: EngineMouseButton::Right,
|
|
||||||
phase: TouchPhase::Moved,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collect mouse wheel events
|
|
||||||
fn collect_mouse_wheel(
|
|
||||||
mut buffer: ResMut<InputEventBuffer>,
|
|
||||||
mut wheel_events: MessageReader<MouseWheel>,
|
|
||||||
windows: Query<&Window>,
|
|
||||||
) {
|
|
||||||
let cursor_pos = windows
|
|
||||||
.single()
|
|
||||||
.ok()
|
|
||||||
.and_then(|w| w.cursor_position())
|
|
||||||
.unwrap_or(Vec2::ZERO);
|
|
||||||
|
|
||||||
for event in wheel_events.read() {
|
|
||||||
buffer.events.push(InputEvent::MouseWheel {
|
|
||||||
delta: to_glam_vec2(Vec2::new(event.x, event.y)),
|
|
||||||
pos: to_glam_vec2(cursor_pos),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collect keyboard events
|
|
||||||
fn collect_keyboard(
|
|
||||||
mut buffer: ResMut<InputEventBuffer>,
|
|
||||||
mut keyboard_events: MessageReader<KeyboardInput>,
|
|
||||||
keys: Res<ButtonInput<KeyCode>>,
|
|
||||||
) {
|
|
||||||
for event in keyboard_events.read() {
|
|
||||||
let modifiers = Modifiers {
|
|
||||||
shift: keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]),
|
|
||||||
ctrl: keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]),
|
|
||||||
alt: keys.any_pressed([KeyCode::AltLeft, KeyCode::AltRight]),
|
|
||||||
meta: keys.any_pressed([KeyCode::SuperLeft, KeyCode::SuperRight]),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert Bevy's KeyCode to engine's KeyCode
|
|
||||||
if let Some(engine_key) = bevy_to_engine_keycode(event.key_code) {
|
|
||||||
buffer.events.push(InputEvent::Keyboard {
|
|
||||||
key: engine_key,
|
|
||||||
pressed: event.state.is_pressed(),
|
|
||||||
modifiers,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +1,13 @@
|
|||||||
//! Input handling modules
|
//! Input handling for Aspen
|
||||||
//!
|
//!
|
||||||
//! This module contains platform-specific input adapters that bridge
|
//! Input flow:
|
||||||
//! native input (Bevy/winit, iOS pencil) to libmarathon's InputEvent system.
|
//! 1. Platform executor (desktop/iOS) captures native input
|
||||||
|
//! 2. Platform layer converts to InputEvents and populates InputEventBuffer
|
||||||
|
//! 3. InputHandler reads buffer and converts to GameActions
|
||||||
|
//! 4. GameActions are applied to entities
|
||||||
|
|
||||||
pub mod event_buffer;
|
pub mod event_buffer;
|
||||||
pub mod input_handler;
|
pub mod input_handler;
|
||||||
|
|
||||||
#[cfg(target_os = "ios")]
|
|
||||||
pub mod pencil;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "ios"))]
|
|
||||||
pub mod desktop_bridge;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "ios"))]
|
|
||||||
pub mod mouse;
|
|
||||||
|
|
||||||
pub use event_buffer::InputEventBuffer;
|
pub use event_buffer::InputEventBuffer;
|
||||||
pub use input_handler::InputHandlerPlugin;
|
pub use input_handler::InputHandlerPlugin;
|
||||||
|
|
||||||
#[cfg(target_os = "ios")]
|
|
||||||
pub use pencil::PencilInputPlugin;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "ios"))]
|
|
||||||
pub use desktop_bridge::DesktopInputBridgePlugin;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "ios"))]
|
|
||||||
pub use mouse::MouseInputPlugin;
|
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
//! Mouse input handling for macOS
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use bevy::input::mouse::{MouseMotion, MouseWheel};
|
|
||||||
use libmarathon::networking::{EntityLockRegistry, NetworkedEntity, NodeVectorClock};
|
|
||||||
|
|
||||||
pub struct MouseInputPlugin;
|
|
||||||
|
|
||||||
impl Plugin for MouseInputPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_systems(Update, handle_mouse_input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mouse interaction state
|
|
||||||
#[derive(Resource, Default)]
|
|
||||||
struct MouseState {
|
|
||||||
/// Whether the left mouse button is currently pressed
|
|
||||||
left_pressed: bool,
|
|
||||||
/// Whether the right mouse button is currently pressed
|
|
||||||
right_pressed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle mouse input to move and rotate cubes that are locked by us
|
|
||||||
fn handle_mouse_input(
|
|
||||||
mouse_buttons: Res<ButtonInput<MouseButton>>,
|
|
||||||
mut mouse_motion: MessageReader<MouseMotion>,
|
|
||||||
mut mouse_wheel: MessageReader<MouseWheel>,
|
|
||||||
mut mouse_state: Local<Option<MouseState>>,
|
|
||||||
lock_registry: Res<EntityLockRegistry>,
|
|
||||||
node_clock: Res<NodeVectorClock>,
|
|
||||||
mut cube_query: Query<(&NetworkedEntity, &mut Transform), With<crate::cube::CubeMarker>>,
|
|
||||||
) {
|
|
||||||
// Initialize mouse state if needed
|
|
||||||
if mouse_state.is_none() {
|
|
||||||
*mouse_state = Some(MouseState::default());
|
|
||||||
}
|
|
||||||
let state = mouse_state.as_mut().unwrap();
|
|
||||||
|
|
||||||
// Update button states
|
|
||||||
state.left_pressed = mouse_buttons.pressed(MouseButton::Left);
|
|
||||||
state.right_pressed = mouse_buttons.pressed(MouseButton::Right);
|
|
||||||
|
|
||||||
let node_id = node_clock.node_id;
|
|
||||||
|
|
||||||
// Get total mouse delta this frame
|
|
||||||
let mut total_delta = Vec2::ZERO;
|
|
||||||
for motion in mouse_motion.read() {
|
|
||||||
total_delta += motion.delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process mouse motion - only for cubes locked by us
|
|
||||||
if total_delta != Vec2::ZERO {
|
|
||||||
for (networked, mut transform) in cube_query.iter_mut() {
|
|
||||||
// Only move cubes that we have locked
|
|
||||||
if !lock_registry.is_locked_by(networked.network_id, node_id, node_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if state.left_pressed {
|
|
||||||
// Left drag: Move cube in XY plane
|
|
||||||
// Scale factor for sensitivity
|
|
||||||
let sensitivity = 0.01;
|
|
||||||
transform.translation.x += total_delta.x * sensitivity;
|
|
||||||
transform.translation.y -= total_delta.y * sensitivity; // Invert Y
|
|
||||||
// Change detection will trigger clock tick automatically
|
|
||||||
} else if state.right_pressed {
|
|
||||||
// Right drag: Rotate cube
|
|
||||||
let sensitivity = 0.01;
|
|
||||||
let rotation_x = Quat::from_rotation_y(total_delta.x * sensitivity);
|
|
||||||
let rotation_y = Quat::from_rotation_x(-total_delta.y * sensitivity);
|
|
||||||
transform.rotation = rotation_x * transform.rotation * rotation_y;
|
|
||||||
// Change detection will trigger clock tick automatically
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process mouse wheel for Z-axis movement - only for cubes locked by us
|
|
||||||
let mut total_scroll = 0.0;
|
|
||||||
for wheel in mouse_wheel.read() {
|
|
||||||
total_scroll += wheel.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
if total_scroll != 0.0 {
|
|
||||||
for (networked, mut transform) in cube_query.iter_mut() {
|
|
||||||
// Only move cubes that we have locked
|
|
||||||
if !lock_registry.is_locked_by(networked.network_id, node_id, node_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scroll: Move in Z axis
|
|
||||||
let sensitivity = 0.1;
|
|
||||||
transform.translation.z += total_scroll * sensitivity;
|
|
||||||
// Change detection will trigger clock tick automatically
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
//! Apple Pencil input system for iOS
|
|
||||||
//!
|
|
||||||
//! This module integrates the platform-agnostic pencil bridge with Bevy.
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use libmarathon::{platform::input::InputEvent, platform::ios};
|
|
||||||
|
|
||||||
pub struct PencilInputPlugin;
|
|
||||||
|
|
||||||
impl Plugin for PencilInputPlugin {
|
|
||||||
fn build(&self, app: &mut App) {
|
|
||||||
app.add_systems(Startup, attach_pencil_capture)
|
|
||||||
.add_systems(PreUpdate, poll_pencil_input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resource to track the latest pencil state
|
|
||||||
#[derive(Resource, Default)]
|
|
||||||
pub struct PencilState {
|
|
||||||
pub latest: Option<InputEvent>,
|
|
||||||
pub points_this_frame: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attach the Swift pencil capture to Bevy's window
|
|
||||||
#[cfg(target_os = "ios")]
|
|
||||||
fn attach_pencil_capture(windows: Query<&bevy::window::RawHandleWrapper, With<bevy::window::PrimaryWindow>>) {
|
|
||||||
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
|
||||||
|
|
||||||
let Ok(handle) = windows.get_single() else {
|
|
||||||
warn!("No primary window for pencil capture");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
if let Ok(raw) = handle.window_handle() {
|
|
||||||
if let RawWindowHandle::UiKit(h) = raw.as_ref() {
|
|
||||||
ios::swift_attach_pencil_capture(h.ui_view.as_ptr() as *mut _);
|
|
||||||
info!("✏️ Apple Pencil capture attached");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "ios"))]
|
|
||||||
fn attach_pencil_capture() {
|
|
||||||
// No-op on non-iOS platforms
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Poll pencil input from the platform layer and update PencilState
|
|
||||||
fn poll_pencil_input(mut commands: Commands, state: Option<ResMut<PencilState>>) {
|
|
||||||
let events = ios::drain_as_input_events();
|
|
||||||
|
|
||||||
if events.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert resource if it doesn't exist
|
|
||||||
if state.is_none() {
|
|
||||||
commands.insert_resource(PencilState::default());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(mut state) = state {
|
|
||||||
state.points_this_frame = events.len();
|
|
||||||
if let Some(latest) = events.last() {
|
|
||||||
state.latest = Some(*latest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -41,9 +41,13 @@ fn main() {
|
|||||||
)
|
)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
// Database path
|
// Application configuration
|
||||||
let db_path = PathBuf::from("cube_demo.db");
|
const APP_NAME: &str = "Aspen";
|
||||||
|
|
||||||
|
// Get platform-appropriate database path
|
||||||
|
let db_path = libmarathon::platform::get_database_path(APP_NAME);
|
||||||
let db_path_str = db_path.to_str().unwrap().to_string();
|
let db_path_str = db_path.to_str().unwrap().to_string();
|
||||||
|
info!("Database path: {}", db_path_str);
|
||||||
|
|
||||||
// Create EngineBridge (for communication between Bevy and EngineCore)
|
// Create EngineBridge (for communication between Bevy and EngineCore)
|
||||||
let (engine_bridge, engine_handle) = EngineBridge::new();
|
let (engine_bridge, engine_handle) = EngineBridge::new();
|
||||||
@@ -79,7 +83,7 @@ fn main() {
|
|||||||
|
|
||||||
// Marathon core plugins (networking, debug UI, persistence)
|
// Marathon core plugins (networking, debug UI, persistence)
|
||||||
app.add_plugins(libmarathon::MarathonPlugin::new(
|
app.add_plugins(libmarathon::MarathonPlugin::new(
|
||||||
db_path,
|
APP_NAME,
|
||||||
PersistenceConfig {
|
PersistenceConfig {
|
||||||
flush_interval_secs: 2,
|
flush_interval_secs: 2,
|
||||||
checkpoint_interval_secs: 30,
|
checkpoint_interval_secs: 30,
|
||||||
@@ -99,6 +103,5 @@ fn main() {
|
|||||||
app.add_plugins(SessionUiPlugin);
|
app.add_plugins(SessionUiPlugin);
|
||||||
app.add_systems(Startup, initialize_offline_resources);
|
app.add_systems(Startup, initialize_offline_resources);
|
||||||
|
|
||||||
// Run with our executor (unbounded event loop)
|
libmarathon::platform::run_executor(app).expect("Failed to run executor");
|
||||||
libmarathon::platform::desktop::run_executor(app).expect("Failed to run executor");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,38 +4,39 @@ version = "0.1.0"
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rusqlite = { version = "0.37.0", features = ["bundled"] }
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
|
||||||
thiserror = "2.0"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json.workspace = true
|
|
||||||
crdts.workspace = true
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
sync-macros = { path = "../sync-macros" }
|
arboard = "3.4"
|
||||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
|
||||||
toml.workspace = true
|
|
||||||
tracing.workspace = true
|
|
||||||
bevy.workspace = true
|
bevy.workspace = true
|
||||||
glam = "0.29"
|
|
||||||
winit = "0.30"
|
|
||||||
raw-window-handle = "0.6"
|
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
bytes = "1.0"
|
|
||||||
futures-lite = "2.0"
|
|
||||||
sha2 = "0.10"
|
|
||||||
blake3 = "1.5"
|
blake3 = "1.5"
|
||||||
rand = "0.8"
|
|
||||||
tokio.workspace = true
|
|
||||||
blocking = "1.6"
|
blocking = "1.6"
|
||||||
|
bytemuck = { version = "1.14", features = ["derive"] }
|
||||||
|
bytes = "1.0"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
crdts.workspace = true
|
||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
|
dirs = "5.0"
|
||||||
|
egui = { version = "0.33", default-features = false, features = ["bytemuck", "default_fonts"] }
|
||||||
|
encase = { version = "0.10", features = ["glam"] }
|
||||||
|
futures-lite = "2.0"
|
||||||
|
glam = "0.29"
|
||||||
iroh = { workspace = true, features = ["discovery-local-network"] }
|
iroh = { workspace = true, features = ["discovery-local-network"] }
|
||||||
iroh-gossip.workspace = true
|
iroh-gossip.workspace = true
|
||||||
egui = { version = "0.33", default-features = false, features = ["bytemuck", "default_fonts"] }
|
|
||||||
arboard = "3.4"
|
|
||||||
bytemuck = { version = "1.14", features = ["derive"] }
|
|
||||||
encase = { version = "0.10", features = ["glam"] }
|
|
||||||
wgpu-types = "26.0"
|
|
||||||
itertools = "0.14"
|
itertools = "0.14"
|
||||||
|
rand = "0.8"
|
||||||
|
raw-window-handle = "0.6"
|
||||||
|
rusqlite = { version = "0.37.0", features = ["bundled"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json.workspace = true
|
||||||
|
sha2 = "0.10"
|
||||||
|
sync-macros = { path = "../sync-macros" }
|
||||||
|
thiserror = "2.0"
|
||||||
|
tokio.workspace = true
|
||||||
|
toml.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||||
|
wgpu-types = "26.0"
|
||||||
|
winit = "0.30"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
|||||||
85
crates/libmarathon/build.rs
Normal file
85
crates/libmarathon/build.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Only compile Swift code when building for iOS
|
||||||
|
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||||
|
|
||||||
|
if target_os != "ios" {
|
||||||
|
println!("cargo:warning=Not building for iOS, skipping Swift compilation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("cargo:warning=Building Swift bridge for iOS");
|
||||||
|
|
||||||
|
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||||
|
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||||
|
|
||||||
|
let swift_src = manifest_dir.join("src/platform/ios/PencilCapture.swift");
|
||||||
|
let header = manifest_dir.join("src/platform/ios/PencilBridge.h");
|
||||||
|
let object_file = out_dir.join("PencilCapture.o");
|
||||||
|
|
||||||
|
// Get the iOS SDK path
|
||||||
|
let sdk_output = Command::new("xcrun")
|
||||||
|
.args(&["--sdk", "iphoneos", "--show-sdk-path"])
|
||||||
|
.output()
|
||||||
|
.expect("Failed to get iOS SDK path");
|
||||||
|
|
||||||
|
let sdk_path = String::from_utf8_lossy(&sdk_output.stdout).trim().to_string();
|
||||||
|
|
||||||
|
println!("cargo:warning=Using iOS SDK: {}", sdk_path);
|
||||||
|
|
||||||
|
// Compile Swift to object file
|
||||||
|
let status = Command::new("swiftc")
|
||||||
|
.args(&[
|
||||||
|
"-sdk", &sdk_path,
|
||||||
|
"-target", "arm64-apple-ios14.0", // Minimum iOS 14
|
||||||
|
"-import-objc-header", header.to_str().unwrap(),
|
||||||
|
"-parse-as-library",
|
||||||
|
"-c", swift_src.to_str().unwrap(),
|
||||||
|
"-o", object_file.to_str().unwrap(),
|
||||||
|
])
|
||||||
|
.status()
|
||||||
|
.expect("Failed to compile Swift");
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
panic!("Swift compilation failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("cargo:warning=Swift compilation succeeded");
|
||||||
|
|
||||||
|
// Create static library from object file
|
||||||
|
let lib_file = out_dir.join("libPencilCapture.a");
|
||||||
|
let ar_status = Command::new("ar")
|
||||||
|
.args(&[
|
||||||
|
"rcs",
|
||||||
|
lib_file.to_str().unwrap(),
|
||||||
|
object_file.to_str().unwrap(),
|
||||||
|
])
|
||||||
|
.status()
|
||||||
|
.expect("Failed to create static library");
|
||||||
|
|
||||||
|
if !ar_status.success() {
|
||||||
|
panic!("Failed to create static library");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("cargo:warning=Created static library: {}", lib_file.display());
|
||||||
|
|
||||||
|
// Tell Cargo to link the static library
|
||||||
|
println!("cargo:rustc-link-search=native={}", out_dir.display());
|
||||||
|
println!("cargo:rustc-link-lib=static=PencilCapture");
|
||||||
|
|
||||||
|
// Link Swift standard library
|
||||||
|
println!("cargo:rustc-link-lib=dylib=swiftCore");
|
||||||
|
println!("cargo:rustc-link-lib=dylib=swiftFoundation");
|
||||||
|
println!("cargo:rustc-link-lib=dylib=swiftUIKit");
|
||||||
|
|
||||||
|
// Link iOS frameworks
|
||||||
|
println!("cargo:rustc-link-lib=framework=UIKit");
|
||||||
|
println!("cargo:rustc-link-lib=framework=Foundation");
|
||||||
|
|
||||||
|
// Rerun if Swift files change
|
||||||
|
println!("cargo:rerun-if-changed={}", swift_src.display());
|
||||||
|
println!("cargo:rerun-if-changed={}", header.display());
|
||||||
|
}
|
||||||
@@ -13,19 +13,8 @@ impl PersistenceManager {
|
|||||||
pub fn new(db_path: &str) -> Self {
|
pub fn new(db_path: &str) -> Self {
|
||||||
let conn = Connection::open(db_path).expect("Failed to open database");
|
let conn = Connection::open(db_path).expect("Failed to open database");
|
||||||
|
|
||||||
// Initialize schema (Phase 1 stub - will load from file in Phase 4)
|
// Schema is handled by the migration system in persistence/migrations
|
||||||
let schema = "
|
// No hardcoded schema creation here
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
conn: Arc::new(Mutex::new(conn)),
|
conn: Arc::new(Mutex::new(conn)),
|
||||||
@@ -35,14 +24,17 @@ impl PersistenceManager {
|
|||||||
pub fn save_session(&self, session: &Session) -> anyhow::Result<()> {
|
pub fn save_session(&self, session: &Session) -> anyhow::Result<()> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
|
|
||||||
|
// Schema from migration 004: id (BLOB), code, name, created_at, last_active, entity_count, state, secret
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT OR REPLACE INTO sessions (id, state, created_at, last_active_at)
|
"INSERT OR REPLACE INTO sessions (id, code, state, created_at, last_active, entity_count)
|
||||||
VALUES (?1, ?2, ?3, ?4)",
|
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),
|
format!("{:?}", session.state),
|
||||||
session.created_at,
|
session.created_at,
|
||||||
session.last_active,
|
session.last_active,
|
||||||
|
0, // entity_count default
|
||||||
),
|
),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -53,21 +45,22 @@ impl PersistenceManager {
|
|||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
|
|
||||||
// Query for the most recently active session
|
// Query for the most recently active session
|
||||||
|
// Schema from migration 004: uses last_active (not last_active_at)
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT id, state, created_at, last_active_at
|
"SELECT code, state, created_at, last_active
|
||||||
FROM sessions
|
FROM sessions
|
||||||
ORDER BY last_active_at DESC
|
ORDER BY last_active DESC
|
||||||
LIMIT 1"
|
LIMIT 1"
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let session = stmt.query_row([], |row| {
|
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 _state: String = row.get(1)?;
|
||||||
let _created_at: String = row.get(2)?;
|
let _created_at: i64 = row.get(2)?;
|
||||||
let _last_active_at: String = row.get(3)?;
|
let _last_active: i64 = row.get(3)?;
|
||||||
|
|
||||||
// Parse session ID from code
|
// 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)))
|
Ok(Some(Session::new(session_id)))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ pub mod sync;
|
|||||||
/// For simple integration, just add this single plugin to your Bevy app.
|
/// 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.
|
/// Note: You'll still need to add your app-specific bridge/event handling.
|
||||||
pub struct MarathonPlugin {
|
pub struct MarathonPlugin {
|
||||||
|
/// Application name (used for database filename)
|
||||||
|
pub app_name: String,
|
||||||
/// Path to the persistence database
|
/// Path to the persistence database
|
||||||
pub db_path: std::path::PathBuf,
|
pub db_path: std::path::PathBuf,
|
||||||
/// Persistence configuration
|
/// Persistence configuration
|
||||||
@@ -47,9 +49,26 @@ pub struct MarathonPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MarathonPlugin {
|
impl MarathonPlugin {
|
||||||
/// Create a new MarathonPlugin with custom database path and config
|
/// Create a new MarathonPlugin with app name and config
|
||||||
pub fn new(db_path: impl Into<std::path::PathBuf>, config: persistence::PersistenceConfig) -> Self {
|
///
|
||||||
|
/// 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 {
|
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(),
|
db_path: db_path.into(),
|
||||||
persistence_config: config,
|
persistence_config: config,
|
||||||
}
|
}
|
||||||
@@ -58,6 +77,7 @@ impl MarathonPlugin {
|
|||||||
/// Create with default settings (database in current directory)
|
/// Create with default settings (database in current directory)
|
||||||
pub fn with_default_db() -> Self {
|
pub fn with_default_db() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
app_name: "marathon".to_string(),
|
||||||
db_path: "marathon.db".into(),
|
db_path: "marathon.db".into(),
|
||||||
persistence_config: Default::default(),
|
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
|
//! 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 mod pencil_bridge;
|
||||||
|
|
||||||
|
pub use executor::run_executor;
|
||||||
pub use pencil_bridge::{
|
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,
|
RawPencilPoint,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! This module captures raw Apple Pencil input via Swift/UIKit and converts
|
//! This module captures raw Apple Pencil input via Swift/UIKit and converts
|
||||||
//! it to engine-agnostic InputEvents.
|
//! it to engine-agnostic InputEvents.
|
||||||
|
|
||||||
use crate::engine::input_events::{InputEvent, TouchPhase};
|
use crate::platform::input::{InputEvent, TouchPhase};
|
||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
use std::sync::Mutex;
|
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.
|
/// This is exposed as a C function so Swift can call it.
|
||||||
/// The `#[no_mangle]` prevents Rust from changing the function name.
|
/// The `#[no_mangle]` prevents Rust from changing the function name.
|
||||||
#[no_mangle]
|
#[unsafe(no_mangle)]
|
||||||
pub extern "C" fn pencil_point_received(point: RawPencilPoint) {
|
pub extern "C" fn rust_push_pencil_point(point: RawPencilPoint) {
|
||||||
if let Ok(mut buf) = BUFFER.lock() {
|
if let Ok(mut buf) = BUFFER.lock() {
|
||||||
buf.push(point);
|
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
|
/// Drain all buffered pencil points and convert to InputEvents
|
||||||
///
|
///
|
||||||
/// Call this from your Bevy Update system to consume input.
|
/// 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")]
|
#[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);
|
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"))]
|
#[cfg(not(target_os = "ios"))]
|
||||||
pub unsafe fn swift_attach_pencil_capture(_: *mut std::ffi::c_void) {
|
pub unsafe fn swift_attach_pencil_capture(_: *mut std::ffi::c_void) {
|
||||||
// No-op on non-iOS platforms
|
// 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)
|
//! - **input**: Abstract input events (keyboard, mouse, touch, gestures)
|
||||||
//! - **desktop**: Concrete winit-based implementation for desktop platforms
|
//! - **desktop**: Concrete winit-based implementation for desktop platforms
|
||||||
//! - **ios**: Concrete UIKit-based implementation for iOS
|
//! - **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;
|
pub mod input;
|
||||||
|
|
||||||
@@ -12,3 +17,57 @@ pub mod ios;
|
|||||||
|
|
||||||
#[cfg(not(target_os = "ios"))]
|
#[cfg(not(target_os = "ios"))]
|
||||||
pub mod desktop;
|
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(".")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
278
docs/ios-deployment.md
Normal file
278
docs/ios-deployment.md
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
# iOS Deployment Guide
|
||||||
|
|
||||||
|
This guide covers building and deploying Aspen (built on Marathon engine) to iOS devices and simulators.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### Required Tools
|
||||||
|
|
||||||
|
1. **Xcode Command Line Tools** (not full Xcode IDE)
|
||||||
|
```bash
|
||||||
|
xcode-select --install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Rust iOS targets**
|
||||||
|
```bash
|
||||||
|
# For simulator (M1/M2/M3 Macs)
|
||||||
|
rustup target add aarch64-apple-ios-sim
|
||||||
|
|
||||||
|
# For device
|
||||||
|
rustup target add aarch64-apple-ios
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **iOS Simulator Runtime** (one-time download, ~8GB)
|
||||||
|
```bash
|
||||||
|
xcodebuild -downloadPlatform iOS
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Swift compiler
|
||||||
|
swiftc --version
|
||||||
|
|
||||||
|
# Check iOS SDK
|
||||||
|
xcrun --sdk iphoneos --show-sdk-path
|
||||||
|
|
||||||
|
# List available simulators
|
||||||
|
xcrun simctl list devices
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building for iOS Simulator
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
Run all steps in one go:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From project root
|
||||||
|
./scripts/ios/build-simulator.sh
|
||||||
|
./scripts/ios/package-app.sh
|
||||||
|
./scripts/ios/deploy-simulator.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step-by-Step
|
||||||
|
|
||||||
|
#### 1. Build the Binary
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build \
|
||||||
|
--package app \
|
||||||
|
--target aarch64-apple-ios-sim \
|
||||||
|
--release
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:** `target/aarch64-apple-ios-sim/release/app`
|
||||||
|
|
||||||
|
#### 2. Package as .app Bundle
|
||||||
|
|
||||||
|
iOS apps must be in a specific `.app` bundle structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
Aspen.app/
|
||||||
|
├── app # The executable binary
|
||||||
|
├── Info.plist # App metadata
|
||||||
|
└── PkgInfo # Legacy file (APPL????)
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the packaging script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/ios/package-app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:** `target/aarch64-apple-ios-sim/release/Aspen.app/`
|
||||||
|
|
||||||
|
#### 3. Deploy to Simulator
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/ios/deploy-simulator.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. Find and boot the iPad Pro 13-inch simulator
|
||||||
|
2. Open Simulator.app
|
||||||
|
3. Uninstall any previous version
|
||||||
|
4. Install the new .app bundle
|
||||||
|
5. Launch the app with console output
|
||||||
|
|
||||||
|
**To use a different simulator:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/ios/deploy-simulator.sh "iPad Air 13-inch (M2)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building for Physical Device
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
1. **Apple Developer Account** (free account works for local testing)
|
||||||
|
2. **Device connected via USB**
|
||||||
|
3. **Code signing setup**
|
||||||
|
|
||||||
|
### Build Steps
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build for device
|
||||||
|
cargo build \
|
||||||
|
--package app \
|
||||||
|
--target aarch64-apple-ios \
|
||||||
|
--release
|
||||||
|
|
||||||
|
# Package (similar to simulator)
|
||||||
|
# TODO: Create device packaging script with code signing
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Signing
|
||||||
|
|
||||||
|
iOS requires apps to be signed before running on device:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create signing identity (first time only)
|
||||||
|
security find-identity -v -p codesigning
|
||||||
|
|
||||||
|
# Sign the app bundle
|
||||||
|
codesign --force --sign "Apple Development: your@email.com" \
|
||||||
|
target/aarch64-apple-ios/release/Aspen.app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy to Device
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using devicectl (modern, requires macOS 13+)
|
||||||
|
xcrun devicectl device install app \
|
||||||
|
--device <UUID> \
|
||||||
|
target/aarch64-apple-ios/release/Aspen.app
|
||||||
|
|
||||||
|
# Launch
|
||||||
|
xcrun devicectl device process launch \
|
||||||
|
--device <UUID> \
|
||||||
|
io.r3t.aspen
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
lonni/
|
||||||
|
├── crates/
|
||||||
|
│ ├── app/ # Aspen application
|
||||||
|
│ └── libmarathon/ # Marathon engine
|
||||||
|
│ ├── src/platform/
|
||||||
|
│ │ ├── ios/
|
||||||
|
│ │ │ ├── executor.rs # iOS winit executor
|
||||||
|
│ │ │ ├── pencil_bridge.rs # Rust FFI
|
||||||
|
│ │ │ ├── PencilBridge.h # C header
|
||||||
|
│ │ │ └── PencilCapture.swift # Swift capture
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── build.rs # Swift compilation
|
||||||
|
├── scripts/
|
||||||
|
│ └── ios/
|
||||||
|
│ ├── Info.plist # App metadata template
|
||||||
|
│ ├── build-simulator.sh # Build binary
|
||||||
|
│ ├── package-app.sh # Create .app bundle
|
||||||
|
│ └── deploy-simulator.sh # Install & launch
|
||||||
|
└── docs/
|
||||||
|
└── ios-deployment.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## App Bundle Configuration
|
||||||
|
|
||||||
|
### Info.plist
|
||||||
|
|
||||||
|
Key fields in `scripts/ios/Info.plist`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.r3t.aspen</string>
|
||||||
|
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Aspen</string>
|
||||||
|
|
||||||
|
<key>MinimumOSVersion</key>
|
||||||
|
<string>14.0</string>
|
||||||
|
```
|
||||||
|
|
||||||
|
To customize, edit `scripts/ios/Info.plist` before packaging.
|
||||||
|
|
||||||
|
## Apple Pencil Support
|
||||||
|
|
||||||
|
Aspen includes native Apple Pencil support via the Marathon engine:
|
||||||
|
|
||||||
|
- **Pressure sensitivity** (0.0 - 1.0)
|
||||||
|
- **Tilt angles** (altitude & azimuth)
|
||||||
|
- **Touch phases** (began/moved/ended/cancelled)
|
||||||
|
- **Timestamps** for precision input
|
||||||
|
|
||||||
|
The Swift → Rust bridge is compiled automatically during `cargo build` for iOS targets.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "runtime profile not found"
|
||||||
|
|
||||||
|
The iOS runtime isn't installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcodebuild -downloadPlatform iOS
|
||||||
|
```
|
||||||
|
|
||||||
|
### "No signing identity found"
|
||||||
|
|
||||||
|
For simulator, signing is not required. For device:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check signing identities
|
||||||
|
security find-identity -v -p codesigning
|
||||||
|
|
||||||
|
# If none exist, create one in Xcode or via command line
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Could not launch app"
|
||||||
|
|
||||||
|
Check simulator logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcrun simctl spawn booted log stream --predicate 'processImagePath contains "Aspen"'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Swift compilation fails
|
||||||
|
|
||||||
|
Ensure the iOS SDK is installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcrun --sdk iphoneos --show-sdk-path
|
||||||
|
```
|
||||||
|
|
||||||
|
Should output a path like:
|
||||||
|
```
|
||||||
|
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS26.2.sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Notes
|
||||||
|
|
||||||
|
### Simulator vs Device
|
||||||
|
|
||||||
|
- **Simulator**: Runs on your Mac's CPU (fast for development)
|
||||||
|
- **Device**: Runs on iPad's chip (true performance testing)
|
||||||
|
|
||||||
|
### Build Times
|
||||||
|
|
||||||
|
- **Simulator build**: ~30s (incremental), ~5min (clean)
|
||||||
|
- **Device build**: Similar, but includes Swift compilation
|
||||||
|
|
||||||
|
### App Size
|
||||||
|
|
||||||
|
- **Debug build**: ~200MB (includes debug symbols)
|
||||||
|
- **Release build**: ~50MB (optimized, stripped)
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [ ] Add device deployment script with code signing
|
||||||
|
- [ ] Create GitHub Actions workflow for CI builds
|
||||||
|
- [ ] Document TestFlight deployment
|
||||||
|
- [ ] Add crash reporting integration
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Apple Developer Documentation](https://developer.apple.com/documentation/)
|
||||||
|
- [Rust iOS Guide](https://github.com/rust-mobile/rust-ios-android)
|
||||||
|
- [Winit iOS Platform](https://docs.rs/winit/latest/winit/platform/ios/)
|
||||||
46
scripts/ios/Info.plist
Normal file
46
scripts/ios/Info.plist
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>app</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.r3t.aspen</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Aspen</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>MinimumOSVersion</key>
|
||||||
|
<string>14.0</string>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
21
scripts/ios/build-simulator.sh
Executable file
21
scripts/ios/build-simulator.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# iOS Simulator Build Script
|
||||||
|
# Builds the Marathon app for iOS simulator (aarch64-apple-ios-sim)
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
echo "🔨 Building for iOS Simulator..."
|
||||||
|
echo "Project root: $PROJECT_ROOT"
|
||||||
|
|
||||||
|
# Build for simulator target
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
cargo build \
|
||||||
|
--package app \
|
||||||
|
--target aarch64-apple-ios-sim \
|
||||||
|
--release
|
||||||
|
|
||||||
|
echo "✅ Build complete!"
|
||||||
|
echo "Binary location: $PROJECT_ROOT/target/aarch64-apple-ios-sim/release/app"
|
||||||
70
scripts/ios/deploy-simulator.sh
Executable file
70
scripts/ios/deploy-simulator.sh
Executable file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# iOS Simulator Deploy Script
|
||||||
|
# Boots simulator, installs app, and launches it
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
APP_NAME="Aspen"
|
||||||
|
BUNDLE_ID="io.r3t.aspen"
|
||||||
|
TARGET="aarch64-apple-ios-sim"
|
||||||
|
BUILD_MODE="release"
|
||||||
|
|
||||||
|
APP_BUNDLE="$PROJECT_ROOT/target/$TARGET/$BUILD_MODE/$APP_NAME.app"
|
||||||
|
|
||||||
|
# Default to iPad Pro 12.9-inch M2 (matches user's physical device)
|
||||||
|
DEVICE_NAME="${1:-iPad Pro 12.9-inch M2}"
|
||||||
|
|
||||||
|
echo "📱 Deploying to iOS Simulator..."
|
||||||
|
echo "Device: $DEVICE_NAME"
|
||||||
|
echo "Bundle: $APP_BUNDLE"
|
||||||
|
|
||||||
|
# Check if bundle exists
|
||||||
|
if [ ! -d "$APP_BUNDLE" ]; then
|
||||||
|
echo "❌ App bundle not found! Run package-app.sh first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get device UUID
|
||||||
|
echo "🔍 Finding device UUID..."
|
||||||
|
DEVICE_UUID=$(xcrun simctl list devices | grep "$DEVICE_NAME" | grep -v "unavailable" | head -1 | sed -E 's/.*\(([0-9A-F-]+)\).*/\1/')
|
||||||
|
|
||||||
|
if [ -z "$DEVICE_UUID" ]; then
|
||||||
|
echo "❌ Device '$DEVICE_NAME' not found or unavailable."
|
||||||
|
echo ""
|
||||||
|
echo "Available devices:"
|
||||||
|
xcrun simctl list devices | grep -i ipad | grep -v unavailable
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Found device: $DEVICE_UUID"
|
||||||
|
|
||||||
|
# Boot device if not already booted
|
||||||
|
echo "🚀 Booting simulator..."
|
||||||
|
xcrun simctl boot "$DEVICE_UUID" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Wait a moment for boot
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Open Simulator.app
|
||||||
|
echo "📱 Opening Simulator.app..."
|
||||||
|
open -a Simulator
|
||||||
|
|
||||||
|
# Uninstall old version if it exists
|
||||||
|
echo "🗑️ Uninstalling old version..."
|
||||||
|
xcrun simctl uninstall "$DEVICE_UUID" "$BUNDLE_ID" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Install the app
|
||||||
|
echo "📲 Installing app..."
|
||||||
|
xcrun simctl install "$DEVICE_UUID" "$APP_BUNDLE"
|
||||||
|
|
||||||
|
# Launch the app
|
||||||
|
echo "🚀 Launching app..."
|
||||||
|
xcrun simctl launch --console "$DEVICE_UUID" "$BUNDLE_ID"
|
||||||
|
|
||||||
|
echo "✅ App deployed and launched!"
|
||||||
|
echo ""
|
||||||
|
echo "To view logs:"
|
||||||
|
echo " xcrun simctl spawn $DEVICE_UUID log stream --predicate 'processImagePath contains \"$APP_NAME\"'"
|
||||||
57
scripts/ios/package-app.sh
Executable file
57
scripts/ios/package-app.sh
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# iOS App Bundle Packaging Script
|
||||||
|
# Creates a proper .app bundle for iOS simulator deployment
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
APP_NAME="Aspen"
|
||||||
|
BUNDLE_ID="io.r3t.aspen"
|
||||||
|
TARGET="aarch64-apple-ios-sim"
|
||||||
|
BUILD_MODE="release"
|
||||||
|
|
||||||
|
BINARY_PATH="$PROJECT_ROOT/target/$TARGET/$BUILD_MODE/app"
|
||||||
|
APP_BUNDLE="$PROJECT_ROOT/target/$TARGET/$BUILD_MODE/$APP_NAME.app"
|
||||||
|
|
||||||
|
echo "📦 Packaging iOS app bundle..."
|
||||||
|
echo "Binary: $BINARY_PATH"
|
||||||
|
echo "Bundle: $APP_BUNDLE"
|
||||||
|
|
||||||
|
# Check if binary exists
|
||||||
|
if [ ! -f "$BINARY_PATH" ]; then
|
||||||
|
echo "❌ Binary not found! Run build-simulator.sh first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove old bundle if it exists
|
||||||
|
if [ -d "$APP_BUNDLE" ]; then
|
||||||
|
echo "🗑️ Removing old bundle..."
|
||||||
|
rm -rf "$APP_BUNDLE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create .app bundle structure
|
||||||
|
echo "📁 Creating bundle structure..."
|
||||||
|
mkdir -p "$APP_BUNDLE"
|
||||||
|
|
||||||
|
# Copy binary
|
||||||
|
echo "📋 Copying binary..."
|
||||||
|
cp "$BINARY_PATH" "$APP_BUNDLE/app"
|
||||||
|
|
||||||
|
# Copy Info.plist
|
||||||
|
echo "📋 Copying Info.plist..."
|
||||||
|
cp "$SCRIPT_DIR/Info.plist" "$APP_BUNDLE/Info.plist"
|
||||||
|
|
||||||
|
# Create PkgInfo
|
||||||
|
echo "📋 Creating PkgInfo..."
|
||||||
|
echo -n "APPL????" > "$APP_BUNDLE/PkgInfo"
|
||||||
|
|
||||||
|
# Set executable permissions
|
||||||
|
chmod +x "$APP_BUNDLE/app"
|
||||||
|
|
||||||
|
echo "✅ App bundle created successfully!"
|
||||||
|
echo "Bundle location: $APP_BUNDLE"
|
||||||
|
echo ""
|
||||||
|
echo "Bundle contents:"
|
||||||
|
ls -lh "$APP_BUNDLE"
|
||||||
Reference in New Issue
Block a user