Skip to main content

swiftui_native/views/
timeline.rs

1//! TimelineView — driving redisplay from Rust state.
2
3use super::*;
4use crate::abi;
5use crate::resolve;
6use core::ffi::c_void;
7use crate::mangle;
8
9// ── TimelineView — driving redisplay from Rust state ──
10
11mangle::result_types! {
12    /// `TimelineView<AnimationTimelineSchedule, AnyView>`
13    /// `TimelineView<AnimationTimelineSchedule, AnyView>`
14    pub(crate) TIMELINE_T = ty("TimelineView").of(vec![ty("AnimationTimelineSchedule"), ty("AnyView")]);
15}
16
17/// The Rust side of a `TimelineView` content closure.
18pub(crate) type TimelineBuilder = Box<dyn Fn() -> ViewHandle>;
19
20/// `(Context) -> Content`. The context is ignored: the point is the *call*, not
21/// its argument — SwiftUI invoking this is what re-reads Rust state.
22pub(crate) unsafe extern "C" fn emit_timeline_view(out: *mut u8, _context: i64, payload: *mut c_void) {
23    let typed = unsafe { &*(payload as *const TypedTimelineBuilder) };
24    let view = (typed.builder)();
25    unsafe {
26        if typed.meta.is_null() {
27            *(out as *mut u64) = view.into_anyview_word();
28            return;
29        }
30        // SwiftUI sized the content for what the probe produced. Same rule as a
31        // `ForEach` row and the same one Swift enforces by making the closure's
32        // result `some View`: one type, with `view_if!` for a branch.
33        assert_eq!(
34            view.meta, typed.meta,
35            "a TimelineView builder returned {} after returning {} — it has to be \
36             one type. Use `view_if!` for a branch.",
37            crate::diag::type_name(view.meta),
38            crate::diag::type_name(typed.meta),
39        );
40        let vwt = &*swift_runtime_sys::SwiftABI::get_value_witness_table(view.meta);
41        if vwt.size > 0 {
42            (vwt.initialize_with_copy)(out as *mut c_void, view.value_ptr() as *mut c_void, view.meta);
43        }
44    }
45}
46
47/// A content builder together with the type it was found to produce.
48pub(crate) struct TypedTimelineBuilder {
49    pub(crate) builder: TimelineBuilder,
50    pub(crate) meta: *const c_void,
51}
52
53/// `TimelineView(.animation) { … }` — rebuild the content on SwiftUI's clock.
54///
55/// This is the answer to triggering. Assigning a hosting view's `rootView`
56/// *replaces* the tree, so SwiftUI has nothing to interpolate and no reason to
57/// treat a change as a change — which is why an external mutation never
58/// animated. A `TimelineView` inverts it: SwiftUI calls back on every display
59/// frame, so the builder re-reads Rust state and the interface follows it with
60/// no `set_root` at all.
61///
62/// ```ignore
63/// let state = BoundState::new(0.0);
64/// let ui = timeline({
65///     let s = state.clone();
66///     move || views::offset(&views::circle(), s.get(), 0.0)
67/// });
68/// // mutating `state` from Rust now shows up by itself
69/// ```
70///
71/// The builder is leaked: SwiftUI keeps calling it for as long as the view lives.
72pub fn timeline(builder: impl Fn() -> ViewHandle + 'static) -> ViewHandle {
73    unsafe {
74        let boxed: TimelineBuilder = Box::new(builder);
75        // Ask the builder what it makes. It takes no argument, so there is
76        // nothing to fabricate — but it is an extra call, and a builder with
77        // side effects will notice.
78        let probe = boxed();
79        let (probe_meta, probe_wt) = (probe.meta, probe.wt);
80        drop(probe);
81        let payload =
82            Box::into_raw(Box::new(TypedTimelineBuilder { builder: boxed, meta: core::ptr::null() }))
83                as *mut c_void;
84        let ctx = Box::leak(Box::new(abi::IndexClosureCtx::new(emit_timeline_view, payload)))
85            as *mut abi::IndexClosureCtx;
86
87        // AnimationTimelineSchedule(minimumInterval: nil, paused: false)
88        //   x0 = Optional<Double> payload, x1 = tag (1 = nil), x2 = paused,
89        //   x8 = @out
90        let sched_meta = abi::metadata0(resolve::anim_schedule_ma());
91        let mut schedule =
92            vec![0u8; abi::type_bytes(sched_meta)];
93        core::arch::asm!("blr {f}", f = in(reg) resolve::anim_schedule_init(),
94            in("x0") 0u64, in("x1") 1u64, in("x2") 0u64,
95            in("x8") schedule.as_mut_ptr(),
96            in("x20") sched_meta,
97            lateout("x3") _, lateout("x4") _, lateout("x5") _, lateout("x6") _,
98            lateout("x7") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
99            lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
100            lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
101            clobber_abi("C"),
102        );
103
104        let sched_wt =
105            abi::get_view_wt_for_proto(sched_meta, resolve::timeline_schedule_proto());
106        assert!(!sched_wt.is_null(), "AnimationTimelineSchedule: TimelineSchedule");
107
108        let (known_meta, known_wt, known_size) = result_type(&TIMELINE_T);
109        // `TimelineView<Schedule, Content>`: two parameters, then their
110        // conformances, so the content is at 1 and its `View` witness at 3.
111        let typed = super::substitute_content(known_meta, 4, 1, 3, probe_meta, probe_wt);
112        let (meta, wt) = typed.unwrap_or((known_meta, known_wt));
113        let size = if typed.is_some() { abi::type_bytes(meta) } else { known_size };
114        let (content_meta, content_wt) = match typed {
115            Some(_) => (probe_meta, probe_wt),
116            None => (resolve::anyview_meta(), resolve::anyview_wt()),
117        };
118        // Publish the promise only once the instantiation actually took.
119        (*(payload as *mut TypedTimelineBuilder)).meta =
120            if typed.is_some() { probe_meta } else { core::ptr::null() };
121        let mut result = vec![0u8; size];
122        // TimelineView.init(_:content:)
123        //   x0 = @in schedule, x1 = closure fn, x2 = closure ctx,
124        //   x3 = Schedule.Type, x4 = Content.Type,
125        //   x5 = Schedule: TimelineSchedule WT, x6 = Content: View WT, x8 = @out
126        core::arch::asm!("blr {f}", f = in(reg) resolve::timeline_init(),
127            in("x0") schedule.as_mut_ptr(),
128            in("x1") abi::index_closure_thunk as *const c_void,
129            in("x2") ctx,
130            in("x3") sched_meta,
131            in("x4") content_meta,
132            in("x5") sched_wt,
133            in("x6") content_wt,
134            in("x8") result.as_mut_ptr(),
135            lateout("x7") _,
136            lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
137            lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
138            lateout("x17") _, lateout("lr") _, lateout("x20") _, lateout("x21") _,
139            clobber_abi("C"),
140        );
141        wrap_owned(result, meta, wt)
142    }
143}