Skip to main content

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