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 identity remains stable across recomposition. Capture [`State`](crate::State)
59/// handles and read their values when the callback runs to observe ongoing
60/// changes. Pass the handle through component props or attach it to an action.
61///
62/// Create callbacks during a Tessera component build.
63#[derive(Clone, Copy)]
64pub struct Callback {
65    repr: CallbackRepr,
66}
67
68#[derive(Clone, Copy, Eq, PartialEq)]
69enum CallbackRepr {
70    Noop,
71    Handle(FunctorHandle),
72}
73
74impl Callback {
75    /// Create a callback handle from a closure.
76    ///
77    /// This must be called during a component build.
78    #[track_caller]
79    pub fn new<F>(handler: F) -> Self
80    where
81        F: Fn() + Send + Sync + 'static,
82    {
83        Self {
84            repr: CallbackRepr::Handle(remember_callback_handle(handler)),
85        }
86    }
87
88    /// Create an empty callback.
89    pub const fn noop() -> Self {
90        Self {
91            repr: CallbackRepr::Noop,
92        }
93    }
94
95    /// Invoke the callback.
96    pub fn call(&self) {
97        match self.repr {
98            CallbackRepr::Noop => {}
99            CallbackRepr::Handle(handle) => invoke_callback_handle(handle),
100        }
101    }
102}
103
104impl<F> From<F> for Callback
105where
106    F: Fn() + Send + Sync + 'static,
107{
108    fn from(handler: F) -> Self {
109        Self::new(handler)
110    }
111}
112
113impl Default for Callback {
114    fn default() -> Self {
115        Self::noop()
116    }
117}
118
119impl PartialEq for Callback {
120    fn eq(&self, other: &Self) -> bool {
121        self.repr == other.repr
122    }
123}
124
125impl Eq for Callback {}
126
127/// Stable, comparable callback handle for `Fn(T) -> R`.
128///
129/// The handle remains stable across recomposition. Capture [`State`](crate::State) handles
130/// and read their values when the callback runs to observe ongoing changes.
131///
132/// This is useful for value-change handlers and similar one-argument callbacks.
133pub struct CallbackWith<T, R = ()> {
134    repr: CallbackWithRepr<T, R>,
135}
136
137enum CallbackWithRepr<T, R> {
138    Handle(FunctorHandle),
139    Static(fn(T) -> R),
140}
141
142impl<T, R> Copy for CallbackWithRepr<T, R> {}
143
144impl<T, R> Clone for CallbackWithRepr<T, R> {
145    fn clone(&self) -> Self {
146        *self
147    }
148}
149
150impl<T, R> CallbackWith<T, R>
151where
152    T: 'static,
153    R: 'static,
154{
155    /// Create a callback handle from a closure.
156    ///
157    /// This must be called during a component build.
158    #[track_caller]
159    pub fn new<F>(handler: F) -> Self
160    where
161        F: Fn(T) -> R + Send + Sync + 'static,
162    {
163        Self {
164            repr: CallbackWithRepr::Handle(remember_callback_with_handle(handler)),
165        }
166    }
167
168    /// Invoke the callback with an argument.
169    pub fn call(&self, value: T) -> R {
170        match self.repr {
171            CallbackWithRepr::Handle(handle) => invoke_callback_with_handle(handle, value),
172            CallbackWithRepr::Static(handler) => handler(value),
173        }
174    }
175
176    fn from_static(handler: fn(T) -> R) -> Self {
177        Self {
178            repr: CallbackWithRepr::Static(handler),
179        }
180    }
181}
182
183impl<T, R, F> From<F> for CallbackWith<T, R>
184where
185    T: 'static,
186    R: 'static,
187    F: Fn(T) -> R + Send + Sync + 'static,
188{
189    fn from(handler: F) -> Self {
190        Self::new(handler)
191    }
192}
193
194impl<T, R> Copy for CallbackWith<T, R> {}
195
196impl<T, R> Clone for CallbackWith<T, R> {
197    fn clone(&self) -> Self {
198        *self
199    }
200}
201
202impl<T, R> PartialEq for CallbackWith<T, R> {
203    fn eq(&self, other: &Self) -> bool {
204        match (self.repr, other.repr) {
205            (CallbackWithRepr::Handle(lhs), CallbackWithRepr::Handle(rhs)) => lhs == rhs,
206            (CallbackWithRepr::Static(lhs), CallbackWithRepr::Static(rhs)) => {
207                ptr::fn_addr_eq(lhs, rhs)
208            }
209            _ => false,
210        }
211    }
212}
213
214impl<T, R> Eq for CallbackWith<T, R> {}
215
216impl<T, R> CallbackWith<T, R>
217where
218    T: 'static,
219    R: Default + 'static,
220{
221    /// Create a callback that ignores its input and returns
222    /// [`Default::default`].
223    pub fn default_value() -> Self {
224        fn default_value_impl<T, R: Default>(_: T) -> R {
225            R::default()
226        }
227
228        Self::from_static(default_value_impl::<T, R>)
229    }
230}
231
232impl<T> CallbackWith<T, T>
233where
234    T: 'static,
235{
236    /// Create a callback that returns its input unchanged.
237    pub fn identity() -> Self {
238        fn identity_impl<T>(value: T) -> T {
239            value
240        }
241
242        Self::from_static(identity_impl::<T>)
243    }
244}
245
246/// Owned UI content expanded by its parent during a Tessera build.
247///
248/// The parent decides whether, when, and where content expands. Captures must
249/// be owned, Send, Sync, and static; capture stable State handles for changing data.
250///
251/// Stored content owns its captures. Move local values into its closure:
252///
253/// ```compile_fail
254/// use tessera_ui::{RenderSlot, tessera};
255///
256/// #[tessera]
257/// fn host(child: Option<RenderSlot>) {}
258///
259/// #[tessera]
260/// fn app() {
261///     let label = String::from("Title");
262///     host().child(|| { let _ = label.len(); });
263/// }
264/// ```
265///
266/// A configuration handle cannot be used in place of deferred child content:
267///
268/// ```compile_fail
269/// use tessera_ui::{RenderSlot, tessera};
270///
271/// #[tessera]
272/// fn host(child: Option<RenderSlot>) {}
273///
274/// #[tessera]
275/// fn leaf() {}
276///
277/// #[tessera]
278/// fn app() {
279///     host().child(leaf());
280/// }
281/// ```
282///
283#[derive(Clone, Copy)]
284pub struct RenderSlot {
285    repr: RenderSlotRepr,
286}
287
288#[derive(Clone, Copy, Eq, PartialEq)]
289enum RenderSlotRepr {
290    Empty,
291    Handle(FunctorHandle),
292}
293
294impl RenderSlot {
295    /// Create a render slot from a closure.
296    ///
297    /// This must be called during a component build.
298    #[track_caller]
299    pub fn new<F, Output>(render: F) -> Self
300    where
301        F: Fn() -> Output + Send + Sync + 'static,
302    {
303        Self {
304            repr: RenderSlotRepr::Handle(remember_render_slot_handle(move || {
305                render();
306            })),
307        }
308    }
309
310    /// Create an empty render slot.
311    pub const fn empty() -> Self {
312        Self {
313            repr: RenderSlotRepr::Empty,
314        }
315    }
316
317    /// Expands the content at the current build position.
318    ///
319    /// Call this only from a component or recognized build closure.
320    pub fn render(&self) {
321        match self.repr {
322            RenderSlotRepr::Empty => {}
323            RenderSlotRepr::Handle(handle) => {
324                render_slot_boundary(handle).props.execute_now();
325            }
326        }
327    }
328}
329
330#[tessera(crate)]
331fn render_slot_boundary(handle: FunctorHandle) {
332    track_render_slot_read_dependency(handle);
333    invoke_render_slot_handle(handle);
334}
335
336impl<F, Output> From<F> for RenderSlot
337where
338    F: Fn() -> Output + Send + Sync + 'static,
339{
340    fn from(render: F) -> Self {
341        Self::new(render)
342    }
343}
344
345impl From<Callback> for RenderSlot {
346    fn from(callback: Callback) -> Self {
347        Self::new(move || callback.call())
348    }
349}
350
351impl Default for RenderSlot {
352    fn default() -> Self {
353        Self::empty()
354    }
355}
356
357impl PartialEq for RenderSlot {
358    fn eq(&self, other: &Self) -> bool {
359        match (&self.repr, &other.repr) {
360            (RenderSlotRepr::Empty, RenderSlotRepr::Empty) => true,
361            (RenderSlotRepr::Handle(lhs), RenderSlotRepr::Handle(rhs)) => lhs == rhs,
362            _ => false,
363        }
364    }
365}
366
367impl Eq for RenderSlot {}
368
369/// Stable, comparable render slot handle for `Fn(T)`.
370///
371/// This follows the same invalidation rules as [`RenderSlot`], while supporting
372/// deferred rendering that depends on an input value.
373pub struct RenderSlotWith<T> {
374    handle: FunctorHandle,
375    marker: PhantomData<fn(T)>,
376}
377
378impl<T> RenderSlotWith<T> {
379    /// Create a render slot from a closure.
380    ///
381    /// This must be called during a component build.
382    #[track_caller]
383    pub fn new<F, Output>(render: F) -> Self
384    where
385        T: 'static,
386        F: Fn(T) -> Output + Send + Sync + 'static,
387    {
388        Self {
389            handle: remember_render_slot_with_handle(move |value| {
390                render(value);
391            }),
392            marker: PhantomData,
393        }
394    }
395
396    /// Expands the content with an input at the current build position.
397    ///
398    /// Call this only from a component or recognized build closure.
399    pub fn render(&self, value: T)
400    where
401        T: Clone + PartialEq + Send + Sync + 'static,
402    {
403        render_slot_with_boundary(self.handle, value)
404            .props
405            .execute_now();
406    }
407}
408
409#[tessera(crate)]
410fn render_slot_with_boundary<T>(handle: FunctorHandle, value: T)
411where
412    T: Clone + PartialEq + Send + Sync + 'static,
413{
414    track_render_slot_read_dependency(handle);
415    invoke_render_slot_with_handle(handle, value)
416}
417
418impl<T, F, Output> From<F> for RenderSlotWith<T>
419where
420    T: 'static,
421    F: Fn(T) -> Output + Send + Sync + 'static,
422{
423    fn from(render: F) -> Self {
424        Self::new(render)
425    }
426}
427
428impl<T> From<CallbackWith<T>> for RenderSlotWith<T>
429where
430    T: 'static,
431{
432    fn from(callback: CallbackWith<T>) -> Self {
433        Self::new(move |value| {
434            callback.call(value);
435        })
436    }
437}
438
439impl<T> Clone for RenderSlotWith<T> {
440    fn clone(&self) -> Self {
441        *self
442    }
443}
444
445impl<T> Copy for RenderSlotWith<T> {}
446
447impl<T> PartialEq for RenderSlotWith<T> {
448    fn eq(&self, other: &Self) -> bool {
449        self.handle == other.handle
450    }
451}
452
453impl<T> Eq for RenderSlotWith<T> {}
454
455/// Internal component props contract used by replay and prop comparison.
456pub trait Prop: Clone + Send + Sync + 'static {
457    /// Compare current props with another props value.
458    fn prop_eq(&self, other: &Self) -> bool;
459    /// Execute the component with these props.
460    fn run_component(self: Arc<Self>);
461}
462
463pub(crate) trait ErasedComponent: Send + Sync {
464    fn as_any(&self) -> &dyn Any;
465    fn as_any_mut(&mut self) -> &mut dyn Any;
466    fn equals(&self, other: &dyn ErasedComponent) -> bool;
467    fn run(self: Arc<Self>);
468}
469
470impl<T> ErasedComponent for T
471where
472    T: Prop,
473{
474    fn as_any(&self) -> &dyn Any {
475        self
476    }
477
478    fn as_any_mut(&mut self) -> &mut dyn Any {
479        self
480    }
481
482    fn equals(&self, other: &dyn ErasedComponent) -> bool {
483        let Some(other) = other.as_any().downcast_ref::<T>() else {
484            return false;
485        };
486        self.prop_eq(other)
487    }
488    fn run(self: Arc<Self>) {
489        self.run_component();
490    }
491}