Skip to main content

whisker_runtime/view/
into_view.rs

1//! [`IntoView`] — uniform return type for components.
2//!
3//! A component fn returns `impl IntoView`. The renderer or parent
4//! component calls `.into_view()` to get either an [`Element`]
5//! (for "this is one element") or a `View` (for fragments, tuples,
6//! components nested inside `render!`).
7//!
8//! The trait is intentionally minimal: every implementor produces a
9//! `View`. A `View` is either a single element, a fragment of
10//! children, or a "marker" view (used by `Show`/`For` to mark
11//! reactive boundaries — Phase 6.5a A3 Step 4).
12
13use super::handle::Element;
14
15/// A renderable. Components return `impl IntoView`; the renderer
16/// (called by the macro at mount time, or by the parent's
17/// `render!` expansion) calls `.into_view()` to get the underlying
18/// `View`.
19pub trait IntoView {
20    fn into_view(self) -> View;
21}
22
23/// Type used by `#[component]` for the conventional `children` prop.
24/// The `render!` macro routes a component invocation's non-kwarg
25/// children into a `move || View::Fragment(…)` closure of this type;
26/// the component body invokes it to materialise the children at the
27/// point in the tree where they should appear.
28///
29/// Two design points:
30///
31/// - `Fn` (not `FnOnce`) so the closure can be re-invoked across
32///   hot-reload remounts and similar "re-run the body" paths.
33/// - `Rc` (not `Box`) so `Children` itself implements `Clone`. The
34///   `#[component]` macro re-clones every prop on every body
35///   invocation, so a `Children` prop has to be a cheaply-cloneable
36///   handle — `Rc<dyn Fn>` is one machine word.
37pub type Children = ::std::rc::Rc<dyn ::std::ops::Fn() -> View + 'static>;
38
39/// Mount a `Children` prop at the current position in the tree.
40///
41/// Returns a phantom element (no on-screen footprint — `is_phantom`
42/// is true) with the children's view attached. The caller (typically
43/// the `render!` macro lowering for the `children()` special node)
44/// hands this back to the parent's `.child(...)` chain so the parent
45/// sees a single Element handle, exactly like any other child node.
46///
47/// `&Children` (not `Children`) on purpose — `Rc::clone` would also
48/// work, but a borrow makes the FnMut re-invocation case unambiguous:
49/// nothing here moves out of the surrounding `#[component]` body,
50/// so the hot-reload outer can re-call without `cannot move out of`.
51///
52/// Calling the children closure here is a `Fn::call(&self, ())`,
53/// which only takes `&Rc<…>` — the children Rc itself is left
54/// untouched and can be passed to additional `mount_children` calls
55/// at other positions (multi-projection a la Leptos `ChildrenFn`).
56pub fn mount_children(children: &Children) -> Element {
57    let ph = super::create_phantom_element();
58    let view = children();
59    view.attach_to(ph);
60    ph
61}
62
63// Function-shaped prop types for control-flow components.
64//
65// These newtypes let `#[component]`-annotated control-flow functions
66// (`For`, `Show`, user-defined ones) accept closure literals via
67// `Into` in the typed-builder `Props`. Each wraps `Rc<dyn Fn>` (not
68// `Box`) so the newtype is `Clone` — `#[component]` re-clones every
69// prop on each body invocation, matching what [`Children`] does.
70
71/// `Fn() -> Vec<T>` — the "what items to render" closure for a
72/// keyed-list control flow. Wrapping in a newtype gives typed-builder
73/// a concrete Props field type plus an `Into` path from any matching
74/// closure literal.
75pub struct EachFn<T: 'static>(pub ::std::rc::Rc<dyn ::std::ops::Fn() -> Vec<T> + 'static>);
76
77impl<T: 'static> Clone for EachFn<T> {
78    fn clone(&self) -> Self {
79        EachFn(::std::rc::Rc::clone(&self.0))
80    }
81}
82
83impl<T: 'static, F: Fn() -> Vec<T> + 'static> From<F> for EachFn<T> {
84    fn from(f: F) -> Self {
85        EachFn(::std::rc::Rc::new(f))
86    }
87}
88
89impl<T: 'static> EachFn<T> {
90    /// Invoke the wrapped closure.
91    pub fn call(&self) -> Vec<T> {
92        (self.0)()
93    }
94}
95
96/// `Fn(&T) -> K` — the "key extractor" closure for a keyed-list
97/// control flow. Items whose keys match across reactive reruns
98/// reuse their owners + per-item state.
99pub struct KeyFn<T: 'static, K: 'static>(pub ::std::rc::Rc<dyn ::std::ops::Fn(&T) -> K + 'static>);
100
101impl<T: 'static, K: 'static> Clone for KeyFn<T, K> {
102    fn clone(&self) -> Self {
103        KeyFn(::std::rc::Rc::clone(&self.0))
104    }
105}
106
107impl<T: 'static, K: 'static, F: Fn(&T) -> K + 'static> From<F> for KeyFn<T, K> {
108    fn from(f: F) -> Self {
109        KeyFn(::std::rc::Rc::new(f))
110    }
111}
112
113impl<T: 'static, K: 'static> KeyFn<T, K> {
114    /// Invoke the wrapped closure on `item`.
115    pub fn call(&self, item: &T) -> K {
116        (self.0)(item)
117    }
118}
119
120/// `Fn(T) -> Element` — the "render one item" closure for a
121/// keyed-list control flow. The returned [`Element`] is what gets
122/// attached to the surrounding fragment / list.
123pub struct ItemFn<T: 'static>(pub ::std::rc::Rc<dyn ::std::ops::Fn(T) -> Element + 'static>);
124
125impl<T: 'static> Clone for ItemFn<T> {
126    fn clone(&self) -> Self {
127        ItemFn(::std::rc::Rc::clone(&self.0))
128    }
129}
130
131impl<T: 'static, F: Fn(T) -> Element + 'static> From<F> for ItemFn<T> {
132    fn from(f: F) -> Self {
133        ItemFn(::std::rc::Rc::new(f))
134    }
135}
136
137impl<T: 'static> ItemFn<T> {
138    /// Invoke the wrapped closure on `item`.
139    pub fn call(&self, item: T) -> Element {
140        (self.0)(item)
141    }
142}
143
144/// `Fn() -> bool` — the predicate closure for a `show`-style
145/// conditional control flow. Wrapping in a newtype gives
146/// typed-builder a concrete Props field type plus an `Into` path
147/// from any matching closure literal.
148pub struct WhenFn(pub ::std::rc::Rc<dyn ::std::ops::Fn() -> bool + 'static>);
149
150impl Clone for WhenFn {
151    fn clone(&self) -> Self {
152        WhenFn(::std::rc::Rc::clone(&self.0))
153    }
154}
155
156impl<F: Fn() -> bool + 'static> From<F> for WhenFn {
157    fn from(f: F) -> Self {
158        WhenFn(::std::rc::Rc::new(f))
159    }
160}
161
162impl WhenFn {
163    /// Invoke the wrapped predicate.
164    pub fn call(&self) -> bool {
165        (self.0)()
166    }
167}
168
169/// The fallback branch of a `show`-style conditional. Wraps an
170/// optional `Fn() -> Element` closure — `None` means "render
171/// nothing on false"; `Some(closure)` is what the user typed as
172/// `fallback: || …`.
173///
174/// `From<F: Fn() -> Element + 'static>` lets a closure literal flow
175/// through typed-builder's `Into<Fallback>` path; `Default` (used
176/// via `#[prop(default)]`) is `None`. (`Fallback` uses an
177/// element-returning closure rather than `Children`'s
178/// view-returning shape because the typical fallback is a single
179/// component invocation like `|| render! { status_banner(...) }`,
180/// which evaluates to `Element`. The implementation re-wraps it
181/// into a `View::Element` before attaching.)
182#[derive(Clone, Default)]
183pub struct Fallback(pub Option<::std::rc::Rc<dyn ::std::ops::Fn() -> Element + 'static>>);
184
185impl<F: Fn() -> Element + 'static> From<F> for Fallback {
186    fn from(f: F) -> Self {
187        Fallback(Some(::std::rc::Rc::new(f)))
188    }
189}
190
191/// A rendered (or about-to-be-rendered) tree fragment.
192#[derive(Debug, Clone)]
193pub enum View {
194    /// A single element handle the caller has already created.
195    Element(Element),
196    /// A text snippet — `materialize` creates a `raw_text` element
197    /// with `text=<value>`. The `IntoView` impls for `&str` /
198    /// `String` / primitive numeric types route through here so
199    /// `{count.get()}` inside `render!` can interpolate scalar
200    /// values without the caller having to manually wrap them in a
201    /// `raw_text { … }` element.
202    Text(String),
203    /// Zero-or-more child views in order. Tuples, option-some /
204    /// option-none → empty, iterator flattening, and the macro's
205    /// multi-child `Show` children all use this.
206    Fragment(Vec<View>),
207    /// A view with no on-screen footprint — `Show { when: false }`
208    /// and `Option::None`.
209    Empty,
210}
211
212impl View {
213    /// Realise the view: create whatever element handles the text /
214    /// fragment variants require, append them to `parent`, and return
215    /// the resulting flat list of leaf handles in attach order. The
216    /// returned list is what the `{expr}` macro path stashes so the
217    /// next effect re-run can detach the previous children before
218    /// attaching the new ones.
219    pub fn attach_to(self, parent: Element) -> Vec<Element> {
220        let mut out = Vec::new();
221        self.materialise_into(parent, &mut out);
222        out
223    }
224
225    fn materialise_into(self, parent: Element, out: &mut Vec<Element>) {
226        match self {
227            View::Element(h) => {
228                super::append_child(parent, h);
229                out.push(h);
230            }
231            View::Text(s) => {
232                let h = super::create_element(crate::element::ElementTag::RawText);
233                super::set_attribute(h, "text", &s);
234                super::append_child(parent, h);
235                out.push(h);
236            }
237            View::Fragment(children) => {
238                for child in children {
239                    child.materialise_into(parent, out);
240                }
241            }
242            View::Empty => {}
243        }
244    }
245
246    /// Collect the (already-realised) leaf element handles this view
247    /// contributes, in child-order. **Only `Element` and `Fragment`
248    /// contribute.** `Text` returns nothing here because its element
249    /// only exists once `attach_to` has run.
250    pub fn elements(&self) -> Vec<Element> {
251        let mut out = Vec::new();
252        self.collect_into(&mut out);
253        out
254    }
255
256    fn collect_into(&self, out: &mut Vec<Element>) {
257        match self {
258            View::Element(h) => out.push(*h),
259            View::Fragment(children) => {
260                for c in children {
261                    c.collect_into(out);
262                }
263            }
264            View::Text(_) | View::Empty => {}
265        }
266    }
267}
268
269impl IntoView for View {
270    fn into_view(self) -> View {
271        self
272    }
273}
274
275impl IntoView for Element {
276    fn into_view(self) -> View {
277        View::Element(self)
278    }
279}
280
281impl IntoView for () {
282    fn into_view(self) -> View {
283        View::Empty
284    }
285}
286
287impl<T: IntoView> IntoView for Option<T> {
288    fn into_view(self) -> View {
289        match self {
290            Some(v) => v.into_view(),
291            None => View::Empty,
292        }
293    }
294}
295
296// Text-shaped `IntoView` impls.
297//
298// Inside `render!`, any `{expr}` that evaluates to a string- or
299// number-shaped value is routed through one of these into a
300// `View::Text`, which becomes a `raw_text` element when the surrounding
301// effect's `attach_to` runs. This is what lets the user write
302// `text { {count.get()} }` and `text { {label} }` interchangeably.
303//
304// We intentionally avoid a blanket `impl<T: Display>` to keep the
305// surface predictable and the orphan rules tractable — primitives
306// list explicitly, custom types implement `IntoView` themselves.
307
308impl IntoView for String {
309    fn into_view(self) -> View {
310        View::Text(self)
311    }
312}
313
314impl IntoView for &str {
315    fn into_view(self) -> View {
316        View::Text(self.to_owned())
317    }
318}
319
320impl IntoView for &String {
321    fn into_view(self) -> View {
322        View::Text(self.clone())
323    }
324}
325
326macro_rules! impl_into_view_via_display {
327    ($($t:ty),+) => {
328        $(
329            impl IntoView for $t {
330                fn into_view(self) -> View {
331                    View::Text(self.to_string())
332                }
333            }
334        )+
335    };
336}
337
338impl_into_view_via_display!(i8, i16, i32, i64, i128, isize);
339impl_into_view_via_display!(u8, u16, u32, u64, u128, usize);
340impl_into_view_via_display!(f32, f64, bool, char);
341
342// Tuple impls for 1–8 elements. Tuples render as fragments —
343// children mount in declaration order.
344macro_rules! impl_into_view_tuple {
345    ($($name:ident),+) => {
346        impl<$($name: IntoView),+> IntoView for ($($name,)+) {
347            #[allow(non_snake_case)]
348            fn into_view(self) -> View {
349                let ($($name,)+) = self;
350                View::Fragment(vec![$($name.into_view()),+])
351            }
352        }
353    };
354}
355
356impl_into_view_tuple!(A);
357impl_into_view_tuple!(A, B);
358impl_into_view_tuple!(A, B, C);
359impl_into_view_tuple!(A, B, C, D);
360impl_into_view_tuple!(A, B, C, D, E);
361impl_into_view_tuple!(A, B, C, D, E, F);
362impl_into_view_tuple!(A, B, C, D, E, F, G);
363impl_into_view_tuple!(A, B, C, D, E, F, G, H);