use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
};
use blinc_animation::AnimationScheduler;
use blinc_core::context_state::{BlincContextState, HookState, SharedHookState};
use blinc_core::reactive::{ReactiveGraph, SignalId};
use blinc_layout::event_router::MouseButton;
use blinc_layout::overlay_state::OverlayContext;
use blinc_layout::prelude::*;
use blinc_layout::widgets::overlay::{overlay_manager, OverlayManager};
use blinc_platform::assets::set_global_asset_loader;
use blinc_platform_ios::{Gesture, GestureDetector, IOSAssetLoader, IOSWakeProxy, TouchPhase};
use crate::app::BlincApp;
use crate::error::{BlincError, Result};
use crate::windowed::{
RefDirtyFlag, SharedAnimationScheduler, SharedElementRegistry, SharedReactiveGraph,
SharedReadyCallbacks, WindowedContext,
};
pub struct IOSApp;
impl IOSApp {
fn init_asset_loader() {
let loader = IOSAssetLoader::new();
let _ = set_global_asset_loader(Box::new(loader));
}
fn init_theme() {
use blinc_theme::{
detect_system_color_scheme, platform_theme_bundle, set_redraw_callback, ThemeState,
};
if ThemeState::try_get().is_none() {
let bundle = platform_theme_bundle();
let scheme = detect_system_color_scheme();
ThemeState::init(bundle, scheme);
}
set_redraw_callback(|| {
tracing::debug!("Theme changed - requesting full rebuild");
blinc_layout::widgets::request_full_rebuild();
});
}
pub fn create_context(width: u32, height: u32, scale_factor: f64) -> Result<IOSRenderContext> {
tracing::info!(
"IOSApp::create_context: {}x{} physical pixels, scale_factor={}",
width,
height,
scale_factor
);
let logical_width = width as f32 / scale_factor as f32;
let logical_height = height as f32 / scale_factor as f32;
tracing::info!(
"IOSApp::create_context: {:.1}x{:.1} logical points",
logical_width,
logical_height
);
Self::init_asset_loader();
crate::text_measurer::init_text_measurer();
Self::init_theme();
let ref_dirty_flag: RefDirtyFlag = Arc::new(AtomicBool::new(false));
let reactive: SharedReactiveGraph = Arc::new(Mutex::new(ReactiveGraph::new()));
let hooks: SharedHookState = Arc::new(Mutex::new(HookState::new()));
if !BlincContextState::is_initialized() {
let stateful_callback: Arc<dyn Fn(&[SignalId]) + Send + Sync> =
Arc::new(|signal_ids| {
blinc_layout::check_stateful_deps(signal_ids);
});
BlincContextState::init_with_callback(
Arc::clone(&reactive),
Arc::clone(&hooks),
Arc::clone(&ref_dirty_flag),
stateful_callback,
);
}
let mut scheduler = AnimationScheduler::new();
let wake_proxy = IOSWakeProxy::new();
let wake_proxy_clone = wake_proxy.clone();
scheduler.set_wake_callback(move || wake_proxy_clone.wake());
scheduler.start_background();
let animations: SharedAnimationScheduler = Arc::new(Mutex::new(scheduler));
{
let scheduler_handle = animations.lock().unwrap().handle();
blinc_animation::set_global_scheduler(scheduler_handle);
}
let element_registry: SharedElementRegistry =
Arc::new(blinc_layout::selector::ElementRegistry::new());
{
let registry_for_query = Arc::clone(&element_registry);
let query_callback: blinc_core::QueryCallback = Arc::new(move |id: &str| {
registry_for_query.get(id).map(|node_id| node_id.to_raw())
});
BlincContextState::get().set_query_callback(query_callback);
}
{
let registry_for_bounds = Arc::clone(&element_registry);
let bounds_callback: blinc_core::BoundsCallback =
Arc::new(move |id: &str| registry_for_bounds.get_bounds(id));
BlincContextState::get().set_bounds_callback(bounds_callback);
}
BlincContextState::get()
.set_element_registry(Arc::clone(&element_registry) as blinc_core::AnyElementRegistry);
let ready_callbacks: SharedReadyCallbacks = Arc::new(Mutex::new(Vec::new()));
let overlays: OverlayManager = overlay_manager();
if !OverlayContext::is_initialized() {
OverlayContext::init(Arc::clone(&overlays));
}
blinc_theme::ThemeState::get().set_scheduler(&animations);
let shared_motion_states = blinc_layout::create_shared_motion_states();
{
let motion_states_for_callback = Arc::clone(&shared_motion_states);
let motion_callback: blinc_core::MotionStateCallback = Arc::new(move |key: &str| {
motion_states_for_callback
.read()
.ok()
.and_then(|states| states.get(key).copied())
.unwrap_or(blinc_core::MotionAnimationState::NotFound)
});
BlincContextState::get().set_motion_state_callback(motion_callback);
}
let logical_width = width as f32 / scale_factor as f32;
let logical_height = height as f32 / scale_factor as f32;
BlincContextState::get().set_viewport_size(logical_width, logical_height);
let windowed_ctx = WindowedContext::new_ios(
logical_width,
logical_height,
scale_factor,
width as f32,
height as f32,
true, Arc::clone(&animations),
Arc::clone(&ref_dirty_flag),
Arc::clone(&reactive),
Arc::clone(&hooks),
Arc::clone(&overlays),
Arc::clone(&element_registry),
Arc::clone(&ready_callbacks),
);
let mut render_state = blinc_layout::RenderState::new(Arc::clone(&animations));
render_state.set_shared_motion_states(Arc::clone(&shared_motion_states));
Ok(IOSRenderContext {
windowed_ctx,
render_state,
render_tree: None,
ref_dirty_flag,
animations,
ready_callbacks,
wake_proxy,
rebuild_count: 0,
last_touch_pos: None,
is_scrolling: false,
gesture_detector: GestureDetector::new(),
last_frame_time_ms: 0,
})
}
pub fn system_font_paths() -> &'static [&'static str] {
blinc_platform_ios::system_font_paths()
}
}
pub struct IOSRenderContext {
pub windowed_ctx: WindowedContext,
render_state: blinc_layout::RenderState,
render_tree: Option<RenderTree>,
ref_dirty_flag: RefDirtyFlag,
animations: SharedAnimationScheduler,
ready_callbacks: SharedReadyCallbacks,
wake_proxy: IOSWakeProxy,
rebuild_count: u64,
last_touch_pos: Option<(f32, f32)>,
is_scrolling: bool,
gesture_detector: GestureDetector,
last_frame_time_ms: u64,
}
impl IOSRenderContext {
pub fn needs_render(&self) -> bool {
let dirty = self.ref_dirty_flag.load(Ordering::SeqCst);
let wake_requested = self.wake_proxy.take_wake_request();
let animations_active = self
.animations
.lock()
.map(|sched| sched.has_active_animations())
.unwrap_or(false);
let has_stateful_updates = blinc_layout::peek_needs_redraw();
let has_pending_rebuilds = blinc_layout::has_pending_subtree_rebuilds();
let css_animating = self
.render_tree
.as_ref()
.map(|tree| !tree.css_animations_empty())
.unwrap_or(false);
dirty
|| wake_requested
|| animations_active
|| has_stateful_updates
|| has_pending_rebuilds
|| css_animating
}
pub fn update_size(&mut self, width: u32, height: u32, scale_factor: f64) {
let physical_width = width as f32;
let physical_height = height as f32;
let logical_width = physical_width / scale_factor as f32;
let logical_height = physical_height / scale_factor as f32;
let changed = (self.windowed_ctx.width - logical_width).abs() > 0.1
|| (self.windowed_ctx.height - logical_height).abs() > 0.1
|| (self.windowed_ctx.scale_factor - scale_factor).abs() > 0.001;
self.windowed_ctx.width = logical_width;
self.windowed_ctx.height = logical_height;
self.windowed_ctx.physical_width = physical_width;
self.windowed_ctx.physical_height = physical_height;
self.windowed_ctx.scale_factor = scale_factor;
BlincContextState::get().set_viewport_size(logical_width, logical_height);
if changed {
tracing::debug!(
"iOS update_size: {:.1}x{:.1} logical ({:.0}x{:.0} physical) @ {:.1}x scale",
logical_width,
logical_height,
physical_width,
physical_height,
scale_factor
);
self.ref_dirty_flag.store(true, Ordering::SeqCst);
}
}
pub fn tick_scroll(&mut self) -> bool {
if let Some(ref mut tree) = self.render_tree {
let current_time = blinc_layout::prelude::elapsed_ms();
let animating = tree.tick_scroll_physics(current_time);
tree.process_pending_scroll_refs();
animating
} else {
false
}
}
pub fn build_ui<F, E>(&mut self, ui_builder: F)
where
F: FnOnce(&mut WindowedContext) -> E,
E: ElementBuilder,
{
self.ref_dirty_flag.swap(false, Ordering::SeqCst);
self.tick_scroll();
if let Ok(mut sched) = self.animations.lock() {
sched.tick();
}
let element = ui_builder(&mut self.windowed_ctx);
blinc_layout::clear_stateful_base_updaters();
blinc_layout::click_outside::clear_click_outside_handlers();
if self.render_tree.is_none() {
tracing::debug!(
"iOS build_ui: Creating tree with scale_factor={}, layout={:.1}x{:.1}",
self.windowed_ctx.scale_factor,
self.windowed_ctx.width,
self.windowed_ctx.height
);
let mut tree = RenderTree::from_element(&element);
tree.set_scale_factor(self.windowed_ctx.scale_factor as f32);
if let Some(ref stylesheet) = self.windowed_ctx.stylesheet {
tree.set_stylesheet_arc(stylesheet.clone());
}
tree.apply_all_stylesheet_styles();
tree.compute_layout(self.windowed_ctx.width, self.windowed_ctx.height);
tree.update_flip_bounds();
tree.start_all_css_animations();
self.render_tree = Some(tree);
} else if let Some(ref mut tree) = self.render_tree {
tree.clear_dirty();
*tree = RenderTree::from_element(&element);
tree.set_scale_factor(self.windowed_ctx.scale_factor as f32);
if let Some(ref stylesheet) = self.windowed_ctx.stylesheet {
tree.set_stylesheet_arc(stylesheet.clone());
}
tree.apply_all_stylesheet_styles();
tree.compute_layout(self.windowed_ctx.width, self.windowed_ctx.height);
tree.update_flip_bounds();
tree.start_all_css_animations();
}
if let Some(ref mut tree) = self.render_tree {
let current_time = blinc_layout::prelude::elapsed_ms();
let dt_ms = if self.last_frame_time_ms > 0 {
(current_time - self.last_frame_time_ms) as f32
} else {
16.0
};
{
let store = tree.css_anim_store();
let mut s = store.lock().unwrap();
s.tick(dt_ms);
}
let flip_active = tree.tick_flip_animations(dt_ms);
let css_active = tree.css_has_active() || flip_active;
if tree.stylesheet().is_some() {
tree.apply_stylesheet_state_styles(&self.windowed_ctx.event_router);
}
if css_active || !tree.css_transitions_empty() || tree.has_active_flip_animations() {
tree.apply_all_css_animation_props();
tree.apply_all_css_transition_props();
tree.apply_flip_animation_props();
if tree.apply_animated_layout_props() {
tree.compute_layout(self.windowed_ctx.width, self.windowed_ctx.height);
tree.update_flip_bounds();
}
}
self.last_frame_time_ms = current_time;
}
self.rebuild_count += 1;
if self.rebuild_count == 1 {
if let Ok(mut callbacks) = self.ready_callbacks.lock() {
for callback in callbacks.drain(..) {
callback();
}
}
}
}
pub fn render_tree(&self) -> Option<&RenderTree> {
self.render_tree.as_ref()
}
pub fn render_state(&self) -> &blinc_layout::RenderState {
&self.render_state
}
pub fn handle_touch(&mut self, touch: blinc_platform_ios::Touch) {
use blinc_layout::tree::LayoutNodeId;
#[derive(Clone, Default)]
struct PendingEvent {
node_id: LayoutNodeId,
event_type: u32,
}
let gesture = self.gesture_detector.process(&touch);
let active_touches = self.gesture_detector.active_touch_count();
match touch.phase {
TouchPhase::Began | TouchPhase::Moved => {
self.windowed_ctx.pointer_query.set_pressure(touch.force);
self.windowed_ctx
.pointer_query
.set_touch_count(active_touches as u32);
}
TouchPhase::Ended | TouchPhase::Cancelled => {
self.windowed_ctx.pointer_query.set_pressure(0.0);
self.windowed_ctx
.pointer_query
.set_touch_count(active_touches as u32);
}
}
let tree = match &self.render_tree {
Some(t) => t,
None => {
tracing::debug!("[Blinc] iOS handle_touch: No render tree yet, ignoring touch");
return;
}
};
let lx = touch.x;
let ly = touch.y;
if let Some(root) = tree.root() {
if let Some(bounds) = tree.layout().get_bounds(root, (0.0, 0.0)) {
tracing::trace!(
"[Blinc] iOS Touch at ({:.1}, {:.1}) - tree root bounds: ({:.1}, {:.1}, {:.1}x{:.1})",
lx, ly, bounds.x, bounds.y, bounds.width, bounds.height
);
} else {
tracing::debug!("[Blinc] iOS Touch: tree root has no bounds!");
}
} else {
tracing::debug!("[Blinc] iOS Touch: tree has no root!");
}
let mut pending_events: Vec<PendingEvent> = Vec::new();
self.windowed_ctx.event_router.set_event_callback({
let events = &mut pending_events as *mut Vec<PendingEvent>;
move |node, event_type| {
unsafe {
(*events).push(PendingEvent {
node_id: node,
event_type,
});
}
}
});
let mut scroll_info: Option<(f32, f32, f32, f32)> = None;
let mut touch_ended = false;
match touch.phase {
TouchPhase::Began => {
tracing::trace!("[Blinc] iOS Touch BEGAN at ({:.1}, {:.1})", lx, ly);
self.windowed_ctx
.event_router
.on_mouse_down(tree, lx, ly, MouseButton::Left);
if active_touches == 1 {
self.last_touch_pos = Some((lx, ly));
self.is_scrolling = false;
} else {
self.last_touch_pos = None;
self.is_scrolling = false;
}
}
TouchPhase::Moved => {
self.windowed_ctx.event_router.on_mouse_move(tree, lx, ly);
if active_touches == 1 {
if let Some((prev_x, prev_y)) = self.last_touch_pos {
let delta_x = lx - prev_x;
let delta_y = ly - prev_y;
if delta_x.abs() > 0.5 || delta_y.abs() > 0.5 {
self.is_scrolling = true;
scroll_info = Some((lx, ly, delta_x, delta_y));
tracing::trace!("Touch scroll: delta=({:.1}, {:.1})", delta_x, delta_y);
}
}
self.last_touch_pos = Some((lx, ly));
} else {
self.last_touch_pos = None;
self.is_scrolling = false;
}
}
TouchPhase::Ended => {
tracing::trace!("[Blinc] iOS Touch ENDED at ({:.1}, {:.1})", lx, ly);
self.windowed_ctx
.event_router
.on_mouse_up(tree, lx, ly, MouseButton::Left);
self.windowed_ctx.event_router.on_mouse_leave();
if self.is_scrolling {
touch_ended = true;
}
self.last_touch_pos = None;
self.is_scrolling = false;
}
TouchPhase::Cancelled => {
tracing::trace!("[Blinc] iOS Touch CANCELLED");
self.windowed_ctx.event_router.on_mouse_leave();
self.last_touch_pos = None;
if self.is_scrolling {
touch_ended = true;
}
self.is_scrolling = false;
}
}
self.windowed_ctx.event_router.clear_event_callback();
tracing::trace!(
"[Blinc] iOS Touch: collected {} pending events",
pending_events.len()
);
if !pending_events.is_empty() {
tracing::trace!("[Blinc] iOS dispatching {} events", pending_events.len());
if let Some(ref mut tree) = self.render_tree {
let router = &self.windowed_ctx.event_router;
for event in pending_events {
let (bounds_x, bounds_y, bounds_width, bounds_height) = router
.get_node_bounds(event.node_id)
.unwrap_or((0.0, 0.0, 0.0, 0.0));
let local_x = lx - bounds_x;
let local_y = ly - bounds_y;
tree.dispatch_event_full(
event.node_id,
event.event_type,
lx,
ly,
local_x,
local_y,
bounds_x,
bounds_y,
bounds_width,
bounds_height,
0.0, 0.0, 1.0, );
}
}
}
if let Some(Gesture::Pinch { scale, center }) = gesture {
if let Some(ref mut tree) = self.render_tree {
let router = &mut self.windowed_ctx.event_router;
if let Some(hit) = router.hit_test(tree, center.0, center.1) {
tree.dispatch_pinch_chain(&hit, center.0, center.1, scale);
self.wake_proxy.wake();
}
}
}
if let Some((mouse_x, mouse_y, delta_x, delta_y)) = scroll_info {
if let Some(ref mut tree) = self.render_tree {
let router = &mut self.windowed_ctx.event_router;
if let Some(hit) = router.hit_test(tree, mouse_x, mouse_y) {
tracing::debug!(
"Dispatching scroll: hit={:?}, delta=({:.1}, {:.1})",
hit.node,
delta_x,
delta_y
);
tree.dispatch_scroll_chain(
hit.node,
&hit.ancestors,
mouse_x,
mouse_y,
delta_x,
delta_y,
);
self.wake_proxy.wake();
}
}
}
if touch_ended {
if let Some(ref mut tree) = self.render_tree {
tracing::debug!("Touch ended - notifying scroll physics");
tree.on_scroll_end();
self.wake_proxy.wake();
}
}
}
pub fn set_focused(&mut self, focused: bool) {
self.windowed_ctx.focused = focused;
}
}
use std::sync::OnceLock;
type RustUIBuilder =
Box<dyn Fn(&mut WindowedContext, Option<&mut RenderTree>) -> RenderTree + Send + Sync>;
static RUST_UI_BUILDER: OnceLock<RustUIBuilder> = OnceLock::new();
pub fn register_rust_ui_builder<F, E>(builder: F)
where
F: Fn(&mut WindowedContext) -> E + Send + Sync + 'static,
E: ElementBuilder + 'static,
{
let boxed_builder: RustUIBuilder = Box::new(move |ctx, _existing_tree| {
blinc_layout::clear_stateful_base_updaters();
blinc_layout::click_outside::clear_click_outside_handlers();
let element = builder(ctx);
let mut tree = RenderTree::from_element(&element);
tree.set_scale_factor(ctx.scale_factor as f32);
if let Some(ref stylesheet) = ctx.stylesheet {
tree.set_stylesheet_arc(stylesheet.clone());
}
tree.apply_all_stylesheet_styles();
tree.compute_layout(ctx.width, ctx.height);
tree.update_flip_bounds();
tree.start_all_css_animations();
tree
});
let _ = RUST_UI_BUILDER.set(boxed_builder);
}
fn get_rust_ui_builder() -> Option<&'static RustUIBuilder> {
RUST_UI_BUILDER.get()
}
pub type UIBuilderFn = extern "C" fn(ctx: *mut WindowedContext);
static mut UI_BUILDER: Option<UIBuilderFn> = None;
#[no_mangle]
pub extern "C" fn blinc_set_ui_builder(builder: UIBuilderFn) {
unsafe {
UI_BUILDER = Some(builder);
}
}
fn get_ui_builder() -> Option<UIBuilderFn> {
unsafe { UI_BUILDER }
}
#[no_mangle]
pub extern "C" fn blinc_build_frame(ctx: *mut IOSRenderContext) {
if ctx.is_null() {
return;
}
unsafe {
let ctx = &mut *ctx;
if let Ok(sched) = ctx.animations.lock() {
sched.tick();
}
if let Some(show) = blinc_layout::widgets::text_input::take_keyboard_state_change() {
extern "C" {
fn blinc_ios_show_keyboard();
fn blinc_ios_hide_keyboard();
}
if show {
blinc_ios_show_keyboard();
} else {
blinc_ios_hide_keyboard();
}
}
let has_stateful_updates = blinc_layout::take_needs_redraw();
let has_pending_rebuilds = blinc_layout::has_pending_subtree_rebuilds();
if has_stateful_updates || has_pending_rebuilds {
let prop_updates = blinc_layout::take_pending_prop_updates();
if let Some(ref mut tree) = ctx.render_tree {
for (node_id, props) in &prop_updates {
tree.update_render_props(*node_id, |p| *p = props.clone());
}
}
let mut needs_layout = false;
if let Some(ref mut tree) = ctx.render_tree {
needs_layout = tree.process_pending_subtree_rebuilds();
}
if needs_layout {
if let Some(ref mut tree) = ctx.render_tree {
tree.apply_stylesheet_layout_overrides();
tree.compute_layout(ctx.windowed_ctx.width, ctx.windowed_ctx.height);
tree.apply_flip_transitions();
tree.update_flip_bounds();
}
}
}
let needs_rebuild = ctx.ref_dirty_flag.swap(false, Ordering::SeqCst);
let no_tree_yet = ctx.render_tree.is_none();
if !needs_rebuild && !no_tree_yet {
return;
}
if let Some(rust_builder) = get_rust_ui_builder() {
let tree = rust_builder(&mut ctx.windowed_ctx, ctx.render_tree.as_mut());
ctx.render_tree = Some(tree);
} else if let Some(builder) = get_ui_builder() {
builder(&mut ctx.windowed_ctx as *mut WindowedContext);
}
}
}
#[no_mangle]
pub extern "C" fn blinc_create_context(
width: u32,
height: u32,
scale_factor: f64,
) -> *mut IOSRenderContext {
match IOSApp::create_context(width, height, scale_factor) {
Ok(ctx) => Box::into_raw(Box::new(ctx)),
Err(e) => {
tracing::error!("Failed to create iOS render context: {}", e);
std::ptr::null_mut()
}
}
}
#[no_mangle]
pub extern "C" fn blinc_needs_render(ctx: *mut IOSRenderContext) -> bool {
if ctx.is_null() {
return false;
}
unsafe { (*ctx).needs_render() }
}
#[no_mangle]
pub extern "C" fn blinc_update_size(
ctx: *mut IOSRenderContext,
width: u32,
height: u32,
scale_factor: f64,
) {
if ctx.is_null() {
return;
}
unsafe {
(*ctx).update_size(width, height, scale_factor);
}
}
#[no_mangle]
pub extern "C" fn blinc_handle_touch(
ctx: *mut IOSRenderContext,
touch_id: u64,
x: f32,
y: f32,
phase: i32,
) {
tracing::trace!(
"[Blinc FFI] blinc_handle_touch called: x={}, y={}, phase={}",
x,
y,
phase
);
if ctx.is_null() {
tracing::debug!("[Blinc FFI] blinc_handle_touch: ctx is NULL!");
return;
}
let touch_phase = match phase {
0 => TouchPhase::Began,
1 => TouchPhase::Moved,
2 => TouchPhase::Ended,
_ => TouchPhase::Cancelled,
};
let touch = blinc_platform_ios::Touch::new(touch_id, x, y, touch_phase);
unsafe {
(*ctx).handle_touch(touch);
}
tracing::trace!("[Blinc FFI] blinc_handle_touch completed");
}
#[no_mangle]
pub extern "C" fn blinc_handle_touch_with_force(
ctx: *mut IOSRenderContext,
touch_id: u64,
x: f32,
y: f32,
phase: i32,
force: f32,
) {
if ctx.is_null() {
return;
}
let touch_phase = match phase {
0 => TouchPhase::Began,
1 => TouchPhase::Moved,
2 => TouchPhase::Ended,
_ => TouchPhase::Cancelled,
};
let touch = blinc_platform_ios::Touch::with_force(touch_id, x, y, touch_phase, force);
unsafe {
(*ctx).handle_touch(touch);
}
}
#[no_mangle]
pub extern "C" fn blinc_set_focused(ctx: *mut IOSRenderContext, focused: bool) {
if ctx.is_null() {
return;
}
unsafe {
(*ctx).set_focused(focused);
}
}
#[no_mangle]
pub extern "C" fn blinc_destroy_context(ctx: *mut IOSRenderContext) {
if !ctx.is_null() {
unsafe {
drop(Box::from_raw(ctx));
}
}
}
#[no_mangle]
pub extern "C" fn blinc_tick_animations(ctx: *mut IOSRenderContext) -> bool {
if ctx.is_null() {
return false;
}
unsafe {
let ctx = &mut *ctx;
let motion_active = if let Ok(mut sched) = ctx.animations.lock() {
sched.tick()
} else {
false
};
let css_active = if let Some(ref mut tree) = ctx.render_tree {
let current_time = blinc_layout::prelude::elapsed_ms();
let dt_ms = if ctx.last_frame_time_ms > 0 {
(current_time - ctx.last_frame_time_ms) as f32
} else {
16.0
};
{
let store = tree.css_anim_store();
let mut s = store.lock().unwrap();
s.tick(dt_ms);
}
let flip_active = tree.tick_flip_animations(dt_ms);
let active = tree.css_has_active() || flip_active;
if tree.stylesheet().is_some() {
tree.apply_stylesheet_state_styles(&ctx.windowed_ctx.event_router);
}
if active || !tree.css_transitions_empty() || tree.has_active_flip_animations() {
tree.apply_all_css_animation_props();
tree.apply_all_css_transition_props();
tree.apply_flip_animation_props();
if tree.apply_animated_layout_props() {
tree.compute_layout(ctx.windowed_ctx.width, ctx.windowed_ctx.height);
tree.update_flip_bounds();
}
}
ctx.last_frame_time_ms = current_time;
active
} else {
false
};
motion_active || css_active
}
}
#[no_mangle]
pub extern "C" fn blinc_get_width(ctx: *mut IOSRenderContext) -> f32 {
if ctx.is_null() {
return 0.0;
}
unsafe { (*ctx).windowed_ctx.width }
}
#[no_mangle]
pub extern "C" fn blinc_get_height(ctx: *mut IOSRenderContext) -> f32 {
if ctx.is_null() {
return 0.0;
}
unsafe { (*ctx).windowed_ctx.height }
}
#[no_mangle]
pub extern "C" fn blinc_get_scale_factor(ctx: *mut IOSRenderContext) -> f64 {
if ctx.is_null() {
return 1.0;
}
unsafe { (*ctx).windowed_ctx.scale_factor }
}
#[no_mangle]
pub extern "C" fn blinc_get_windowed_context(ctx: *mut IOSRenderContext) -> *mut WindowedContext {
if ctx.is_null() {
return std::ptr::null_mut();
}
unsafe { &mut (*ctx).windowed_ctx as *mut WindowedContext }
}
#[no_mangle]
pub extern "C" fn blinc_mark_dirty(ctx: *mut IOSRenderContext) {
if ctx.is_null() {
return;
}
unsafe {
(*ctx).ref_dirty_flag.store(true, Ordering::SeqCst);
}
}
#[no_mangle]
pub extern "C" fn blinc_clear_dirty(ctx: *mut IOSRenderContext) {
if ctx.is_null() {
return;
}
unsafe {
(*ctx).ref_dirty_flag.store(false, Ordering::SeqCst);
}
}
#[no_mangle]
pub extern "C" fn blinc_get_physical_width(ctx: *mut IOSRenderContext) -> u32 {
if ctx.is_null() {
return 0;
}
unsafe {
let ctx = &*ctx;
(ctx.windowed_ctx.width * ctx.windowed_ctx.scale_factor as f32) as u32
}
}
#[no_mangle]
pub extern "C" fn blinc_get_physical_height(ctx: *mut IOSRenderContext) -> u32 {
if ctx.is_null() {
return 0;
}
unsafe {
let ctx = &*ctx;
(ctx.windowed_ctx.height * ctx.windowed_ctx.scale_factor as f32) as u32
}
}
pub struct IOSGpuRenderer {
app: BlincApp,
surface: wgpu::Surface<'static>,
surface_config: wgpu::SurfaceConfiguration,
render_ctx: *mut IOSRenderContext,
}
#[no_mangle]
pub extern "C" fn blinc_init_gpu(
ctx: *mut IOSRenderContext,
metal_layer: *mut std::ffi::c_void,
width: u32,
height: u32,
) -> *mut IOSGpuRenderer {
use blinc_gpu::{GpuRenderer, RendererConfig, TextRenderingContext};
if ctx.is_null() || metal_layer.is_null() {
tracing::error!("blinc_init_gpu: null context or metal_layer");
return std::ptr::null_mut();
}
let config = crate::BlincConfig::default();
let renderer_config = RendererConfig {
max_primitives: config.max_primitives,
max_glass_primitives: config.max_glass_primitives,
max_glyphs: config.max_glyphs,
sample_count: 1,
texture_format: None,
unified_text_rendering: true,
..RendererConfig::default()
};
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::METAL,
..Default::default()
});
let surface_target = wgpu::SurfaceTargetUnsafe::CoreAnimationLayer(metal_layer);
let surface = match unsafe { instance.create_surface_unsafe(surface_target) } {
Ok(s) => s,
Err(e) => {
tracing::error!("blinc_init_gpu: failed to create surface: {}", e);
return std::ptr::null_mut();
}
};
let renderer = match pollster::block_on(async {
GpuRenderer::with_instance_and_surface(instance, &surface, renderer_config).await
}) {
Ok(r) => r,
Err(e) => {
tracing::error!("blinc_init_gpu: failed to create renderer: {}", e);
return std::ptr::null_mut();
}
};
let device = renderer.device_arc();
let queue = renderer.queue_arc();
let mut text_ctx = TextRenderingContext::new(device.clone(), queue.clone());
let mut fonts_loaded = 0;
for font_path in IOSApp::system_font_paths() {
let path = std::path::Path::new(font_path);
tracing::debug!("Checking font path: {}", font_path);
if path.exists() {
match std::fs::read(path) {
Ok(data) => {
tracing::info!("Loading font from: {} ({} bytes)", font_path, data.len());
let loaded = text_ctx.load_font_data_to_registry(data);
if loaded > 0 {
tracing::info!("Successfully loaded {} faces from: {}", loaded, font_path);
fonts_loaded += loaded;
} else {
tracing::warn!("No faces loaded from font {}", font_path);
}
}
Err(e) => {
tracing::warn!("Failed to read font file {}: {}", font_path, e);
}
}
} else {
tracing::debug!("Font path does not exist: {}", font_path);
}
}
tracing::info!("Loaded {} font faces total", fonts_loaded);
text_ctx.preload_fonts(&[
".SF UI", ".SF UI Text", ".SF UI Display", "Helvetica", "Helvetica Neue", "Avenir", "Avenir Next", "Menlo", "Courier New", ]);
text_ctx.preload_generic_styles(blinc_gpu::GenericFont::SansSerif, &[400, 700], false);
tracing::info!("Font preloading complete, {} fonts loaded", fonts_loaded);
let render_context =
crate::context::RenderContext::new(renderer, text_ctx, device, queue, config.sample_count);
let app = BlincApp::from_context(render_context, config);
let format = app.texture_format();
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width,
height,
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(app.device(), &surface_config);
tracing::info!(
"blinc_init_gpu: GPU initialized ({}x{}, format: {:?})",
width,
height,
format
);
Box::into_raw(Box::new(IOSGpuRenderer {
app,
surface,
surface_config,
render_ctx: ctx,
}))
}
#[no_mangle]
pub extern "C" fn blinc_gpu_resize(gpu: *mut IOSGpuRenderer, width: u32, height: u32) {
if gpu.is_null() {
return;
}
unsafe {
let gpu = &mut *gpu;
if width > 0
&& height > 0
&& (gpu.surface_config.width != width || gpu.surface_config.height != height)
{
gpu.surface_config.width = width;
gpu.surface_config.height = height;
gpu.surface.configure(gpu.app.device(), &gpu.surface_config);
tracing::debug!("blinc_gpu_resize: {}x{}", width, height);
}
}
}
#[no_mangle]
pub extern "C" fn blinc_render_frame(gpu: *mut IOSGpuRenderer) -> bool {
if gpu.is_null() {
return false;
}
unsafe {
let gpu = &mut *gpu;
let ctx = match gpu.render_ctx.as_mut() {
Some(c) => c,
None => return false,
};
let surface_texture = match gpu.surface.get_current_texture() {
Ok(st) => st,
Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
gpu.surface.configure(gpu.app.device(), &gpu.surface_config);
match gpu.surface.get_current_texture() {
Ok(st) => st,
Err(e) => {
tracing::error!("blinc_render_frame: surface error: {:?}", e);
return false;
}
}
}
Err(e) => {
tracing::error!("blinc_render_frame: surface error: {:?}", e);
return false;
}
};
let tree = match ctx.render_tree.as_ref() {
Some(t) => t,
None => {
surface_texture.present();
return true; }
};
let view = surface_texture
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
if let Err(e) = gpu.app.render_tree_with_motion(
tree,
&ctx.render_state,
&view,
gpu.surface_config.width,
gpu.surface_config.height,
) {
tracing::error!("blinc_render_frame: render error: {}", e);
surface_texture.present();
return false;
}
surface_texture.present();
true
}
}
#[no_mangle]
pub extern "C" fn blinc_destroy_gpu(gpu: *mut IOSGpuRenderer) {
if !gpu.is_null() {
unsafe {
drop(Box::from_raw(gpu));
}
}
}
#[no_mangle]
pub extern "C" fn blinc_load_bundled_font(
gpu: *mut IOSGpuRenderer,
path: *const std::ffi::c_char,
) -> u32 {
if gpu.is_null() || path.is_null() {
return 0;
}
unsafe {
let gpu = &mut *gpu;
let path_str = match std::ffi::CStr::from_ptr(path).to_str() {
Ok(s) => s,
Err(_) => {
tracing::error!("blinc_load_bundled_font: invalid path string");
return 0;
}
};
tracing::info!("Loading bundled font from: {}", path_str);
let path = std::path::Path::new(path_str);
if !path.exists() {
tracing::error!(
"blinc_load_bundled_font: font file does not exist: {}",
path_str
);
return 0;
}
match std::fs::read(path) {
Ok(data) => {
tracing::info!("Read {} bytes from bundled font", data.len());
let loaded = gpu.app.load_font_data_to_registry(data);
tracing::info!("Loaded {} font faces from bundled font", loaded);
loaded as u32
}
Err(e) => {
tracing::error!("blinc_load_bundled_font: failed to read font: {}", e);
0
}
}
}
}
#[no_mangle]
pub extern "C" fn blinc_ios_handle_deep_link(uri: *const std::ffi::c_char) {
if uri.is_null() {
return;
}
let uri_str = unsafe { std::ffi::CStr::from_ptr(uri) };
if let Ok(uri) = uri_str.to_str() {
tracing::info!("iOS deep link received: {}", uri);
blinc_router::dispatch_deep_link(uri);
}
}
#[no_mangle]
pub extern "C" fn blinc_dispatch_stream_data(stream_id: u64, data_ptr: *const u8, data_len: u64) {
if data_ptr.is_null() || data_len == 0 {
return;
}
let bytes = unsafe { std::slice::from_raw_parts(data_ptr, data_len as usize) };
blinc_core::native_bridge::dispatch_stream_data(
stream_id,
blinc_core::native_bridge::NativeValue::Bytes(bytes.to_vec()),
);
}