swiftui-native 0.0.1

SwiftUI from pure Rust — no Swift bridge, no shim, only swift-runtime calls
Documentation
//! TimelineView — driving redisplay from Rust state.

use super::*;
use crate::abi;
use crate::resolve;
use core::ffi::c_void;
use crate::mangle;

// ── TimelineView — driving redisplay from Rust state ──

mangle::result_types! {
    /// `TimelineView<AnimationTimelineSchedule, AnyView>`
    /// `TimelineView<AnimationTimelineSchedule, AnyView>`
    pub(crate) TIMELINE_T = ty("TimelineView").of(vec![ty("AnimationTimelineSchedule"), ty("AnyView")]);
}

/// The Rust side of a `TimelineView` content closure.
pub(crate) type TimelineBuilder = Box<dyn Fn() -> ViewHandle>;

/// `(Context) -> Content`. The context is ignored: the point is the *call*, not
/// its argument — SwiftUI invoking this is what re-reads Rust state.
pub(crate) unsafe extern "C" fn emit_timeline_view(out: *mut u8, _context: i64, payload: *mut c_void) {
    let typed = unsafe { &*(payload as *const TypedTimelineBuilder) };
    let view = (typed.builder)();
    unsafe {
        if typed.meta.is_null() {
            *(out as *mut u64) = view.into_anyview_word();
            return;
        }
        // SwiftUI sized the content for what the probe produced. Same rule as a
        // `ForEach` row and the same one Swift enforces by making the closure's
        // result `some View`: one type, with `view_if!` for a branch.
        assert_eq!(
            view.meta, typed.meta,
            "a TimelineView builder returned {} after returning {} — it has to be \
             one type. Use `view_if!` for a branch.",
            crate::diag::type_name(view.meta),
            crate::diag::type_name(typed.meta),
        );
        let vwt = &*swift_runtime_sys::SwiftABI::get_value_witness_table(view.meta);
        if vwt.size > 0 {
            (vwt.initialize_with_copy)(out as *mut c_void, view.value_ptr() as *mut c_void, view.meta);
        }
    }
}

/// A content builder together with the type it was found to produce.
pub(crate) struct TypedTimelineBuilder {
    pub(crate) builder: TimelineBuilder,
    pub(crate) meta: *const c_void,
}

/// `TimelineView(.animation) { … }` — rebuild the content on SwiftUI's clock.
///
/// This is the answer to triggering. Assigning a hosting view's `rootView`
/// *replaces* the tree, so SwiftUI has nothing to interpolate and no reason to
/// treat a change as a change — which is why an external mutation never
/// animated. A `TimelineView` inverts it: SwiftUI calls back on every display
/// frame, so the builder re-reads Rust state and the interface follows it with
/// no `set_root` at all.
///
/// ```ignore
/// let state = BoundState::new(0.0);
/// let ui = timeline({
///     let s = state.clone();
///     move || views::offset(&views::circle(), s.get(), 0.0)
/// });
/// // mutating `state` from Rust now shows up by itself
/// ```
///
/// The builder is leaked: SwiftUI keeps calling it for as long as the view lives.
pub fn timeline(builder: impl Fn() -> ViewHandle + 'static) -> ViewHandle {
    unsafe {
        let boxed: TimelineBuilder = Box::new(builder);
        // Ask the builder what it makes. It takes no argument, so there is
        // nothing to fabricate — but it is an extra call, and a builder with
        // side effects will notice.
        let probe = boxed();
        let (probe_meta, probe_wt) = (probe.meta, probe.wt);
        drop(probe);
        let payload =
            Box::into_raw(Box::new(TypedTimelineBuilder { builder: boxed, meta: core::ptr::null() }))
                as *mut c_void;
        let ctx = Box::leak(Box::new(abi::IndexClosureCtx::new(emit_timeline_view, payload)))
            as *mut abi::IndexClosureCtx;

        // AnimationTimelineSchedule(minimumInterval: nil, paused: false)
        //   x0 = Optional<Double> payload, x1 = tag (1 = nil), x2 = paused,
        //   x8 = @out
        let sched_meta = abi::metadata0(resolve::anim_schedule_ma());
        let mut schedule =
            vec![0u8; abi::type_bytes(sched_meta)];
        core::arch::asm!("blr {f}", f = in(reg) resolve::anim_schedule_init(),
            in("x0") 0u64, in("x1") 1u64, in("x2") 0u64,
            in("x8") schedule.as_mut_ptr(),
            in("x20") sched_meta,
            lateout("x3") _, lateout("x4") _, lateout("x5") _, lateout("x6") _,
            lateout("x7") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
            lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
            lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
            clobber_abi("C"),
        );

        let sched_wt =
            abi::get_view_wt_for_proto(sched_meta, resolve::timeline_schedule_proto());
        assert!(!sched_wt.is_null(), "AnimationTimelineSchedule: TimelineSchedule");

        let (known_meta, known_wt, known_size) = result_type(&TIMELINE_T);
        // `TimelineView<Schedule, Content>`: two parameters, then their
        // conformances, so the content is at 1 and its `View` witness at 3.
        let typed = super::substitute_content(known_meta, 4, 1, 3, probe_meta, probe_wt);
        let (meta, wt) = typed.unwrap_or((known_meta, known_wt));
        let size = if typed.is_some() { abi::type_bytes(meta) } else { known_size };
        let (content_meta, content_wt) = match typed {
            Some(_) => (probe_meta, probe_wt),
            None => (resolve::anyview_meta(), resolve::anyview_wt()),
        };
        // Publish the promise only once the instantiation actually took.
        (*(payload as *mut TypedTimelineBuilder)).meta =
            if typed.is_some() { probe_meta } else { core::ptr::null() };
        let mut result = vec![0u8; size];
        // TimelineView.init(_:content:)
        //   x0 = @in schedule, x1 = closure fn, x2 = closure ctx,
        //   x3 = Schedule.Type, x4 = Content.Type,
        //   x5 = Schedule: TimelineSchedule WT, x6 = Content: View WT, x8 = @out
        core::arch::asm!("blr {f}", f = in(reg) resolve::timeline_init(),
            in("x0") schedule.as_mut_ptr(),
            in("x1") abi::index_closure_thunk as *const c_void,
            in("x2") ctx,
            in("x3") sched_meta,
            in("x4") content_meta,
            in("x5") sched_wt,
            in("x6") content_wt,
            in("x8") result.as_mut_ptr(),
            lateout("x7") _,
            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
            clobber_abi("C"),
        );
        wrap_owned(result, meta, wt)
    }
}