tessera_ui/
prop.rs

1//! Component prop and replay abstractions.
2//!
3//! ## Usage
4//!
5//! Internal prop comparison and replay support used by generated component
6//! props.
7
8use std::{any::Any, marker::PhantomData, ptr, sync::Arc};
9
10use crate::runtime::{
11    FunctorHandle, invoke_callback_handle, invoke_callback_with_handle, invoke_render_slot_handle,
12    invoke_render_slot_with_handle, remember_callback_handle, remember_callback_with_handle,
13    remember_render_slot_handle, remember_render_slot_with_handle,
14    track_render_slot_read_dependency,
15};
16
17/// Stable, comparable slot handle for any shared callable trait object.
18///
19/// `Slot` compares by identity (`Arc::ptr_eq`) so it can be used in component
20/// props without forcing deep closure comparisons.
21pub struct Slot<F: ?Sized> {
22    inner: Arc<F>,
23}
24
25impl<F: ?Sized> Slot<F> {
26    /// Create a slot from a shared callable trait object.
27    pub fn from_shared(handler: Arc<F>) -> Self {
28        Self { inner: handler }
29    }
30
31    /// Read the current callable.
32    pub fn shared(&self) -> Arc<F> {
33        Arc::clone(&self.inner)
34    }
35}
36
37impl<F: ?Sized> Clone for Slot<F> {
38    fn clone(&self) -> Self {
39        Self {
40            inner: Arc::clone(&self.inner),
41        }
42    }
43}
44
45impl<F: ?Sized> PartialEq for Slot<F> {
46    fn eq(&self, other: &Self) -> bool {
47        Arc::ptr_eq(&self.inner, &other.inner)
48    }
49}
50
51impl<F: ?Sized> Eq for Slot<F> {}
52
53/// Stable, comparable callback handle for `Fn()`.
54///
55/// `Callback` is a stable handle to the latest callback closure created at the
56/// same build call site.
57///
58/// Callback identity does not change when the captured closure changes during
59/// recomposition. As a result, callback updates do not force prop mismatches or
60/// replay invalidation. Event handlers always invoke the latest closure stored
61/// behind the handle.
62///
63/// Create callbacks only during a Tessera component build.
64#[derive(Clone, Copy)]
65pub struct Callback {
66    repr: CallbackRepr,
67}
68
69#[derive(Clone, Copy, Eq, PartialEq)]
70enum CallbackRepr {
71    Noop,
72    Handle(FunctorHandle),
73}
74
75impl Callback {
76    /// Create a callback handle from a closure.
77    ///
78    /// This must be called during a component build.
79    #[track_caller]
80    pub fn new<F>(handler: F) -> Self
81    where
82        F: Fn() + Send + Sync + 'static,
83    {
84        Self {
85            repr: CallbackRepr::Handle(remember_callback_handle(handler)),
86        }
87    }
88
89    /// Create an empty callback.
90    pub const fn noop() -> Self {
91        Self {
92            repr: CallbackRepr::Noop,
93        }
94    }
95
96    /// Invoke the callback.
97    pub fn call(&self) {
98        match self.repr {
99            CallbackRepr::Noop => {}
100            CallbackRepr::Handle(handle) => invoke_callback_handle(handle),
101        }
102    }
103}
104
105impl<F> From<F> for Callback
106where
107    F: Fn() + Send + Sync + 'static,
108{
109    fn from(handler: F) -> Self {
110        Self::new(handler)
111    }
112}
113
114impl Default for Callback {
115    fn default() -> Self {
116        Self::noop()
117    }
118}
119
120impl PartialEq for Callback {
121    fn eq(&self, other: &Self) -> bool {
122        self.repr == other.repr
123    }
124}
125
126impl Eq for Callback {}
127
128/// Stable, comparable callback handle for `Fn(T) -> R`.
129///
130/// This follows the same stability rules as [`Callback`]: the handle remains
131/// stable across recomposition, while calls always observe the latest closure.
132///
133/// This is useful for value-change handlers and similar one-argument callbacks.
134pub struct CallbackWith<T, R = ()> {
135    repr: CallbackWithRepr<T, R>,
136}
137
138enum CallbackWithRepr<T, R> {
139    Handle(FunctorHandle),
140    Static(fn(T) -> R),
141}
142
143impl<T, R> Copy for CallbackWithRepr<T, R> {}
144
145impl<T, R> Clone for CallbackWithRepr<T, R> {
146    fn clone(&self) -> Self {
147        *self
148    }
149}
150
151impl<T, R> CallbackWith<T, R>
152where
153    T: 'static,
154    R: 'static,
155{
156    /// Create a callback handle from a closure.
157    ///
158    /// This must be called during a component build.
159    #[track_caller]
160    pub fn new<F>(handler: F) -> Self
161    where
162        F: Fn(T) -> R + Send + Sync + 'static,
163    {
164        Self {
165            repr: CallbackWithRepr::Handle(remember_callback_with_handle(handler)),
166        }
167    }
168
169    /// Invoke the callback with an argument.
170    pub fn call(&self, value: T) -> R {
171        match self.repr {
172            CallbackWithRepr::Handle(handle) => invoke_callback_with_handle(handle, value),
173            CallbackWithRepr::Static(handler) => handler(value),
174        }
175    }
176
177    fn from_static(handler: fn(T) -> R) -> Self {
178        Self {
179            repr: CallbackWithRepr::Static(handler),
180        }
181    }
182}
183
184impl<T, R, F> From<F> for CallbackWith<T, R>
185where
186    T: 'static,
187    R: 'static,
188    F: Fn(T) -> R + Send + Sync + 'static,
189{
190    fn from(handler: F) -> Self {
191        Self::new(handler)
192    }
193}
194
195impl<T, R> Copy for CallbackWith<T, R> {}
196
197impl<T, R> Clone for CallbackWith<T, R> {
198    fn clone(&self) -> Self {
199        *self
200    }
201}
202
203impl<T, R> PartialEq for CallbackWith<T, R> {
204    fn eq(&self, other: &Self) -> bool {
205        match (self.repr, other.repr) {
206            (CallbackWithRepr::Handle(lhs), CallbackWithRepr::Handle(rhs)) => lhs == rhs,
207            (CallbackWithRepr::Static(lhs), CallbackWithRepr::Static(rhs)) => {
208                ptr::fn_addr_eq(lhs, rhs)
209            }
210            _ => false,
211        }
212    }
213}
214
215impl<T, R> Eq for CallbackWith<T, R> {}
216
217impl<T, R> CallbackWith<T, R>
218where
219    T: 'static,
220    R: Default + 'static,
221{
222    /// Create a callback that ignores its input and returns
223    /// [`Default::default`].
224    pub fn default_value() -> Self {
225        fn default_value_impl<T, R: Default>(_: T) -> R {
226            R::default()
227        }
228
229        Self::from_static(default_value_impl::<T, R>)
230    }
231}
232
233impl<T> CallbackWith<T, T>
234where
235    T: 'static,
236{
237    /// Create a callback that returns its input unchanged.
238    pub fn identity() -> Self {
239        fn identity_impl<T>(value: T) -> T {
240            value
241        }
242
243        Self::from_static(identity_impl::<T>)
244    }
245}
246
247/// Stable, comparable render slot handle.
248///
249/// `RenderSlot` is a stable handle to deferred UI content.
250///
251/// Like [`Callback`], the handle stays stable across recomposition. Unlike
252/// callbacks, updating a render slot's closure invalidates component instances
253/// that rendered the slot, so slot content changes can trigger replayed
254/// components to rebuild with the latest UI.
255///
256/// Create render slots only during a Tessera component build.
257#[derive(Clone, Copy)]
258pub struct RenderSlot {
259    repr: RenderSlotRepr,
260}
261
262#[derive(Clone, Copy, Eq, PartialEq)]
263enum RenderSlotRepr {
264    Empty,
265    Handle(FunctorHandle),
266}
267
268impl RenderSlot {
269    /// Create a render slot from a closure.
270    ///
271    /// This must be called during a component build.
272    #[track_caller]
273    pub fn new<F>(render: F) -> Self
274    where
275        F: Fn() + Send + Sync + 'static,
276    {
277        Self {
278            repr: RenderSlotRepr::Handle(remember_render_slot_handle(render)),
279        }
280    }
281
282    /// Create an empty render slot.
283    pub const fn empty() -> Self {
284        Self {
285            repr: RenderSlotRepr::Empty,
286        }
287    }
288
289    fn invoke(&self) {
290        match &self.repr {
291            RenderSlotRepr::Empty => {}
292            RenderSlotRepr::Handle(handle) => invoke_render_slot_handle(*handle),
293        }
294    }
295
296    /// Execute the render closure.
297    pub fn render(&self) {
298        if let RenderSlotRepr::Handle(handle) = self.repr {
299            track_render_slot_read_dependency(handle);
300        }
301        self.invoke();
302    }
303}
304
305impl<F> From<F> for RenderSlot
306where
307    F: Fn() + Send + Sync + 'static,
308{
309    fn from(render: F) -> Self {
310        Self::new(render)
311    }
312}
313
314impl From<Callback> for RenderSlot {
315    fn from(callback: Callback) -> Self {
316        Self::new(move || callback.call())
317    }
318}
319
320impl Default for RenderSlot {
321    fn default() -> Self {
322        Self::empty()
323    }
324}
325
326impl PartialEq for RenderSlot {
327    fn eq(&self, other: &Self) -> bool {
328        match (&self.repr, &other.repr) {
329            (RenderSlotRepr::Empty, RenderSlotRepr::Empty) => true,
330            (RenderSlotRepr::Handle(lhs), RenderSlotRepr::Handle(rhs)) => lhs == rhs,
331            _ => false,
332        }
333    }
334}
335
336impl Eq for RenderSlot {}
337
338/// Stable, comparable render slot handle for `Fn(T)`.
339///
340/// This follows the same invalidation rules as [`RenderSlot`], while supporting
341/// deferred rendering that depends on an input value.
342pub struct RenderSlotWith<T> {
343    handle: FunctorHandle,
344    marker: PhantomData<fn(T)>,
345}
346
347impl<T> RenderSlotWith<T> {
348    /// Create a render slot from a closure.
349    ///
350    /// This must be called during a component build.
351    #[track_caller]
352    pub fn new<F>(render: F) -> Self
353    where
354        T: 'static,
355        F: Fn(T) + Send + Sync + 'static,
356    {
357        Self {
358            handle: remember_render_slot_with_handle(render),
359            marker: PhantomData,
360        }
361    }
362
363    /// Execute the render closure with an input value.
364    pub fn render(&self, value: T)
365    where
366        T: 'static,
367    {
368        track_render_slot_read_dependency(self.handle);
369        invoke_render_slot_with_handle(self.handle, value)
370    }
371}
372
373impl<T, F> From<F> for RenderSlotWith<T>
374where
375    T: 'static,
376    F: Fn(T) + Send + Sync + 'static,
377{
378    fn from(render: F) -> Self {
379        Self::new(render)
380    }
381}
382
383impl<T> From<CallbackWith<T>> for RenderSlotWith<T>
384where
385    T: 'static,
386{
387    fn from(callback: CallbackWith<T>) -> Self {
388        Self::new(move |value| {
389            callback.call(value);
390        })
391    }
392}
393
394impl<T> Clone for RenderSlotWith<T> {
395    fn clone(&self) -> Self {
396        *self
397    }
398}
399
400impl<T> Copy for RenderSlotWith<T> {}
401
402impl<T> PartialEq for RenderSlotWith<T> {
403    fn eq(&self, other: &Self) -> bool {
404        self.handle == other.handle
405    }
406}
407
408impl<T> Eq for RenderSlotWith<T> {}
409
410/// Internal component props contract used by replay and prop comparison.
411pub trait Prop: Clone + Send + Sync + 'static {
412    /// Compare current props with another props value.
413    fn prop_eq(&self, other: &Self) -> bool;
414}
415
416impl Prop for () {
417    fn prop_eq(&self, _other: &Self) -> bool {
418        true
419    }
420}
421
422/// Type-erased prop value used by the core replay path.
423pub trait ErasedProp: Send + Sync {
424    /// Access the concrete prop value as `Any`.
425    fn as_any(&self) -> &dyn Any;
426    /// Clone this erased prop object.
427    fn clone_box(&self) -> Box<dyn ErasedProp>;
428    /// Compare with another erased prop object.
429    fn equals(&self, other: &dyn ErasedProp) -> bool;
430}
431
432impl<T> ErasedProp for T
433where
434    T: Prop,
435{
436    fn as_any(&self) -> &dyn Any {
437        self
438    }
439
440    fn clone_box(&self) -> Box<dyn ErasedProp> {
441        Box::new(self.clone())
442    }
443
444    fn equals(&self, other: &dyn ErasedProp) -> bool {
445        let Some(other) = other.as_any().downcast_ref::<T>() else {
446            return false;
447        };
448        self.prop_eq(other)
449    }
450}
451
452/// Type-erased component runner used for replay.
453pub trait ErasedComponentRunner: Send + Sync {
454    /// Execute the component with erased props.
455    fn run(&self, props: &dyn ErasedProp);
456}
457
458struct ComponentRunner<P: Prop> {
459    run_fn: fn(&P),
460}
461
462impl<P> ErasedComponentRunner for ComponentRunner<P>
463where
464    P: Prop,
465{
466    fn run(&self, props: &dyn ErasedProp) {
467        let Some(props) = props.as_any().downcast_ref::<P>() else {
468            panic!(
469                "component runner props type mismatch: expected {}",
470                std::any::type_name::<P>()
471            );
472        };
473        (self.run_fn)(props);
474    }
475}
476
477/// Build a type-erased runner from a component function.
478pub fn make_component_runner<P>(run_fn: fn(&P)) -> Arc<dyn ErasedComponentRunner>
479where
480    P: Prop,
481{
482    Arc::new(ComponentRunner { run_fn })
483}
484
485/// Snapshot of a replayable component invocation.
486#[derive(Clone)]
487pub struct ComponentReplayData {
488    /// Type-erased component runner.
489    pub runner: Arc<dyn ErasedComponentRunner>,
490    /// Latest props snapshot.
491    pub props: Arc<dyn ErasedProp>,
492}
493
494impl ComponentReplayData {
495    /// Create replay data from typed props.
496    pub fn new<P>(runner: Arc<dyn ErasedComponentRunner>, props: &P) -> Self
497    where
498        P: Prop,
499    {
500        Self {
501            runner,
502            props: Arc::new(props.clone()),
503        }
504    }
505}