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);