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