Skip to main content

day_core/
build.rs

1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! The build layer (DESIGN.md §5.1–§5.2): pieces are descriptions consumed exactly once.
5//! `BuildCx` holds no tree borrow; every operation goes through `with_tree`, so bindings
6//! and structural effects created during build can re-enter safely.
7
8use std::any::Any;
9use std::rc::Rc;
10
11use day_reactive::Scope;
12use day_spec::{Event, PieceKind};
13
14use crate::layout::Layout;
15use crate::tree::{Flex, RNode, with_tree};
16
17/// Measure-invalidation boundary (§7.4): `Yes` stops upward needs-measure propagation
18/// (scroll viewports, nav pages). Named enum instead of a bare bool at call sites
19/// (docs/api-style.md).
20#[derive(Clone, Copy, PartialEq, Eq, Debug)]
21pub enum Boundary {
22    No,
23    Yes,
24}
25
26pub struct BuildCx {
27    parent: RNode,
28}
29
30impl BuildCx {
31    pub fn new(parent: RNode) -> Self {
32        BuildCx { parent }
33    }
34
35    pub fn parent(&self) -> RNode {
36        self.parent
37    }
38
39    /// Create + attach a native leaf.
40    pub fn leaf(&mut self, kind: PieceKind, props: &dyn Any, flex: Flex) -> RNode {
41        let n = with_tree(|t| {
42            t.create_node(
43                kind,
44                props,
45                Rc::new(crate::layout::LeafLayout),
46                flex,
47                true,
48                false,
49                Scope::current(),
50            )
51        });
52        with_tree(|t| t.attach(self.parent, n));
53        n
54    }
55
56    /// Create + attach a native node with a custom layout (containers, scroll).
57    pub fn native(
58        &mut self,
59        kind: PieceKind,
60        props: &dyn Any,
61        layout: Rc<dyn Layout>,
62        flex: Flex,
63        boundary: Boundary,
64    ) -> RNode {
65        let n = with_tree(|t| {
66            t.create_node(
67                kind,
68                props,
69                layout,
70                flex,
71                true,
72                boundary == Boundary::Yes,
73                Scope::current(),
74            )
75        });
76        with_tree(|t| t.attach(self.parent, n));
77        n
78    }
79
80    /// Create + attach a layout-only node (wrappers, groups, spacer).
81    pub fn layout_only(&mut self, layout: Rc<dyn Layout>, flex: Flex, boundary: Boundary) -> RNode {
82        let n = with_tree(|t| {
83            t.create_node(
84                "day.layout",
85                &(),
86                layout,
87                flex,
88                false,
89                boundary == Boundary::Yes,
90                Scope::current(),
91            )
92        });
93        with_tree(|t| t.attach(self.parent, n));
94        n
95    }
96
97    /// Build `f` with `node` as the parent.
98    pub fn under<R>(&mut self, node: RNode, f: impl FnOnce(&mut BuildCx) -> R) -> R {
99        let mut cx = BuildCx { parent: node };
100        f(&mut cx)
101    }
102
103    /// Register a native-event handler for a node (runs under the registration scope, §4.3).
104    pub fn on(&mut self, node: RNode, h: impl Fn(&Event) + 'static) {
105        let scope = Scope::current();
106        let wrapped: Rc<dyn Fn(&Event)> = Rc::new(move |ev| {
107            if scope.is_alive() {
108                let ev = ev.clone();
109                scope.enter(|| h(&ev));
110            }
111        });
112        with_tree(|t| t.on_event(node, wrapped));
113    }
114}
115
116// ---------------------------------------------------------------------------
117// Piece
118// ---------------------------------------------------------------------------
119
120/// A UI description consumed once (§5.2). Returns the root realized node it created.
121pub trait Piece: 'static {
122    fn build(self, cx: &mut BuildCx) -> RNode;
123}
124
125/// Type-erased piece for heterogeneous branches and dynamic construction.
126pub struct AnyPiece(Box<dyn FnOnce(&mut BuildCx) -> RNode>);
127
128impl AnyPiece {
129    pub fn new<P: Piece>(p: P) -> Self {
130        AnyPiece(Box::new(move |cx| p.build(cx)))
131    }
132
133    /// Already erased: hands itself back instead of boxing a box.
134    ///
135    /// Inherent so it shadows the blanket `Decorate::any` (inherent methods win method
136    /// resolution). Every `Decorate` modifier returns an erased piece, so `.padding(8.0).any()`
137    /// and friends are common in app code and would otherwise pay a second allocation and a
138    /// second indirect call for nothing.
139    pub fn any(self) -> AnyPiece {
140        self
141    }
142}
143
144impl Piece for AnyPiece {
145    fn build(self, cx: &mut BuildCx) -> RNode {
146        (self.0)(cx)
147    }
148}
149
150/// A closure kept as a concrete piece: the deferral primitive (§5.2).
151///
152/// A piece that must read something only available at build time (an ambient `environment`, a
153/// scope, the laid-out size) defers its body into a closure. Deferring is not erasing: this
154/// newtype carries the closure inline, so the constructors built on [`piece_fn`] can return
155/// `impl Piece` and keep the caller's type concrete. Erase with `.any()` where a
156/// heterogeneous collection or a stored callback needs it.
157pub struct PieceFn<F>(F);
158
159impl<F: FnOnce(&mut BuildCx) -> RNode + 'static> Piece for PieceFn<F> {
160    fn build(self, cx: &mut BuildCx) -> RNode {
161        (self.0)(cx)
162    }
163}
164
165/// A piece from a closure, built when the tree reaches it.
166pub fn piece_fn<F: FnOnce(&mut BuildCx) -> RNode + 'static>(f: F) -> PieceFn<F> {
167    PieceFn(f)
168}
169
170/// A build-time branch between two piece TYPES, without erasing either (§5.1).
171///
172/// `if compact { a.any() } else { b.any() }` boxes whichever arm it takes; `Either` keeps both
173/// concrete, so the branch costs nothing and the chosen arm's type survives into the parent:
174///
175/// ```ignore
176/// if compact { Either::Left(row(children)) } else { Either::Right(column(children)) }
177/// ```
178///
179/// For a branch on a signal, one that must re-evaluate, use `when(…).otherwise(…)` instead.
180/// This is a plain `if` resolved once at build.
181pub enum Either<A, B> {
182    Left(A),
183    Right(B),
184}
185
186impl<A: Piece, B: Piece> Piece for Either<A, B> {
187    fn build(self, cx: &mut BuildCx) -> RNode {
188        match self {
189            Either::Left(a) => a.build(cx),
190            Either::Right(b) => b.build(cx),
191        }
192    }
193}
194
195/// An optional piece (§5.1): `Some` builds the piece, `None` builds the same zero-size,
196/// layout-transparent anchor a false [`when`](/api/day_pieces/fn.when.html) leaves behind, so
197/// the slot keeps its place in a tuple without drawing anything.
198///
199/// ```ignore
200/// let precip = (d.precip > 0).then(|| label(format!("{}%", d.precip)));
201/// row((icon, precip))
202/// ```
203///
204/// Resolved once at build. For an `Option` that changes while the piece lives, use
205/// `keyed(move || state.get(), |v| …)`.
206impl<P: Piece> Piece for Option<P> {
207    fn build(self, cx: &mut BuildCx) -> RNode {
208        match self {
209            Some(p) => p.build(cx),
210            None => cx.layout_only(
211                Rc::new(crate::layout::PassThrough),
212                Flex {
213                    is_group: true,
214                    ..Default::default()
215                },
216                Boundary::No,
217            ),
218        }
219    }
220}
221
222#[cfg(test)]
223mod any_piece_tests {
224    use super::*;
225    use slotmap::Key as _;
226
227    fn box_addr(p: &AnyPiece) -> *const () {
228        (&*p.0 as *const dyn FnOnce(&mut BuildCx) -> RNode).cast::<()>()
229    }
230
231    /// `.any()` on an already-erased piece hands the same allocation back. Guards the inherent
232    /// method against being deleted as "redundant with `Decorate::any`": it is what stops a
233    /// second box, and method resolution silently falls back to the blanket trait without it.
234    #[test]
235    fn any_on_an_erased_piece_reuses_its_box() {
236        let p = AnyPiece::new(piece_fn(|_| RNode::null()));
237        let before = box_addr(&p);
238        let p = p.any();
239        assert_eq!(before, box_addr(&p));
240    }
241}
242
243// ---------------------------------------------------------------------------
244// PieceSeq: tuple children (§5.1), flattening recursively.
245// ---------------------------------------------------------------------------
246
247/// Children of a container (§5.1): a tuple of pieces (the floem `ViewTuple` pattern), a
248/// `Vec` or array of one piece type, or an `Option` of a piece. Implemented for exactly those
249/// shapes, never via a blanket, to stay coherent. All of them are build-time children:
250/// evaluated once when the parent builds, with no keyed identity or reconciliation. A child
251/// that must come and go at runtime is a [`when`](/api/day_pieces/fn.when.html), a
252/// `keyed`, or an `each`.
253pub trait PieceSeq: 'static {
254    fn build_each(self, cx: &mut BuildCx);
255}
256
257impl PieceSeq for () {
258    fn build_each(self, _cx: &mut BuildCx) {}
259}
260
261/// A homogeneous list of children, which is also how a runtime-heterogeneous list reads:
262/// `row(items.iter().map(cell).collect::<Vec<_>>())`, or a `Vec<AnyPiece>` when the children
263/// differ in type.
264impl<P: Piece> PieceSeq for Vec<P> {
265    fn build_each(self, cx: &mut BuildCx) {
266        for p in self {
267            let _ = p.build(cx);
268        }
269    }
270}
271
272impl<P: Piece, const N: usize> PieceSeq for [P; N] {
273    fn build_each(self, cx: &mut BuildCx) {
274        for p in self {
275            let _ = p.build(cx);
276        }
277    }
278}
279
280/// A child that may be absent: `None` builds nothing at all (no node).
281impl<P: Piece> PieceSeq for Option<P> {
282    fn build_each(self, cx: &mut BuildCx) {
283        if let Some(p) = self {
284            let _ = p.build(cx);
285        }
286    }
287}
288
289macro_rules! impl_piece_seq {
290    ($($name:ident),+) => {
291        impl<$($name: Piece),+> PieceSeq for ($($name,)+) {
292            #[allow(non_snake_case)]
293            fn build_each(self, cx: &mut BuildCx) {
294                let ($($name,)+) = self;
295                $(let _ = $name.build(cx);)+
296            }
297        }
298    };
299}
300
301impl_piece_seq!(A);
302impl_piece_seq!(A, B);
303impl_piece_seq!(A, B, C);
304impl_piece_seq!(A, B, C, D);
305impl_piece_seq!(A, B, C, D, E);
306impl_piece_seq!(A, B, C, D, E, F);
307impl_piece_seq!(A, B, C, D, E, F, G);
308impl_piece_seq!(A, B, C, D, E, F, G, H);
309impl_piece_seq!(A, B, C, D, E, F, G, H, I);
310impl_piece_seq!(A, B, C, D, E, F, G, H, I, J);
311impl_piece_seq!(A, B, C, D, E, F, G, H, I, J, K);
312impl_piece_seq!(A, B, C, D, E, F, G, H, I, J, K, L);
313impl_piece_seq!(A, B, C, D, E, F, G, H, I, J, K, L, M);
314impl_piece_seq!(A, B, C, D, E, F, G, H, I, J, K, L, M, N);
315impl_piece_seq!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O);
316impl_piece_seq!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, Q);