Skip to main content

day_pieces/
decorators.rs

1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! `Decorate`, the chainable modifiers every piece inherits: padding and sizing, background and
5//! corner radius, gestures (`on_tap`, drag), accessibility (`A11yBuilder`), and native-handle
6//! capture (`NativeRef`), plus the `Modifier` / `IntoInsets` supporting traits.
7//!
8//! Modifiers return [`Decorated<P>`], which keeps the decorated piece's type, so a chain
9//! never stops reaching that piece's builder methods (§5.2).
10
11use std::cell::Cell;
12use std::rc::Rc;
13
14use day_core::*;
15use day_reactive::{Scope, bind};
16use day_spec::props::*;
17use day_spec::{A11yProps, AnimSpec, Color, Cursor, Event, Insets, Role, Transform, kinds};
18
19use crate::menus::lower_menu_scoped;
20use crate::*;
21
22// ---------------------------------------------------------------------------
23// Decorators (§5.2 Decorate)
24// ---------------------------------------------------------------------------
25
26pub trait IntoInsets {
27    fn into_insets(self) -> Insets;
28}
29impl IntoInsets for f64 {
30    fn into_insets(self) -> Insets {
31        Insets::all(self)
32    }
33}
34impl IntoInsets for Insets {
35    fn into_insets(self) -> Insets {
36        self
37    }
38}
39
40/// A one-shot, by-value view transform (the SwiftUI `ViewModifier` analog): wrap a piece into a
41/// new one. Pure composition, with no per-backend work. A plain `FnOnce(AnyPiece) -> AnyPiece`
42/// closure is a `Modifier` too (the blanket impl below), so the common case needs no new type.
43/// Apply one with [`Decorate::modifier`].
44///
45/// **`AnyPiece` here is a trade, not a requirement.** It is not object safety: a `Modifier` is
46/// never stored as `dyn Modifier`, and [`Decorate::modifier`] takes `impl Modifier` and applies
47/// it on the spot. It is the closure impl below. A closure has one fixed parameter type, and
48/// Rust has no `for<P> FnOnce(P) -> _` bound (higher-ranked bounds range over lifetimes, not
49/// types), so making `apply` generic over the content (`fn apply<P: Piece>(self, c: P) ->
50/// Self::Out<P>`, which a named modifier can satisfy with a GAT) would leave no closure able to
51/// implement the trait at all. Pinning the input to the one piece type that accepts anything is
52/// what keeps `|p| …` a modifier, and the erasure is what that costs, which is why this is the
53/// single `Decorate` method that erases.
54///
55/// If a modifier ever needs to preserve its content's type, parameterize the trait rather than
56/// the method: `trait Modifier<P: Piece> { type Out: Piece; fn apply(self, c: P) -> Self::Out; }`.
57/// Closures survive that (`impl<P: Piece, O: Piece, F: FnOnce(P) -> O> Modifier<P> for F`, and
58/// their parameter still infers unannotated), while a named modifier such as day-piece-rating's
59/// `Card` becomes `impl<P: Piece> Modifier<P> for Card` with `type Out = Decorated<P>`. The bill
60/// is a `Decorate::modifier` whose return type varies per modifier, a generic impl for every
61/// named one, and a breaking change: not worth paying while the tree has two implementors.
62pub trait Modifier {
63    fn apply(self, content: AnyPiece) -> AnyPiece;
64}
65
66impl<F> Modifier for F
67where
68    F: FnOnce(AnyPiece) -> AnyPiece,
69{
70    fn apply(self, content: AnyPiece) -> AnyPiece {
71        self(content)
72    }
73}
74
75/// A liveness-checked reference to a mounted piece's realized node: the retained half of the
76/// tweaks API (docs/tweaks.md). Capture one with [`Decorate::native_ref`], then reach the native
77/// widget later (from event handlers, timers) through a toolkit ext accessor. `node`/`with` yield
78/// `None` before mount and after the node's subtree is disposed, so async races are safe no-ops.
79///
80/// Reads are reactive: inside a binding or memo, `node()` subscribes to the ref's mount/clear
81/// transitions (a `Trigger` underneath), so a label like
82/// `label(move || if r.node().is_some() { "live" } else { "cleared" })` updates when the
83/// referenced piece unmounts, as in the toggle demo on the showcase Tweaks page. (The
84/// `when`-arm's disposal lands at the turn boundary, after ordinary bindings re-ran;
85/// piggybacking on some other signal would read a stale mount state, while the trigger fires
86/// at the actual transition.) Main-thread only, like every realized-tree type.
87#[derive(Clone)]
88pub struct NativeRef {
89    cell: Rc<std::cell::Cell<Option<day_core::RNode>>>,
90    changed: day_reactive::Trigger,
91}
92
93impl Default for NativeRef {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99impl NativeRef {
100    pub fn new() -> Self {
101        NativeRef {
102            cell: Rc::new(std::cell::Cell::new(None)),
103            changed: day_reactive::Trigger::new(),
104        }
105    }
106
107    /// The mounted node, if it is currently live. A tracked read (see the type docs).
108    pub fn node(&self) -> Option<day_core::RNode> {
109        self.changed.track();
110        let node = self.cell.get()?;
111        // Generational slotmap keys make a disposed node a clean miss, never a stale hit.
112        let live = day_core::try_with_tree(|t| t.node_kind(node).is_some()).unwrap_or(false);
113        live.then_some(node)
114    }
115
116    /// Run `f` with the live node (e.g. inside `day_toolkit_appkit::with_native`); `None` if disposed.
117    pub fn with<R>(&self, f: impl FnOnce(day_core::RNode) -> R) -> Option<R> {
118        self.node().map(f)
119    }
120
121    fn transition(&self, node: Option<day_core::RNode>) {
122        self.cell.set(node);
123        self.changed.notify();
124    }
125}
126
127/// A transparent native layer node (`CONTAINER`, no fill/clip/corner) used by the animatable
128/// modifiers (`.opacity`/`.transform`) to carry a per-node opacity or transform. Layout-transparent (`FillThrough`), so it never affects sizing and a
129/// granted stretch flows through to what it paints.
130fn layer_node(cx: &mut BuildCx) -> RNode {
131    cx.native(
132        kinds::CONTAINER,
133        &ContainerProps {
134            background: None,
135            corner_radius: 0.0,
136            clips: false,
137            role: None,
138        },
139        Rc::new(FillThrough),
140        Flex::default(),
141        Boundary::No,
142    )
143}
144
145// ---------------------------------------------------------------------------
146// Decorated: a piece plus its modifiers, with the piece's type kept (§5.2)
147// ---------------------------------------------------------------------------
148
149/// The build of a piece plus every modifier applied to it so far.
150type Build = Box<dyn FnOnce(&mut BuildCx) -> RNode>;
151
152/// A piece with modifiers chained onto it, keeping the decorated piece's type (§5.2).
153///
154/// Every [`Decorate`] modifier returns one of these rather than erasing to [`AnyPiece`], which is
155/// what lets a chain keep reaching the piece's builder methods: `label(…).padding(8.0)` is
156/// still a decorated `Label`, so `.font(…)` after it resolves. Modifiers applied to a `Decorated`
157/// append in place (the inherent methods below shadow the trait's), so a chain stays flat instead
158/// of nesting `Decorated<Decorated<…>>`.
159///
160/// Modifiers fall into three classes (docs/api-style.md "Modifier classes"), and only one of
161/// them is ordered:
162///
163/// - **Identity** (`id`, `id_of`) names the piece's own node, the one its build
164///   returns, wherever it sits in the chain: `label(t).padding(8.0).id(day_core::id!(x))` and
165///   `label(t).id(day_core::id!(x)).padding(8.0)` are the same tree.
166/// - **Wrapping** modifiers (`padding`, `frame`, `background`, `grow`, …) each add a node around
167///   what came before, so they compose outward in call order, as they read.
168/// - **Outer-layout facts** (`grid_span`, `grid_align`) describe the piece to the container it
169///   sits in, so they land on the outermost node whatever wrappers follow them.
170///
171/// `wrapper_id` is the one way to name a wrapper deliberately: it tags whatever node is
172/// outermost at the point it is called, for a frame assertion on a padded box.
173///
174/// Erase explicitly with `.any()` when a single [`AnyPiece`] is what's needed (a `Vec<AnyPiece>`, a
175/// `-> AnyPiece` signature).
176pub struct Decorated<P> {
177    inner: P,
178    /// Wrapping modifiers, applied outward in call order.
179    ops: Vec<Box<dyn FnOnce(Build) -> Build>>,
180    /// Identity modifiers, applied to the node the piece's own build returns.
181    node_ops: Vec<Box<dyn FnOnce(RNode)>>,
182    /// Outer-layout facts, applied to the outermost node once every wrapper is in place.
183    outer_ops: Vec<Box<dyn FnOnce(RNode)>>,
184}
185
186impl<P: Piece> Decorated<P> {
187    /// An undecorated piece, ready to have modifiers applied CONDITIONALLY. Starting here gives
188    /// every branch the same type, so an optional modifier needs no erasure:
189    ///
190    /// ```ignore
191    /// let leaf = Decorated::new(draw);
192    /// let leaf = match id { Some(id) => leaf.id(id), None => leaf };
193    /// let leaf = if editable { leaf.on_tap(f) } else { leaf };
194    /// ```
195    ///
196    /// [`apply_if`](Self::apply_if) and [`apply_some`](Self::apply_some) are the same move as a
197    /// chain step.
198    pub fn new(inner: P) -> Self {
199        Decorated {
200            inner,
201            ops: Vec::new(),
202            node_ops: Vec::new(),
203            outer_ops: Vec::new(),
204        }
205    }
206
207    fn push(mut self, op: impl FnOnce(Build) -> Build + 'static) -> Self {
208        self.ops.push(Box::new(op));
209        self
210    }
211
212    fn push_node(mut self, op: impl FnOnce(RNode) + 'static) -> Self {
213        self.node_ops.push(Box::new(op));
214        self
215    }
216
217    fn push_outer(mut self, op: impl FnOnce(RNode) + 'static) -> Self {
218        self.outer_ops.push(Box::new(op));
219        self
220    }
221
222    /// Replace the undecorated piece, keeping the modifier chain: how a typed builder trait
223    /// reaches through a decoration (docs/api-style.md "Typed builders"). `f` sees the piece as
224    /// it was before any modifier, which is why modifier order stops mattering.
225    pub fn map_inner<Q: Piece>(self, f: impl FnOnce(P) -> Q) -> Decorated<Q> {
226        Decorated {
227            inner: f(self.inner),
228            ops: self.ops,
229            node_ops: self.node_ops,
230            outer_ops: self.outer_ops,
231        }
232    }
233
234    /// Apply `f` to the chain when `cond` holds, else continue unchanged: the conditional
235    /// modifier, with no `let x = if c { x.m() } else { x }` rebinding and no erasure.
236    ///
237    /// ```ignore
238    /// nav(sel).apply_if(!primary, |n| n.local())
239    /// ```
240    ///
241    /// `cond` is a plain `bool` resolved at build, like the arms of a `match`; the modifiers
242    /// `f` applies may themselves be reactive.
243    pub fn apply_if(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
244        if cond { f(self) } else { self }
245    }
246
247    /// Apply `f` with the value when there is one, else continue unchanged:
248    /// `canvas(draw).apply_some(id, |c, id| c.id(id))`.
249    pub fn apply_some<T>(self, value: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
250        match value {
251            Some(v) => f(self, v),
252            None => self,
253        }
254    }
255}
256
257impl<P: Piece> Piece for Decorated<P> {
258    fn build(self, cx: &mut BuildCx) -> RNode {
259        let Decorated {
260            inner,
261            ops,
262            node_ops,
263            outer_ops,
264        } = self;
265        // The piece's own node first, with its identity; then the wrappers, composed outward
266        // in call order (the first modifier is innermost); then the facts for the parent, on
267        // whatever ended up outermost.
268        let wraps = !ops.is_empty();
269        let own = Rc::new(Cell::new(None::<RNode>));
270        let own_out = own.clone();
271        let mut build: Build = Box::new(move |cx| {
272            let n = inner.build(cx);
273            for op in node_ops {
274                op(n);
275            }
276            own_out.set(Some(n));
277            n
278        });
279        for op in ops {
280            build = op(build);
281        }
282        let outer = build(cx);
283        // The span a gesture addressed to the piece's own node (where `.id` sits) climbs to
284        // reach a wrapper's `.on_tap` (day-core's `gesture_target`).
285        if wraps && let Some(n) = own.get() {
286            with_tree(|t| t.set_element_outer(n, outer));
287        }
288        for op in outer_ops {
289            op(outer);
290        }
291        outer
292    }
293}
294
295// --- The modifier bodies, written once and shared by the trait and the inherent impl ---
296
297fn op_id(id: day_core::ElementId) -> impl FnOnce(RNode) {
298    move |n| with_tree(|t| t.set_id(n, id.as_str().to_owned()))
299}
300
301fn op_id_of(id: impl Fn() -> day_core::ElementId + 'static) -> impl FnOnce(RNode) {
302    move |n| {
303        day_reactive::bind(id, move |s: &day_core::ElementId| {
304            let s = s.as_str().to_owned();
305            with_tree(|t| t.set_id(n, s));
306        });
307    }
308}
309
310/// Name whatever node is outermost at this point in the chain (see [`Decorated::wrapper_id`]).
311fn op_wrapper_id(id: day_core::ElementId) -> impl FnOnce(Build) -> Build {
312    move |inner| {
313        Box::new(move |cx| {
314            let n = inner(cx);
315            with_tree(|t| t.set_id(n, id.as_str().to_owned()));
316            n
317        })
318    }
319}
320
321fn op_tweak(f: impl FnOnce(day_core::RNode) + 'static) -> impl FnOnce(Build) -> Build {
322    move |inner| {
323        Box::new(move |cx| {
324            let n = inner(cx);
325            f(n);
326            // Mark the node so a later backing swap (`.selectable()` on a toolkit that
327            // rebuilds the widget) warns about the discarded tweak instead of losing it
328            // silently (docs/tweaks.md).
329            with_tree(|t| t.note_node_tweaked(n));
330            n
331        })
332    }
333}
334
335fn op_selectable() -> impl FnOnce(Build) -> Build {
336    move |inner| {
337        Box::new(move |cx| {
338            let n = inner(cx);
339            with_tree(|t| t.set_node_selectable(n, true));
340            n
341        })
342    }
343}
344
345fn op_cursor(cursor: Reactive<Cursor>) -> impl FnOnce(Build) -> Build {
346    move |inner| {
347        Box::new(move |cx| {
348            let n = inner(cx);
349            with_tree(|t| t.set_node_cursor(n, cursor.get_untracked()));
350            // Only a reactive source needs a binding; a constant shape is applied once at
351            // mount. The setter is idempotent, so a re-run costs one native call.
352            if let Reactive::Dyn(_) = &cursor {
353                bind(
354                    move || cursor.get(),
355                    move |c: &Cursor| {
356                        with_tree(|t| t.set_node_cursor(n, c.clone()));
357                    },
358                );
359            }
360            n
361        })
362    }
363}
364
365fn op_window_drag_region() -> impl FnOnce(Build) -> Build {
366    move |inner| {
367        Box::new(move |cx| {
368            let n = inner(cx);
369            with_tree(|t| t.set_node_drag_region(n, true));
370            n
371        })
372    }
373}
374
375fn op_native_ref(r: NativeRef) -> impl FnOnce(Build) -> Build {
376    move |inner| {
377        Box::new(move |cx| {
378            let n = inner(cx);
379            r.transition(Some(n));
380            let cleared = r.clone();
381            Scope::current().on_cleanup(move || cleared.transition(None));
382            n
383        })
384    }
385}
386
387fn op_padding(insets: Insets) -> impl FnOnce(Build) -> Build {
388    move |inner| {
389        Box::new(move |cx| {
390            let w = cx.layout_only(
391                Rc::new(PaddingLayout { insets }),
392                Flex::default(),
393                Boundary::No,
394            );
395            cx.under(w, |cx| {
396                let _ = inner(cx);
397            });
398            w
399        })
400    }
401}
402
403fn op_max_width(max: f64) -> impl FnOnce(Build) -> Build {
404    move |inner| {
405        Box::new(move |cx| {
406            let w = cx.layout_only(
407                Rc::new(MaxWidthLayout { max }),
408                Flex::default(),
409                Boundary::No,
410            );
411            cx.under(w, |cx| {
412                let _ = inner(cx);
413            });
414            w
415        })
416    }
417}
418
419fn op_max_height(max: f64) -> impl FnOnce(Build) -> Build {
420    move |inner| {
421        Box::new(move |cx| {
422            // Yielding, so a column sizes its other children first and gives this one what is
423            // left, up to `max`: a capped scroll gives way to the button under it on a short
424            // screen instead of pushing it out of the column, and the column still hugs.
425            let h = cx.layout_only(
426                Rc::new(day_core::MaxHeightLayout { max }),
427                Flex {
428                    yields_h: true,
429                    ..Flex::default()
430                },
431                Boundary::No,
432            );
433            cx.under(h, |cx| {
434                let _ = inner(cx);
435            });
436            h
437        })
438    }
439}
440
441fn op_min_width(min: f64) -> impl FnOnce(Build) -> Build {
442    move |inner| {
443        Box::new(move |cx| {
444            let w = cx.layout_only(
445                Rc::new(day_core::MinWidthLayout { min }),
446                Flex::default(),
447                Boundary::No,
448            );
449            cx.under(w, |cx| {
450                let _ = inner(cx);
451            });
452            w
453        })
454    }
455}
456
457fn op_reserving(sample: String) -> impl FnOnce(Build) -> Build {
458    move |inner| {
459        Box::new(move |cx| {
460            let w = cx.layout_only(
461                Rc::new(day_core::ReserveLayout),
462                Flex::default(),
463                Boundary::No,
464            );
465            cx.under(w, |cx| {
466                // children[0]: the measured-but-invisible sample.
467                let _ = crate::label(sample.clone()).opacity(0.0).build(cx);
468                // children[1]: the real content.
469                let _ = inner(cx);
470            });
471            w
472        })
473    }
474}
475
476/// `frame`/`width`/`height`: a fixed size on one or both axes. Two fixed axes make a layout
477/// boundary (§7.4); one does not.
478fn op_frame(
479    width: Option<f64>,
480    height: Option<f64>,
481    boundary: Boundary,
482) -> impl FnOnce(Build) -> Build {
483    move |inner| {
484        Box::new(move |cx| {
485            let w = cx.layout_only(
486                Rc::new(FrameLayout { width, height }),
487                Flex::default(),
488                boundary,
489            );
490            cx.under(w, |cx| {
491                let _ = inner(cx);
492            });
493            w
494        })
495    }
496}
497
498fn op_a11y(f: impl FnOnce(A11yBuilder) -> A11yBuilder + 'static) -> impl FnOnce(Build) -> Build {
499    move |inner| {
500        Box::new(move |cx| {
501            let n = inner(cx);
502            let (props, live, selected) = f(A11yBuilder::default()).split();
503            let seed = props.selected;
504            with_tree(|t| t.set_a11y(n, props));
505            // The selected state follows its signal the same way: re-sent alone, merged by
506            // day-core onto the node's set.
507            if let Some(selected) = selected {
508                day_reactive::bind_seeded(
509                    seed.unwrap_or(false),
510                    move || selected.get(),
511                    move |on: &bool| {
512                        with_tree(|t| {
513                            t.set_a11y(
514                                n,
515                                A11yProps {
516                                    selected: Some(*on),
517                                    ..Default::default()
518                                },
519                            )
520                        })
521                    },
522                );
523            }
524            // A string that reads a signal or the locale re-sends itself alone; day-core
525            // merges it onto the node's set and re-applies the whole picture (§13).
526            for (src, patch) in live {
527                if let TextSource::Dyn(read) = src {
528                    let seed = day_reactive::untrack(|| read());
529                    day_reactive::bind_seeded(
530                        seed,
531                        move || read(),
532                        move |s: &String| with_tree(|t| t.set_a11y(n, patch(s.clone()))),
533                    );
534                }
535            }
536            n
537        })
538    }
539}
540
541fn op_on_tap(f: impl Fn() + 'static) -> impl FnOnce(Build) -> Build {
542    move |inner| {
543        Box::new(move |cx| {
544            let n = inner(cx);
545            with_tree(|t| t.enable_gesture(n, GestureKind::Tap));
546            cx.on(n, move |ev| {
547                if matches!(ev, Event::Tap(_)) {
548                    f();
549                }
550            });
551            n
552        })
553    }
554}
555
556fn op_on_tap_at(f: impl Fn(day_spec::Point) + 'static) -> impl FnOnce(Build) -> Build {
557    move |inner| {
558        Box::new(move |cx| {
559            let n = inner(cx);
560            with_tree(|t| t.enable_gesture(n, GestureKind::Tap));
561            cx.on(n, move |ev| {
562                if let Event::Tap(p) = ev {
563                    f(*p);
564                }
565            });
566            n
567        })
568    }
569}
570
571fn op_on_key(f: impl Fn(&day_spec::KeyEvent) + 'static) -> impl FnOnce(Build) -> Build {
572    move |inner| {
573        Box::new(move |cx| {
574            let n = inner(cx);
575            // Declare the intent as well as listening: a backend whose focused view would have
576            // to claim the key from the platform's dispatch checks this first. That check runs
577            // on the view, which sits below `n` when `n` is a layout wrapper (`.frame(..)`), so
578            // the view is marked too; the key it claims reaches this handler through the
579            // wrapper (day-core's dispatch).
580            day_spec::keys::mark(day_core::rnode_to_id(n));
581            if let Some(view) = with_tree(|t| t.native_view_of(n))
582                && view != n
583            {
584                day_spec::keys::mark(day_core::rnode_to_id(view));
585            }
586            cx.on(n, move |ev| {
587                if let Event::Key(k) = ev {
588                    f(k);
589                }
590            });
591            n
592        })
593    }
594}
595
596/// Declare toolbar items for the chrome this piece sits under (docs/toolbars.md).
597///
598/// The chrome is resolved where the piece is built, not where the modifier was written: the
599/// innermost navigation page being built, or the window when there is none. The design exists so
600/// that a command sits beside the content it acts on, and the app never says twice which bar it
601/// belongs to.
602fn op_toolbar(source: crate::ToolbarSource) -> impl FnOnce(Build) -> Build {
603    move |inner| {
604        Box::new(move |cx| {
605            let n = inner(cx);
606            crate::contribute(day_core::current_chrome(), source);
607            n
608        })
609    }
610}
611
612fn op_focusable() -> impl FnOnce(Build) -> Build {
613    move |inner| {
614        Box::new(move |cx| {
615            let n = inner(cx);
616            with_tree(|t| t.set_focusable(n, true));
617            n
618        })
619    }
620}
621
622fn op_focused(
623    want: Box<dyn Fn() -> bool>,
624    on_native: Box<dyn Fn(bool)>,
625) -> impl FnOnce(Build) -> Build {
626    move |inner| {
627        Box::new(move |cx| {
628            let n = inner(cx);
629            // Echo cell: the control's focus state as last reported by the native side. An
630            // apply whose desired state matches it is the echo of a native change (or already
631            // satisfied) and must not re-drive the toolkit: the nav host echo-cell rule.
632            let native = Rc::new(Cell::new(false));
633            {
634                let native = native.clone();
635                cx.on(n, move |ev| {
636                    if let Event::FocusChanged(f) = ev {
637                        native.set(*f);
638                        on_native(*f);
639                    }
640                });
641            }
642            // Signal → native, deferred one turn (`on_main`): focus is async by contract, and
643            // the deferral also lets a mount-time `Some(K::V)` land after the widget is in the
644            // window (dialog default focus). The initial `false` is not applied, because resigning
645            // focus the control never had would steal it from whoever has it.
646            let first = Cell::new(true);
647            bind(want, move |want: &bool| {
648                let want = *want;
649                if first.replace(false) && !want {
650                    return;
651                }
652                if native.get() == want {
653                    return;
654                }
655                day_reactive::on_main(move || with_tree(|t| t.focus_node(n, want)));
656            });
657            n
658        })
659    }
660}
661
662fn op_context_menu(items: Vec<MenuEntry>) -> impl FnOnce(Build) -> Build {
663    move |inner| {
664        Box::new(move |cx| {
665            // The overlay host places the decorated subtree at its own bounds and the
666            // composed-menu mount beside it (see `OverlayHost`; a plain sibling would sit
667            // under a single-child layout, never placed).
668            let w = cx.layout_only(Rc::new(OverlayHost), Flex::default(), Boundary::No);
669            let mut n = w;
670            cx.under(w, |cx| {
671                n = inner(cx);
672                // Scoped: the action closures die with the build scope, not the process;
673                // an unscoped registration here leaks one closure per remount.
674                let model = lower_menu_scoped(items);
675                with_tree(|t| t.set_context_menu(n, model.clone()));
676                // A backend with no native menu reports the summon instead; the composed
677                // presenter replays the same lowered model (docs/menus.md).
678                let model = std::rc::Rc::new(model);
679                crate::menus::mount_composed_menu(cx, n, Rc::new(move |_p| (*model).clone()));
680            });
681            // Sized by its content, so a platform row stays one through the menu's wrapper.
682            with_tree(|t| t.forward_whole_row(w, n));
683            // Later ops decorate the content node: ids, gestures and menus belong to it,
684            // while the wrapper only exists to keep the mount in the layout pass.
685            n
686        })
687    }
688}
689
690/// The `.context_menu*` wrapper's layout: the decorated subtree fills the wrapper; every
691/// other child (the composed menu's lazy mount) is layout-inert but must still be visited,
692/// because a cover lays its content out from the size the backend reports, but only if the
693/// place pass reaches it at all.
694struct OverlayHost;
695
696impl Layout for OverlayHost {
697    fn measure(
698        &self,
699        cx: &mut dyn LayoutOps,
700        children: &[day_core::RNode],
701        p: day_spec::Proposal,
702    ) -> day_spec::Size {
703        match children.first() {
704            Some(&c) => cx.measure_child(c, p),
705            None => day_spec::Size::ZERO,
706        }
707    }
708    fn place(&self, cx: &mut dyn LayoutOps, children: &[day_core::RNode], bounds: day_spec::Rect) {
709        let mut it = children.iter();
710        if let Some(&c) = it.next() {
711            cx.place_child(c, day_spec::Rect::from_size(bounds.size));
712        }
713        for &c in it {
714            cx.place_child(c, day_spec::Rect::ZERO);
715        }
716    }
717    fn baseline(
718        &self,
719        cx: &mut dyn LayoutOps,
720        children: &[day_core::RNode],
721        size: day_spec::Size,
722    ) -> Option<f64> {
723        children.first().and_then(|&c| cx.baseline_of(c, size))
724    }
725}
726
727fn op_context_menu_fn(
728    f: Rc<dyn Fn(day_spec::Point) -> Vec<MenuEntry>>,
729) -> impl FnOnce(Build) -> Build {
730    move |inner| {
731        Box::new(move |cx| {
732            let w = cx.layout_only(Rc::new(OverlayHost), Flex::default(), Boundary::No);
733            let mut out = w;
734            cx.under(w, |cx| {
735                out = op_context_menu_fn_body(inner, f, cx);
736            });
737            // Sized by its content, so a platform row stays one through the menu's wrapper.
738            with_tree(|t| t.forward_whole_row(w, out));
739            out
740        })
741    }
742}
743
744fn op_context_menu_fn_body(
745    inner: Build,
746    f: Rc<dyn Fn(day_spec::Point) -> Vec<MenuEntry>>,
747    cx: &mut day_core::BuildCx,
748) -> day_core::RNode {
749    {
750        {
751            let n = inner(cx);
752            // Each summon lowers a fresh menu whose action closures live in their own scope,
753            // disposed when the next summon replaces them (and with the build scope at
754            // teardown), so per-click menus never accumulate registrations.
755            let last: Rc<std::cell::RefCell<Option<day_reactive::Scope>>> = Rc::default();
756            {
757                let last = last.clone();
758                day_reactive::Scope::current().on_cleanup(move || {
759                    if let Some(s) = last.borrow_mut().take() {
760                        s.dispose();
761                    }
762                });
763            }
764            let provider: day_spec::ContextMenuFn = Rc::new(move |p| {
765                if let Some(s) = last.borrow_mut().take() {
766                    s.dispose();
767                }
768                let scope = day_reactive::Scope::child();
769                let items = scope.enter(|| crate::menus::lower_menu_scoped(f(p)));
770                *last.borrow_mut() = Some(scope);
771                items
772            });
773            with_tree(|t| t.set_context_menu_fn(n, provider.clone()));
774            // A backend with no native menu reports the summon instead; the composed
775            // presenter calls the same provider there (docs/menus.md).
776            crate::menus::mount_composed_menu(cx, n, provider);
777            n
778        }
779    }
780}
781
782fn op_on_drag(f: impl Fn(Drag) + 'static) -> impl FnOnce(Build) -> Build {
783    move |inner| {
784        Box::new(move |cx| {
785            let n = inner(cx);
786            with_tree(|t| t.enable_gesture(n, GestureKind::Drag));
787            cx.on(n, move |ev| {
788                if let Event::Drag {
789                    phase,
790                    location,
791                    translation,
792                } = ev
793                {
794                    f(Drag {
795                        phase: *phase,
796                        location: *location,
797                        translation: *translation,
798                    });
799                }
800            });
801            n
802        })
803    }
804}
805
806fn op_on_pinch(f: impl Fn(Pinch) + 'static) -> impl FnOnce(Build) -> Build {
807    move |inner| {
808        Box::new(move |cx| {
809            let n = inner(cx);
810            with_tree(|t| t.enable_gesture(n, GestureKind::Pinch));
811            cx.on(n, move |ev| {
812                if let Event::Pinch {
813                    phase,
814                    scale,
815                    location,
816                } = ev
817                {
818                    f(Pinch {
819                        phase: *phase,
820                        scale: *scale,
821                        location: *location,
822                    });
823                }
824            });
825            n
826        })
827    }
828}
829
830fn op_on_hover(f: impl Fn(Option<day_spec::Point>) + 'static) -> impl FnOnce(Build) -> Build {
831    move |inner| {
832        Box::new(move |cx| {
833            let n = inner(cx);
834            with_tree(|t| t.enable_gesture(n, GestureKind::Hover));
835            cx.on(n, move |ev| {
836                if let Event::Hover { phase, location } = ev {
837                    // `None` on exit rather than a phase the caller has to match on: what a
838                    // hover handler wants to know is where the pointer IS, and "nowhere" is a
839                    // state, not an event kind. It is also what makes a handler that writes a
840                    // `Signal<Option<Point>>` a one-liner.
841                    f(match phase {
842                        day_spec::DragPhase::Ended => None,
843                        _ => Some(*location),
844                    });
845                }
846            });
847            n
848        })
849    }
850}
851
852fn op_on_pan(f: impl Fn(Pan) + 'static) -> impl FnOnce(Build) -> Build {
853    move |inner| {
854        Box::new(move |cx| {
855            let n = inner(cx);
856            with_tree(|t| t.enable_gesture(n, GestureKind::Pan));
857            cx.on(n, move |ev| {
858                if let Event::Pan {
859                    phase,
860                    delta,
861                    location,
862                } = ev
863                {
864                    f(Pan {
865                        phase: *phase,
866                        delta: *delta,
867                        location: *location,
868                    });
869                }
870            });
871            n
872        })
873    }
874}
875
876fn op_background(color: Reactive<Color>) -> impl FnOnce(Build) -> Build {
877    move |inner| {
878        Box::new(move |cx| {
879            let node = cx.native(
880                kinds::CONTAINER,
881                &ContainerProps {
882                    background: Some(color.get_untracked()),
883                    corner_radius: 0.0,
884                    clips: false,
885                    role: None,
886                },
887                Rc::new(FillThrough),
888                Flex::default(),
889                Boundary::No,
890            );
891            cx.under(node, |cx| {
892                let _ = inner(cx);
893            });
894            // Only a reactive source needs a binding; a constant fill is applied once at realize.
895            if let Reactive::Dyn(_) = &color {
896                bind(
897                    move || color.get(),
898                    move |c: &Color| {
899                        with_tree(|t| {
900                            t.patch(node, Box::new(ContainerPatch::Background(Some(*c))), false)
901                        });
902                    },
903                );
904            }
905            node
906        })
907    }
908}
909
910/// A semantic surface around the content: the toolkit's material for `role`, clipped to
911/// `radius` corners.
912fn op_surface(role: day_spec::SurfaceRole, radius: f64) -> impl FnOnce(Build) -> Build {
913    move |inner| {
914        Box::new(move |cx| {
915            let w = cx.native(
916                kinds::CONTAINER,
917                &ContainerProps {
918                    background: None,
919                    corner_radius: radius,
920                    clips: true,
921                    role: Some(role),
922                },
923                Rc::new(FillThrough),
924                Flex::default(),
925                Boundary::No,
926            );
927            cx.under(w, |cx| {
928                let _ = inner(cx);
929            });
930            w
931        })
932    }
933}
934
935fn op_corner_radius(radius: f64) -> impl FnOnce(Build) -> Build {
936    move |inner| {
937        Box::new(move |cx| {
938            let node = cx.native(
939                kinds::CONTAINER,
940                &ContainerProps {
941                    background: None,
942                    corner_radius: radius,
943                    clips: true,
944                    role: None,
945                },
946                Rc::new(FillThrough),
947                Flex::default(),
948                Boundary::No,
949            );
950            cx.under(node, |cx| {
951                let _ = inner(cx);
952            });
953            node
954        })
955    }
956}
957
958fn op_opacity(op: Reactive<f64>) -> impl FnOnce(Build) -> Build {
959    move |inner| {
960        Box::new(move |cx| {
961            let node = layer_node(cx);
962            cx.under(node, |cx| {
963                let _ = inner(cx);
964            });
965            bind(
966                move || op.get(),
967                move |v: &f64| with_tree(|t| t.set_node_opacity(node, *v)),
968            );
969            node
970        })
971    }
972}
973
974fn op_transform(t: Reactive<Transform>) -> impl FnOnce(Build) -> Build {
975    move |inner| {
976        Box::new(move |cx| {
977            let node = layer_node(cx);
978            cx.under(node, |cx| {
979                let _ = inner(cx);
980            });
981            bind(
982                move || t.get(),
983                move |v: &Transform| with_tree(|tr| tr.set_node_transform(node, *v)),
984            );
985            node
986        })
987    }
988}
989
990fn op_animation(anim: AnimSpec) -> impl FnOnce(Build) -> Build {
991    move |inner| {
992        Box::new(move |cx| {
993            // Layout-only: the implicit animation is Day's own state, which the patches beneath
994            // look up through their ancestors, and needs no native view. A native layer here
995            // also clipped what it held on the toolkits whose containers clip their children
996            // (Qt always, Android by default), so a translated or scaled view under
997            // `.animation(..)` vanished past the layer's frame.
998            let node = cx.layout_only(Rc::new(FillThrough), Flex::default(), Boundary::No);
999            with_tree(|t| t.set_implicit_anim(node, Some(anim)));
1000            cx.under(node, |cx| {
1001                let _ = inner(cx);
1002            });
1003            node
1004        })
1005    }
1006}
1007
1008fn op_overlay_aligned(align: Alignment, over: impl Piece) -> impl FnOnce(Build) -> Build {
1009    move |inner| {
1010        Box::new(move |cx| {
1011            let node = cx.native(
1012                kinds::CONTAINER,
1013                &ContainerProps::default(),
1014                Rc::new(OverlayLayout {
1015                    align,
1016                    size_to_first: true,
1017                }),
1018                Flex::default(),
1019                Boundary::No,
1020            );
1021            cx.under(node, |cx| {
1022                let _ = inner(cx); // sizing content (bottom)
1023                let _ = over.build(cx); // annotation on top
1024            });
1025            node
1026        })
1027    }
1028}
1029
1030fn op_aspect_ratio(ratio: f64) -> impl FnOnce(Build) -> Build {
1031    move |inner| {
1032        Box::new(move |cx| {
1033            if !(ratio.is_finite() && ratio > 0.0) {
1034                return inner(cx);
1035            }
1036            let node = cx.layout_only(
1037                Rc::new(AspectRatioLayout { ratio }),
1038                Flex::default(),
1039                // Not a boundary: the child still measures itself, and the ratio only decides
1040                // the box it is offered.
1041                Boundary::No,
1042            );
1043            cx.under(node, |cx| {
1044                let _ = inner(cx);
1045            });
1046            node
1047        })
1048    }
1049}
1050
1051fn op_grow_axes(w: bool, h: bool) -> impl FnOnce(Build) -> Build {
1052    move |inner| {
1053        Box::new(move |cx| {
1054            let node = cx.layout_only(
1055                Rc::new(GrowLayout { w, h }),
1056                Flex {
1057                    grow_w: w,
1058                    grow_h: h,
1059                    ..Default::default()
1060                },
1061                Boundary::No,
1062            );
1063            cx.under(node, |cx| {
1064                let _ = inner(cx);
1065            });
1066            node
1067        })
1068    }
1069}
1070
1071fn op_grid_facts(facts: GridFacts) -> impl FnOnce(RNode) {
1072    move |node| with_tree(|t| t.set_grid_facts(node, facts))
1073}
1074
1075fn op_defers_system_gestures(edges: day_spec::Edges) -> impl FnOnce(Build) -> Build {
1076    move |inner| {
1077        Box::new(move |cx| {
1078            let token = day_core::shield::push_gesture_deferral(edges);
1079            Scope::current().on_cleanup(move || day_core::shield::pop_gesture_deferral(token));
1080            inner(cx)
1081        })
1082    }
1083}
1084
1085fn op_status_bar_hidden(hidden: Reactive<bool>) -> impl FnOnce(Build) -> Build {
1086    move |inner| {
1087        Box::new(move |cx| {
1088            let token = day_core::shield::push_status_bar_request(hidden.get_untracked());
1089            Scope::current().on_cleanup(move || day_core::shield::pop_status_bar_request(token));
1090            // A constant answer is registered once; a reactive one follows its source while
1091            // the subtree stays mounted (a reader's "hide status bar" setting).
1092            if let Reactive::Dyn(_) = &hidden {
1093                bind(
1094                    move || hidden.get(),
1095                    move |h: &bool| day_core::shield::set_status_bar_request(token, *h),
1096                );
1097            }
1098            inner(cx)
1099        })
1100    }
1101}
1102
1103fn op_interactive_dismiss_disabled() -> impl FnOnce(Build) -> Build {
1104    move |inner| {
1105        Box::new(move |cx| {
1106            let token = day_core::shield::push_dismiss_disabled();
1107            Scope::current().on_cleanup(move || day_core::shield::pop_dismiss_disabled(token));
1108            inner(cx)
1109        })
1110    }
1111}
1112
1113// --- The chained modifier surface, on an already-decorated piece ---
1114//
1115// These inherent methods shadow the `Decorate` trait's (inherent wins method resolution), so a
1116// modifier applied to a `Decorated` appends to its op list instead of wrapping it in another
1117// `Decorated`. Each is the same one-liner the trait method is; the bodies live in the `op_*`
1118// functions above. Documentation stays on the trait, which is the surface every piece has.
1119impl<P: Piece> Decorated<P> {
1120    pub fn id(self, id: day_core::ElementId) -> Self {
1121        self.push_node(op_id(id))
1122    }
1123    pub fn id_of(self, id: impl Fn() -> day_core::ElementId + 'static) -> Self {
1124        self.push_node(op_id_of(id))
1125    }
1126    pub fn wrapper_id(self, id: day_core::ElementId) -> Self {
1127        self.push(op_wrapper_id(id))
1128    }
1129    pub fn tweak(self, f: impl FnOnce(day_core::RNode) + 'static) -> Self {
1130        self.push(op_tweak(f))
1131    }
1132    pub fn selectable(self) -> Self {
1133        self.push(op_selectable())
1134    }
1135    pub fn cursor<M>(self, cursor: impl IntoReactive<Cursor, M>) -> Self {
1136        self.push(op_cursor(cursor.into_reactive()))
1137    }
1138    pub fn native_ref(self, r: &NativeRef) -> Self {
1139        self.push(op_native_ref(r.clone()))
1140    }
1141    pub fn window_drag_region(self) -> Self {
1142        self.push(op_window_drag_region())
1143    }
1144    pub fn padding(self, insets: impl IntoInsets) -> Self {
1145        self.push(op_padding(insets.into_insets()))
1146    }
1147    pub fn max_width(self, max: f64) -> Self {
1148        self.push(op_max_width(max))
1149    }
1150    /// Never taller than `max`: the child is never proposed more height, so a scroll stops
1151    /// growing at `max` and still shrinks where the screen is shorter. In a column it is sized
1152    /// after its siblings and takes what they leave, up to `max`, so the controls around a
1153    /// capped scroll stay on screen.
1154    pub fn max_height(self, max: f64) -> Self {
1155        self.push(op_max_height(max))
1156    }
1157    /// Never narrower than `min`: a stretching control keeps a usable width, and a row that
1158    /// cannot give it that overflows, so a `labeled` row stacks the control under its label
1159    /// rather than squeezing it (docs/forms.md).
1160    pub fn min_width(self, min: f64) -> Self {
1161        self.push(op_min_width(min))
1162    }
1163    pub fn reserving(self, sample: impl Into<String>) -> Self {
1164        self.push(op_reserving(sample.into()))
1165    }
1166    pub fn frame(self, width: f64, height: f64) -> Self {
1167        self.push(op_frame(Some(width), Some(height), Boundary::Yes))
1168    }
1169    pub fn width(self, width: f64) -> Self {
1170        self.push(op_frame(Some(width), None, Boundary::No))
1171    }
1172    pub fn height(self, height: f64) -> Self {
1173        self.push(op_frame(None, Some(height), Boundary::No))
1174    }
1175    pub fn a11y(self, f: impl FnOnce(A11yBuilder) -> A11yBuilder + 'static) -> Self {
1176        self.push(op_a11y(f))
1177    }
1178    /// Handle a tap gesture on this piece.
1179    ///
1180    /// Do not use this to activate a native button; use [`crate::ButtonBuilder::action`].
1181    /// A separate gesture recognizer can delay pressed feedback and bypass native keyboard,
1182    /// accessibility, and disabled behavior. Day warns when it attaches a tap to a button.
1183    pub fn on_tap(self, f: impl Fn() + 'static) -> Self {
1184        self.push(op_on_tap(f))
1185    }
1186    /// Handle a tap with its local position. Like [`Self::on_tap`], this is a gesture,
1187    /// not native button activation; use [`crate::ButtonBuilder::action`] for buttons.
1188    pub fn on_tap_at(self, f: impl Fn(day_spec::Point) + 'static) -> Self {
1189        self.push(op_on_tap_at(f))
1190    }
1191    pub fn focused<M>(self, binding: impl IntoFocusBinding<M>) -> Self {
1192        let (want, on_native) = binding.into_focus_binding();
1193        self.push(op_focused(want, on_native))
1194    }
1195    pub fn on_key(self, f: impl Fn(&day_spec::KeyEvent) + 'static) -> Self {
1196        self.push(op_on_key(f))
1197    }
1198    pub fn focusable(self) -> Self {
1199        self.push(op_focusable())
1200    }
1201    pub fn toolbar<M>(self, content: impl crate::ToolbarContent<M>) -> Self {
1202        self.push(op_toolbar(content.into_source()))
1203    }
1204    /// See [`Decorate::searchable`].
1205    pub fn searchable(self, search: crate::Search) -> Self {
1206        self.push(move |inner| {
1207            Box::new(move |cx| {
1208                let n = inner(cx);
1209                crate::nav::install_page_search(search);
1210                n
1211            })
1212        })
1213    }
1214    /// Offer data through the platform's native drag session.
1215    pub fn drag_source(
1216        self,
1217        source: impl Fn(day_spec::Point) -> Option<day_spec::transfer::Offer> + 'static,
1218    ) -> Self {
1219        self.push(move |inner| {
1220            Box::new(move |cx| {
1221                let n = layer_node(cx);
1222                cx.under(n, |cx| {
1223                    inner(cx);
1224                });
1225                let source = scoped_drag_source(Rc::new(source));
1226                with_tree(|t| t.set_drag_source(n, source));
1227                n
1228            })
1229        })
1230    }
1231    /// Attach native drop acceptance policy and an owned-data receiver.
1232    pub fn drop_target(self, target: day_spec::transfer::Target) -> Self {
1233        self.push(move |inner| {
1234            Box::new(move |cx| {
1235                let n = layer_node(cx);
1236                cx.under(n, |cx| {
1237                    inner(cx);
1238                });
1239                let target = scoped_drop_target(target);
1240                with_tree(|t| t.set_drop_target(n, target));
1241                n
1242            })
1243        })
1244    }
1245    pub fn context_menu<M>(self, menu: impl ContextMenuContent<M>) -> Self {
1246        match menu.into_context_menu() {
1247            ContextMenuSource::Fixed(items) => self.push(op_context_menu(items)),
1248            ContextMenuSource::OnSummon(f) => self.push(op_context_menu_fn(f)),
1249        }
1250    }
1251    pub fn on_drag(self, f: impl Fn(Drag) + 'static) -> Self {
1252        self.push(op_on_drag(f))
1253    }
1254    pub fn on_pinch(self, f: impl Fn(Pinch) + 'static) -> Self {
1255        self.push(op_on_pinch(f))
1256    }
1257    pub fn on_pan(self, f: impl Fn(Pan) + 'static) -> Self {
1258        self.push(op_on_pan(f))
1259    }
1260    /// The pointer moving over this piece: `Some(point)` in its own coordinates while inside,
1261    /// `None` when it leaves (docs/canvas.md "Interaction").
1262    ///
1263    /// Pointer-only, which is not a gap: every desktop delivers it, an
1264    /// iPad does with a trackpad or pencil, an Android device with a mouse or stylus, and a
1265    /// touch-only phone delivers nothing at all. **Anything reachable by hover must also be
1266    /// reachable by a tap**: wire `on_tap_at` alongside it, exactly as a chart's selection does.
1267    pub fn on_hover(self, f: impl Fn(Option<day_spec::Point>) + 'static) -> Self {
1268        self.push(op_on_hover(f))
1269    }
1270    /// The platform's card: the grouped-content surface (`SurfaceRole::SectionCard`) with
1271    /// rounded corners and a `Space::M` inset. Drawn by the toolkit, so it follows the
1272    /// appearance and never steals hit testing from the content.
1273    pub fn card(self) -> Self {
1274        self.padding(crate::Space::M)
1275            .push(op_surface(day_spec::SurfaceRole::SectionCard, 10.0))
1276    }
1277    pub fn background<M>(self, color: impl IntoReactive<Color, M>) -> Self {
1278        self.push(op_background(color.into_reactive()))
1279    }
1280    pub fn corner_radius(self, radius: f64) -> Self {
1281        self.push(op_corner_radius(radius))
1282    }
1283    pub fn opacity<M>(self, opacity: impl IntoReactive<f64, M>) -> Self {
1284        self.push(op_opacity(opacity.into_reactive()))
1285    }
1286    pub fn transform<M>(self, t: impl IntoReactive<Transform, M>) -> Self {
1287        self.push(op_transform(t.into_reactive()))
1288    }
1289    pub fn scale<M>(self, factor: impl IntoReactive<f64, M>) -> Self {
1290        let f = factor.into_reactive();
1291        self.transform(move || Transform::scale(f.get(), f.get()))
1292    }
1293    pub fn rotation<M>(self, degrees: impl IntoReactive<f64, M>) -> Self {
1294        let d = degrees.into_reactive();
1295        self.transform(move || Transform::rotate(d.get()))
1296    }
1297    pub fn translation<Mx, My>(
1298        self,
1299        x: impl IntoReactive<f64, Mx>,
1300        y: impl IntoReactive<f64, My>,
1301    ) -> Self {
1302        let (x, y) = (x.into_reactive(), y.into_reactive());
1303        self.transform(move || Transform::translate(x.get(), y.get()))
1304    }
1305    pub fn animation(self, anim: AnimSpec) -> Self {
1306        self.push(op_animation(anim))
1307    }
1308    /// Erases, like [`Decorate::modifier`]: `Modifier` is defined over [`AnyPiece`].
1309    pub fn modifier(self, m: impl Modifier) -> AnyPiece {
1310        m.apply(self.any())
1311    }
1312    pub fn overlay(self, over: impl Piece) -> Self {
1313        self.overlay_aligned(Alignment::Center, over)
1314    }
1315    pub fn overlay_aligned(self, align: Alignment, over: impl Piece) -> Self {
1316        self.push(op_overlay_aligned(align, over))
1317    }
1318    pub fn aspect_ratio(self, ratio: f64) -> Self {
1319        self.push(op_aspect_ratio(ratio))
1320    }
1321    pub fn grow(self) -> Self {
1322        self.grow_axes(true, true)
1323    }
1324    pub fn grow_w(self) -> Self {
1325        self.grow_axes(true, false)
1326    }
1327    pub fn grow_h(self) -> Self {
1328        self.grow_axes(false, true)
1329    }
1330    #[doc(hidden)]
1331    fn grow_axes(self, w: bool, h: bool) -> Self {
1332        self.push(op_grow_axes(w, h))
1333    }
1334    pub fn grid_span(self, n: usize) -> Self {
1335        self.push_outer(op_grid_facts(GridFacts {
1336            col_span: n.clamp(1, u16::MAX as usize) as u16,
1337            ..Default::default()
1338        }))
1339    }
1340    pub fn grid_align(self, a: Alignment) -> Self {
1341        self.push_outer(op_grid_facts(GridFacts {
1342            align: Some(a),
1343            ..Default::default()
1344        }))
1345    }
1346    pub fn defers_system_gestures(self, edges: day_spec::Edges) -> Self {
1347        self.push(op_defers_system_gestures(edges))
1348    }
1349    pub fn interactive_dismiss_disabled(self) -> Self {
1350        self.push(op_interactive_dismiss_disabled())
1351    }
1352    pub fn status_bar_hidden<M>(self, hidden: impl IntoReactive<bool, M>) -> Self {
1353        self.push(op_status_bar_hidden(hidden.into_reactive()))
1354    }
1355    /// Erase to a single [`AnyPiece`].
1356    pub fn any(self) -> AnyPiece {
1357        AnyPiece::new(self)
1358    }
1359}
1360
1361/// Where a context menu's entries come from (see [`Decorate::context_menu`]).
1362pub enum ContextMenuSource {
1363    Fixed(Vec<MenuEntry>),
1364    OnSummon(Rc<dyn Fn(day_spec::Point) -> Vec<MenuEntry>>),
1365}
1366
1367/// Anything [`Decorate::context_menu`] accepts: a `Vec` or array of entries, a closure building
1368/// them on summon, or a closure given the summon point. The marker parameter is the same E0119
1369/// dodge as [`IntoText`].
1370pub trait ContextMenuContent<M> {
1371    fn into_context_menu(self) -> ContextMenuSource;
1372}
1373
1374/// Marker for a summon-time closure that takes the summon point.
1375pub struct AtPointMark;
1376
1377impl ContextMenuContent<crate::ManyMark> for Vec<MenuEntry> {
1378    fn into_context_menu(self) -> ContextMenuSource {
1379        ContextMenuSource::Fixed(self)
1380    }
1381}
1382
1383impl<const N: usize> ContextMenuContent<crate::ManyMark> for [MenuEntry; N] {
1384    fn into_context_menu(self) -> ContextMenuSource {
1385        ContextMenuSource::Fixed(Vec::from(self))
1386    }
1387}
1388
1389impl<C, F> ContextMenuContent<crate::DerivedMark> for F
1390where
1391    C: IntoIterator<Item = MenuEntry>,
1392    F: Fn() -> C + 'static,
1393{
1394    fn into_context_menu(self) -> ContextMenuSource {
1395        ContextMenuSource::OnSummon(Rc::new(move |_| self().into_iter().collect()))
1396    }
1397}
1398
1399impl<C, F> ContextMenuContent<AtPointMark> for F
1400where
1401    C: IntoIterator<Item = MenuEntry>,
1402    F: Fn(day_spec::Point) -> C + 'static,
1403{
1404    fn into_context_menu(self) -> ContextMenuSource {
1405        ContextMenuSource::OnSummon(Rc::new(move |p| self(p).into_iter().collect()))
1406    }
1407}
1408
1409pub trait Decorate: Piece + Sized {
1410    /// Stable element identifier: a11y identifier + dayscript locator + lint uniqueness (§5.5).
1411    ///
1412    /// Names the piece's own node wherever it sits in the chain (docs/api-style.md "Modifier
1413    /// classes"): `.padding(8.0).id(day_core::id!(x))` and `.id(day_core::id!(x)).padding(8.0)` tag the same node. To name
1414    /// a wrapper instead, use [`wrapper_id`](Self::wrapper_id).
1415    fn id(self, id: day_core::ElementId) -> Decorated<Self> {
1416        Decorated::new(self).id(id)
1417    }
1418
1419    /// Name the node that is outermost at this point in the chain: the padding box, the frame,
1420    /// the background, whichever wrapper the modifiers before it built. For a dayscript frame
1421    /// assertion on the wrapper rather than on the piece. Ordered, unlike [`id`](Self::id).
1422    fn wrapper_id(self, id: day_core::ElementId) -> Decorated<Self> {
1423        Decorated::new(self).wrapper_id(id)
1424    }
1425
1426    /// Apply `f` to the chain when `cond` holds, else continue unchanged
1427    /// (see [`Decorated::apply_if`]): `nav(sel).apply_if(!primary, |n| n.local())`. The
1428    /// closure receives the decorated piece, so forwarded builder methods and modifiers both
1429    /// chain inside it.
1430    fn apply_if(
1431        self,
1432        cond: bool,
1433        f: impl FnOnce(Decorated<Self>) -> Decorated<Self>,
1434    ) -> Decorated<Self> {
1435        Decorated::new(self).apply_if(cond, f)
1436    }
1437
1438    /// Apply `f` with the value when there is one, else continue unchanged
1439    /// (see [`Decorated::apply_some`]): `canvas(draw).apply_some(id, |c, id| c.id(id))`.
1440    fn apply_some<T>(
1441        self,
1442        value: Option<T>,
1443        f: impl FnOnce(Decorated<Self>, T) -> Decorated<Self>,
1444    ) -> Decorated<Self> {
1445        Decorated::new(self).apply_some(value, f)
1446    }
1447
1448    /// Reactive element id: the id for rows inside a recycling [`list`](crate::list). A plain
1449    /// [`id`](Self::id) is assigned once at build, but a recycled cell rebinds to different
1450    /// items over its life (and drag-to-reorder rebinds eagerly), so a static item-derived id
1451    /// keeps naming the first-bound item. This variant re-registers whenever the closure's
1452    /// value changes; read your `ItemSlot` inside it:
1453    /// `.id_of(move || day_core::id!(row_remove, slot.key()))`.
1454    fn id_of(self, id: impl Fn() -> day_core::ElementId + 'static) -> Decorated<Self> {
1455        Decorated::new(self).id_of(id)
1456    }
1457
1458    /// Apply a **tweak**: `f` runs once at mount, after the native widget exists, with the
1459    /// realized node (docs/tweaks.md). Reach the typed native handle through the compiled
1460    /// backend's ext accessor (`day_toolkit_appkit::with_native`, `day_toolkit_gtk::with_native`, …), or apply
1461    /// a packaged `day-tweak-*` crate's modifier instead of calling this directly. If the native
1462    /// change affects the widget's intrinsic size, follow it with
1463    /// [`day_core::invalidate_size`]. Day may overwrite *managed* properties (title, value,
1464    /// enabled, frame, a11y) on its next patch; unmanaged properties are stable.
1465    ///
1466    /// Order it after any modifier that can rebuild the backing widget; today that is
1467    /// [`selectable`](Decorate::selectable), which on UIKit realizes the label as a different
1468    /// native class. Chained before it, the tweak runs against the widget the rebuild discards
1469    /// (Day warns at runtime); chained after, it sees the widget that ships.
1470    fn tweak(self, f: impl FnOnce(day_core::RNode) + 'static) -> Decorated<Self> {
1471        Decorated::new(self).tweak(f)
1472    }
1473
1474    /// Make this piece's text **user-selectable**, so the reader can select and copy it
1475    /// (docs/text.md). Most useful on a [`label`](crate::label): text is not selectable by default
1476    /// on any backend, matching each platform's native behavior.
1477    ///
1478    /// Every backend honors it on a label: most flip the native widget's selection affordance
1479    /// (AppKit, GTK, Qt, XAML, HarmonyOS, Android, web); UIKit, whose `UILabel` has none,
1480    /// rebuilds the label as a read-only `UITextView` behind the same handle. On other widgets
1481    /// it is best-effort: a backing with no selection affordance leaves the text unselectable
1482    /// rather than erroring, and a container cascades only where the platform's affordance does
1483    /// (the web), so prefer the label itself. Selection visuals and the copy shortcut are the
1484    /// platform's own. Unmanaged: set once at mount, and it survives Day's text updates.
1485    fn selectable(self) -> Decorated<Self> {
1486        Decorated::new(self).selectable()
1487    }
1488
1489    /// Shape the pointer while it is over this piece and its descendants (docs/cursor.md): a
1490    /// [`Cursor`] constant, a `Signal<Cursor>`, or a closure, so a canvas tool or a busy state
1491    /// moves the shape without rebuilding the piece. The nearest ancestor's cursor wins, and
1492    /// `Cursor::Default` releases the piece back to the platform. `capability(Cap::Cursor)`
1493    /// says whether this toolkit draws the requested shapes, a nearest neighbor, or nothing;
1494    /// a touch-only host never shows one, which is correct rather than a failure.
1495    fn cursor<M>(self, cursor: impl IntoReactive<Cursor, M>) -> Decorated<Self> {
1496        Decorated::new(self).cursor(cursor)
1497    }
1498
1499    /// Let the user move the window by dragging this piece, as a title bar does
1500    /// (docs/window-chrome.md): what a custom title bar in a [`WindowChrome::Overlay`] or
1501    /// [`WindowChrome::Frameless`] window needs. The piece's own background starts the drag,
1502    /// with the platform's snapping and tiling; buttons and fields inside it keep their clicks;
1503    /// a double click does what the user's title-bar setting says (zoom, minimize, nothing).
1504    /// `capability(Cap::DragRegion)`; on a phone the piece is an ordinary view.
1505    ///
1506    /// [`WindowChrome::Overlay`]: day_spec::WindowChrome::Overlay
1507    /// [`WindowChrome::Frameless`]: day_spec::WindowChrome::Frameless
1508    fn window_drag_region(self) -> Decorated<Self> {
1509        Decorated::new(self).window_drag_region()
1510    }
1511
1512    /// Capture a [`NativeRef`] to this piece's realized node for later imperative access
1513    /// (docs/tweaks.md). The ref clears automatically when the piece's scope is disposed.
1514    fn native_ref(self, r: &NativeRef) -> Decorated<Self> {
1515        Decorated::new(self).native_ref(r)
1516    }
1517
1518    fn padding(self, insets: impl IntoInsets) -> Decorated<Self> {
1519        Decorated::new(self).padding(insets)
1520    }
1521
1522    /// Cap this piece's width at `max` points: the child is never PROPOSED more, so text
1523    /// wraps inside the cap (chat bubbles, readable columns) while narrower content hugs.
1524    fn max_width(self, max: f64) -> Decorated<Self> {
1525        Decorated::new(self).max_width(max)
1526    }
1527    /// Cap this piece's height at `max` points, the vertical twin of [`Decorate::max_width`]: a
1528    /// scroll or another piece that takes the height it is offered stops at `max`, and still
1529    /// gets less where its parent has less to give.
1530    fn max_height(self, max: f64) -> Decorated<Self> {
1531        Decorated::new(self).max_height(max)
1532    }
1533    /// See [`Decorated::min_width`].
1534    fn min_width(self, min: f64) -> Decorated<Self> {
1535        Decorated::new(self).min_width(min)
1536    }
1537
1538    /// Reserve at least the space `sample` needs, so this piece's size stops changing with its
1539    /// content.
1540    ///
1541    /// For a numeric readout beside a slider: `label(move || value()).reserving("100")` keeps the
1542    /// row still while the number changes, because the reservation is a real measurement of
1543    /// `"100"` in this piece's own font: it scales with the platform's accessibility text size
1544    /// instead of being a point value that clips when someone turns text up.
1545    ///
1546    /// Pass the widest value the field can show (`"100"`, `"-99.9"`, `"88:88"`). Pair it with
1547    /// tabular numbers so the digits themselves stop shifting inside the reservation.
1548    /// The sample never paints and never takes hit-testing area.
1549    fn reserving(self, sample: impl Into<String>) -> Decorated<Self> {
1550        Decorated::new(self).reserving(sample)
1551    }
1552
1553    fn frame(self, width: f64, height: f64) -> Decorated<Self> {
1554        Decorated::new(self).frame(width, height)
1555    }
1556
1557    /// Fix this piece's width to `width` points while its height stays flexible (hugging its
1558    /// content or filling on the cross axis). The single-axis complement to [`Self::frame`], e.g.
1559    /// a fixed-width sidebar pane in a `row` whose height fills the window.
1560    fn width(self, width: f64) -> Decorated<Self> {
1561        Decorated::new(self).width(width)
1562    }
1563
1564    /// Fix this piece's height to `height` points while its width stays flexible. The single-axis
1565    /// complement to [`Self::frame`], e.g. a fixed-height header/toolbar bar that fills its width.
1566    fn height(self, height: f64) -> Decorated<Self> {
1567        Decorated::new(self).height(height)
1568    }
1569
1570    fn a11y(self, f: impl FnOnce(A11yBuilder) -> A11yBuilder + 'static) -> Decorated<Self> {
1571        Decorated::new(self).a11y(f)
1572    }
1573
1574    /// Fire when this piece is tapped (bounding-box; shapes override with path-precise testing).
1575    ///
1576    /// Do not use this to activate a native button; use [`crate::Button::action`] instead.
1577    /// A separate gesture recognizer can delay pressed feedback and bypass native keyboard,
1578    /// accessibility, and disabled behavior. Day warns when it attaches a tap to a button.
1579    fn on_tap(self, f: impl Fn() + 'static) -> Decorated<Self> {
1580        Decorated::new(self).on_tap(f)
1581    }
1582
1583    /// [`on_tap`](Self::on_tap), told where: the point in the piece's own coordinate space,
1584    /// origin at its top-leading corner.
1585    ///
1586    /// What a drawn control needs and a native one does not: a canvas showing a color wheel, a
1587    /// map, or a waveform has to turn "the user pressed here" into a value, and only the piece
1588    /// knows how. Pair it with [`on_drag`](Self::on_drag), which already reports a location, to
1589    /// track a press that turns into a drag; the two are idempotent together, so a backend that
1590    /// reports a tap as a zero-length drag costs nothing.
1591    ///
1592    /// `Event::Tap` has always carried the point; this is the decorator that stops throwing it
1593    /// away.
1594    ///
1595    /// For native buttons use [`crate::Button::action`], not this gesture decorator.
1596    fn on_tap_at(self, f: impl Fn(day_spec::Point) + 'static) -> Decorated<Self> {
1597        Decorated::new(self).on_tap_at(f)
1598    }
1599
1600    /// Bind this control's keyboard focus to a signal (docs/focus.md), two-way like every other
1601    /// binding: native focus changes write the signal; writing the signal moves focus. Takes a
1602    /// `Signal<bool>` for one control, or `(Signal<Option<K>>, K::Variant)` binding one control
1603    /// of a group; writing `false`/`None` resigns focus (dismissing the soft keyboard on
1604    /// mobile). Focus applies asynchronously: a write is a request, resolved on the next turn,
1605    /// and the signal always ends up reflecting what the platform actually did.
1606    fn focused<M>(self, binding: impl IntoFocusBinding<M>) -> Decorated<Self> {
1607        Decorated::new(self).focused(binding)
1608    }
1609
1610    /// Handle the non-text keys (the arrows) that reach this piece while it has focus
1611    /// (docs/menus.md). Keys follow focus, so pair it with [`Decorate::focused`] or with a
1612    /// piece the user can click into: a canvas takes focus on a press, and only the focused
1613    /// piece hears the keys, so a nudge handler can never fire while a text field, a list or a
1614    /// sidebar is the one being typed into.
1615    ///
1616    /// Which pieces can hold focus is the platform's to answer (docs/focus.md): a `canvas`
1617    /// is focusable on the backends that draw one from a real view (appkit, web-dom today).
1618    fn on_key(self, f: impl Fn(&day_spec::KeyEvent) + 'static) -> Decorated<Self> {
1619        Decorated::new(self).on_key(f)
1620    }
1621
1622    /// Declare toolbar items on the chrome this piece sits under (docs/toolbars.md).
1623    ///
1624    /// Takes one item, a list of them, or a closure that derives the list and re-runs whenever
1625    /// its reactive reads change:
1626    ///
1627    /// ```ignore
1628    /// page.toolbar(toolbar_button(day_core::id!(share), tr("share")).icon(Symbol::Share).action(share))
1629    /// page.toolbar([reply, forward, archive])
1630    /// page.toolbar(move || vec![toolbar_button(day_core::id!(undo), tr("undo")).enabled(can_undo)])
1631    /// ```
1632    ///
1633    /// Which chrome carries them follows from where this piece is built (a destination page's
1634    /// own bar, a content-list pane's, or the window's if it is under no page at all), and the
1635    /// items are withdrawn when this piece is disposed. Where on that chrome they sit is
1636    /// [`ToolbarEntry::placement`](crate::ToolbarEntry::placement).
1637    fn toolbar<M>(self, content: impl crate::ToolbarContent<M>) -> Decorated<Self> {
1638        Decorated::new(self).toolbar(content)
1639    }
1640
1641    /// A search field for this navigation page (docs/search.md, "Page search"), drawn where the
1642    /// platform puts a page's search: the window toolbar on a desktop, the page's navigation bar
1643    /// on iOS (a `UISearchController`), its app bar on Android. It shows while this page does, and
1644    /// its text is the app's `query` both ways. For a search the server runs, act on
1645    /// [`crate::Search::on_submit`]; for a local filter, read the query. Declare it on a
1646    /// `nav_stack` destination, as `.toolbar(…)` is declared; a whole navigation surface takes
1647    /// [`crate::Nav::searchable`] instead.
1648    fn searchable(self, search: crate::Search) -> Decorated<Self> {
1649        Decorated::new(self).searchable(search)
1650    }
1651
1652    /// Opt this piece into the platform's focus system (docs/focus.md): the canvas contract
1653    /// for anything composed: it joins the key loop, takes focus on a press, reports through
1654    /// `.focused(…)`, and hears the arrows through `.on_key(…)` while focused. A composed
1655    /// list column is the motivating case (docs/navigation.md). On a backend without the
1656    /// `set_focusable` duty the piece renders normally and never takes focus.
1657    fn focusable(self) -> Decorated<Self> {
1658        Decorated::new(self).focusable()
1659    }
1660
1661    /// Attach a context menu, shown with the platform's native affordance on secondary-click (desktop)
1662    /// or long-press (mobile). Items are built with [`menu_item`]/[`sub_menu`]/[`menu_role`]/
1663    /// [`menu_separator`]. Passing an empty `Vec` removes any menu.
1664    fn drag_source(
1665        self,
1666        source: impl Fn(day_spec::Point) -> Option<day_spec::transfer::Offer> + 'static,
1667    ) -> Decorated<Self> {
1668        Decorated::new(self).drag_source(source)
1669    }
1670    fn drop_target(self, target: day_spec::transfer::Target) -> Decorated<Self> {
1671        Decorated::new(self).drop_target(target)
1672    }
1673    /// A context menu (docs/menus.md): shown on secondary-click / long-press, built from the
1674    /// same `menu_item`/`sub_menu` entries as everywhere else. Takes a fixed list, or a closure
1675    /// built when the menu is summoned ("Dynamic context menus"): with no argument, or with the
1676    /// summon point in this piece's own coordinates, so a canvas can select what is under the
1677    /// pointer and offer commands for that selection. An empty result shows nothing.
1678    ///
1679    /// ```ignore
1680    /// label(t).context_menu([menu_item(res::str::copy()).action(copy)])
1681    /// canvas(draw).context_menu(move |at: Point| commands_for(hit_test(at)))
1682    /// ```
1683    fn context_menu<M>(self, menu: impl ContextMenuContent<M>) -> Decorated<Self> {
1684        Decorated::new(self).context_menu(menu)
1685    }
1686
1687    fn on_drag(self, f: impl Fn(Drag) + 'static) -> Decorated<Self> {
1688        Decorated::new(self).on_drag(f)
1689    }
1690
1691    /// Fire on each phase of a pinch/magnify over this piece (docs/canvas.md "Zoom and
1692    /// pan"). Only backends with a native recognizer wired emit it; pair a zoom with
1693    /// visible controls.
1694    fn on_pinch(self, f: impl Fn(Pinch) + 'static) -> Decorated<Self> {
1695        Decorated::new(self).on_pinch(f)
1696    }
1697
1698    /// Fire on each viewport-pan event over this piece (docs/canvas.md "Zoom and pan"):
1699    /// trackpad two-finger scroll, two-finger touch pan. `delta` is incremental; apply it
1700    /// as it arrives.
1701    fn on_pan(self, f: impl Fn(Pan) + 'static) -> Decorated<Self> {
1702        Decorated::new(self).on_pan(f)
1703    }
1704
1705    /// The pointer moving over this piece: `Some(point)` in its own coordinates while inside,
1706    /// `None` when it leaves (docs/canvas.md "Interaction"). Pointer-only; wire `on_tap_at`
1707    /// alongside it so a touch-only device can reach the same thing.
1708    fn on_hover(self, f: impl Fn(Option<day_spec::Point>) + 'static) -> Decorated<Self> {
1709        Decorated::new(self).on_hover(f)
1710    }
1711
1712    /// Fill the piece's bounds with a solid color painted behind it: a message-bubble / card /
1713    /// badge surface. Accepts a constant [`Color`], a `Signal<Color>`, or a `Fn() -> Color`; a
1714    /// reactive color repaints the surface when its source changes. Wraps the piece in a native
1715    /// container that carries the fill, so it composes with [`Self::corner_radius`] for a rounded
1716    /// colored surface and with [`Self::padding`] for interior inset.
1717    /// The platform's card around this piece: the grouped-content surface with rounded
1718    /// corners and a `Space::M` inset (see [`Decorated::card`]).
1719    fn card(self) -> Decorated<Self> {
1720        Decorated::new(self).card()
1721    }
1722
1723    fn background<M>(self, color: impl IntoReactive<Color, M>) -> Decorated<Self> {
1724        Decorated::new(self).background(color)
1725    }
1726
1727    /// Round the piece's corners to `radius` points, clipping its background and content to the
1728    /// rounded rectangle. Compose after [`Self::background`] for a rounded colored surface, or use
1729    /// alone to round a clipped child (e.g. an avatar image).
1730    fn corner_radius(self, radius: f64) -> Decorated<Self> {
1731        Decorated::new(self).corner_radius(radius)
1732    }
1733
1734    /// Animate/set the piece's opacity (`0.0` transparent … `1.0` opaque). Wrapped in a native
1735    /// layer so it composes with `.background`; the change animates when made inside
1736    /// [`with_animation`] or under a `.animation` ancestor (§8.4).
1737    fn opacity<M>(self, opacity: impl IntoReactive<f64, M>) -> Decorated<Self> {
1738        Decorated::new(self).opacity(opacity)
1739    }
1740
1741    /// Apply an animatable [`Transform`] (translate/scale/rotate about the center): the cheap
1742    /// movement/scaling channel that never triggers relayout (§8.4). Prefer this over `.offset`
1743    /// for animated motion.
1744    fn transform<M>(self, t: impl IntoReactive<Transform, M>) -> Decorated<Self> {
1745        Decorated::new(self).transform(t)
1746    }
1747
1748    /// Uniformly scale the piece by `factor` about its center (animatable). Convenience over
1749    /// [`Self::transform`].
1750    fn scale<M>(self, factor: impl IntoReactive<f64, M>) -> Decorated<Self> {
1751        Decorated::new(self).scale(factor)
1752    }
1753
1754    /// Rotate the piece by `degrees` clockwise about its center (animatable).
1755    fn rotation<M>(self, degrees: impl IntoReactive<f64, M>) -> Decorated<Self> {
1756        Decorated::new(self).rotation(degrees)
1757    }
1758
1759    /// Translate the piece by (`x`, `y`) points without relayout (animatable): the
1760    /// animation-friendly sibling of `.offset`.
1761    fn translation<Mx, My>(
1762        self,
1763        x: impl IntoReactive<f64, Mx>,
1764        y: impl IntoReactive<f64, My>,
1765    ) -> Decorated<Self> {
1766        Decorated::new(self).translation(x, y)
1767    }
1768
1769    /// Attach an implicit animation (§8.4): changes to this piece's (and its descendants')
1770    /// animatable properties animate with `anim` even outside a [`with_animation`]. SwiftUI's
1771    /// `.animation`. The ambient `with_animation` takes precedence when both apply.
1772    fn animation(self, anim: AnimSpec) -> Decorated<Self> {
1773        Decorated::new(self).animation(anim)
1774    }
1775
1776    /// Apply a [`Modifier`] (or, via the blanket impl, a plain `FnOnce(AnyPiece) -> AnyPiece`
1777    /// closure) to this piece. Pure composition: `content.modifier(m) == m.apply(content.any())`.
1778    ///
1779    /// The one modifier that erases: `Modifier` is defined over [`AnyPiece`], so the piece's own
1780    /// type cannot survive it.
1781    fn modifier(self, m: impl Modifier) -> AnyPiece {
1782        m.apply(self.any())
1783    }
1784
1785    /// Draw `over` on top of this piece, centered, without affecting layout size: a badge /
1786    /// annotation overlay. `self` is the sizing content (bottom of the z-order); `over` is proposed
1787    /// `self`'s size and drawn on top. For an explicit alignment use [`Self::overlay_aligned`]; for
1788    /// a stack that sizes to the union of its children use [`zstack`].
1789    fn overlay(self, over: impl Piece) -> Decorated<Self> {
1790        Decorated::new(self).overlay(over)
1791    }
1792
1793    /// [`Self::overlay`] with an explicit [`Alignment`] for the annotation (e.g. a corner badge with
1794    /// [`Alignment::TopTrailing`]).
1795    fn overlay_aligned(self, align: Alignment, over: impl Piece) -> Decorated<Self> {
1796        Decorated::new(self).overlay_aligned(align, over)
1797    }
1798
1799    /// Constrain this piece to a `width / height` ratio: the largest box of that shape which
1800    /// fits whatever the parent offers (SwiftUI's `.aspectRatio(_:contentMode: .fit)`).
1801    ///
1802    /// Pair it with [`Self::grow_w`] for a piece that takes the width available and derives its
1803    /// height from it: a `canvas` whose drawing has to keep its proportions as the window
1804    /// resizes, say. `image` has carried this since it shipped; this is the same layout, for any
1805    /// piece.
1806    ///
1807    /// A ratio that is not finite and positive describes no box, so it is ignored.
1808    fn aspect_ratio(self, ratio: f64) -> Decorated<Self> {
1809        Decorated::new(self).aspect_ratio(ratio)
1810    }
1811
1812    /// Expand to fill the available space on both axes (a filling pane / card that stretches to
1813    /// its container). Wraps the piece in a layout-only node carrying grow [`Flex`]: the stack
1814    /// offers it the space and it fills; no native backing, so this is a pure layout change.
1815    fn grow(self) -> Decorated<Self> {
1816        Decorated::new(self).grow()
1817    }
1818
1819    /// Expand to fill the available horizontal space.
1820    fn grow_w(self) -> Decorated<Self> {
1821        Decorated::new(self).grow_w()
1822    }
1823
1824    /// Expand to fill the available vertical space.
1825    fn grow_h(self) -> Decorated<Self> {
1826        Decorated::new(self).grow_h()
1827    }
1828
1829    #[doc(hidden)]
1830    /// Span `n` columns (n ≥ 1) of the enclosing [`grid`] (docs/grid.md). Grid modifiers set
1831    /// facts on the node the grid sees: apply them last (outermost), like `.grow_w()`; an
1832    /// outer wrapper would hide the facts from the grid.
1833    fn grid_span(self, n: usize) -> Decorated<Self> {
1834        Decorated::new(self).grid_span(n)
1835    }
1836
1837    /// Override this cell's alignment within its cell rect of the enclosing [`grid`]
1838    /// (docs/grid.md). Apply last (outermost), like [`Decorated::grid_span`].
1839    fn grid_align(self, a: Alignment) -> Decorated<Self> {
1840        Decorated::new(self).grid_align(a)
1841    }
1842
1843    /// While this subtree is mounted, ask the OS to require a second swipe for its edge
1844    /// gestures on `edges` (docs/cover.md), the SwiftUI `defersSystemGestures(on:)`
1845    /// analogue. Put it on a game or drawing surface whose touches run to the screen edge,
1846    /// so a swipe up from the bottom doesn't leave the app. iOS defers the chosen edges'
1847    /// system gestures; Android enters swipe-to-reveal immersive mode while any subtree
1848    /// requests deferral; desktop backends no-op.
1849    fn defers_system_gestures(self, edges: day_spec::Edges) -> Decorated<Self> {
1850        Decorated::new(self).defers_system_gestures(edges)
1851    }
1852
1853    /// While this subtree is mounted and `hidden` is true, hide the system status bar, the
1854    /// SwiftUI `statusBarHidden(_:)` analogue (docs/cover.md). `hidden` is a constant, a
1855    /// `Signal`, or a closure, so a setting can turn it on and off in place. Put it on a page
1856    /// that wants the whole screen (a reader, a game, a photo viewer); the bar returns when
1857    /// the page unmounts. Any mounted request that answers true hides the bar. iOS, Android and
1858    /// HarmonyOS hide it (`Cap::StatusBarHidden`); desktop and web have none and ignore it.
1859    fn status_bar_hidden<M>(self, hidden: impl IntoReactive<bool, M>) -> Decorated<Self> {
1860        Decorated::new(self).status_bar_hidden(hidden)
1861    }
1862
1863    /// While this subtree is mounted, the enclosing [`cover`] (or other modal surface) must
1864    /// not be dismissed interactively, the SwiftUI `interactiveDismissDisabled()` analogue
1865    /// (docs/cover.md). System back / sheet gestures are ignored; only programmatic writes
1866    /// (an explicit close control) dismiss it.
1867    fn interactive_dismiss_disabled(self) -> Decorated<Self> {
1868        Decorated::new(self).interactive_dismiss_disabled()
1869    }
1870
1871    /// Erase to a single [`AnyPiece`]: for a `Vec<AnyPiece>`, a `-> AnyPiece` signature, or any other
1872    /// place one concrete type is required. [`AnyPiece::any`] is inherent and returns `self`, so
1873    /// erasing an already-erased piece costs nothing.
1874    fn any(self) -> AnyPiece {
1875        AnyPiece::new(self)
1876    }
1877}
1878
1879impl<P: Piece> Decorate for P {}
1880
1881/// One a11y string that reads a signal or the locale, with the patch that re-sends it alone.
1882type LiveA11yString = (TextSource, fn(String) -> A11yProps);
1883
1884/// The annotations `.a11y(|a| …)` collects (docs/accessibility.md). The three strings are
1885/// [`TextSource`]s, so each takes a literal, a `String`, a `Signal<String>` or a closure: a
1886/// gauge's value follows the gauge, and a localized label follows the locale.
1887#[derive(Default)]
1888pub struct A11yBuilder {
1889    label: Option<TextSource>,
1890    hint: Option<TextSource>,
1891    value: Option<TextSource>,
1892    /// The selected state, reactive so a tab or row follows the selection.
1893    selected: Option<Reactive<bool>>,
1894    props: A11yProps,
1895}
1896
1897impl A11yBuilder {
1898    /// What the screen reader calls the element.
1899    pub fn label<M>(mut self, s: impl IntoText<M>) -> Self {
1900        self.label = Some(s.into_text());
1901        self
1902    }
1903    /// A longer description of what the element does.
1904    pub fn hint<M>(mut self, s: impl IntoText<M>) -> Self {
1905        self.hint = Some(s.into_text());
1906        self
1907    }
1908    /// The control's current value read aloud by the screen reader (e.g. a `Meter`'s "72%").
1909    /// Reactive like the others: `.value(move || format!("{:.0}%", level.get()))` re-sends the
1910    /// value whenever `level` changes.
1911    pub fn value<M>(mut self, s: impl IntoText<M>) -> Self {
1912        self.value = Some(s.into_text());
1913        self
1914    }
1915    pub fn role(mut self, r: Role) -> Self {
1916        self.props.role = r;
1917        self
1918    }
1919    /// Whether this element is the selected one among its peers (a `Role::Tab`, a row): the
1920    /// platform's selected state, which a screen reader reads as "selected". Reactive like the
1921    /// strings: `.selected(move || current.get() == key)` re-sends it when the selection moves.
1922    pub fn selected<M>(mut self, on: impl IntoReactive<bool, M>) -> Self {
1923        self.selected = Some(on.into_reactive());
1924        self
1925    }
1926    /// Hide this element from assistive tech (still visible on screen), e.g. a redundant chrome
1927    /// element already announced by its labeled sibling.
1928    pub fn hidden(mut self) -> Self {
1929        self.props.hidden = true;
1930        self
1931    }
1932    /// Purely decorative (a background flourish): hidden from assistive tech and, for images,
1933    /// exempt from the "needs a label" lint (§13).
1934    pub fn decorative(mut self) -> Self {
1935        self.props.decorative = true;
1936        self.props.hidden = true;
1937        self
1938    }
1939
1940    /// The annotations as first applied, with every string resolved once, the sources that
1941    /// can change, each paired with the patch that re-sends it, and the selected state when it
1942    /// reads a signal.
1943    fn split(self) -> (A11yProps, Vec<LiveA11yString>, Option<Reactive<bool>>) {
1944        let mut props = self.props;
1945        let mut live = Vec::new();
1946        let selected = match self.selected {
1947            Some(Reactive::Const(on)) => {
1948                props.selected = Some(on);
1949                None
1950            }
1951            Some(dynamic @ Reactive::Dyn(_)) => {
1952                props.selected = Some(day_reactive::untrack(|| dynamic.get()));
1953                Some(dynamic)
1954            }
1955            None => None,
1956        };
1957        let mut take =
1958            |src: Option<TextSource>, slot: &mut Option<String>, patch: fn(String) -> A11yProps| {
1959                if let Some(src) = src {
1960                    *slot = Some(src.initial());
1961                    if matches!(src, TextSource::Dyn(_)) {
1962                        live.push((src, patch));
1963                    }
1964                }
1965            };
1966        take(self.label, &mut props.label, |label| A11yProps {
1967            label: Some(label),
1968            ..Default::default()
1969        });
1970        take(self.hint, &mut props.hint, |hint| A11yProps {
1971            hint: Some(hint),
1972            ..Default::default()
1973        });
1974        take(self.value, &mut props.value, |value| A11yProps {
1975            value: Some(value),
1976            ..Default::default()
1977        });
1978        (props, live, selected)
1979    }
1980}
1981
1982// Native callbacks arrive outside the event dispatcher. Enter their owning scope so
1983// window ambient state resolves correctly, and reject callbacks retained after disposal.
1984fn scoped_drag_source(source: day_spec::transfer::Source) -> day_spec::transfer::Source {
1985    let scope = Scope::current();
1986    let alive = Rc::new(Cell::new(true));
1987    let cleanup = alive.clone();
1988    scope.on_cleanup(move || cleanup.set(false));
1989    Rc::new(move |point| alive.get().then(|| scope.enter(|| source(point))).flatten())
1990}
1991fn scoped_drop_target(target: day_spec::transfer::Target) -> day_spec::transfer::Target {
1992    let scope = Scope::current();
1993    let alive = Rc::new(Cell::new(true));
1994    let cleanup = alive.clone();
1995    scope.on_cleanup(move || cleanup.set(false));
1996    let accept_alive = alive.clone();
1997    day_spec::transfer::Target {
1998        types: target.types,
1999        accept: Rc::new(move |location| {
2000            if accept_alive.get() {
2001                scope.enter(|| (target.accept)(location))
2002            } else {
2003                day_spec::transfer::Operation::None
2004            }
2005        }),
2006        receive: Rc::new(move |drop| alive.get() && scope.enter(|| (target.receive)(drop))),
2007    }
2008}
2009
2010// ---------------------------------------------------------------------------
2011// Conformance cases (docs/testing.md)
2012// ---------------------------------------------------------------------------
2013
2014/// The modifiers' `#[day::test]` cases, next to the `Decorate` methods they prove.
2015#[cfg(feature = "conformance")]
2016pub(crate) mod conformance {
2017    use day_core::AnyPiece;
2018    use day_core::conformance::{Case, Drive, FrameExpect};
2019    use day_reactive::Signal;
2020    use day_spec::{Cap, Color, DragPhase, Transform};
2021
2022    use crate::*;
2023
2024    fn size(width: f64, height: f64) -> FrameExpect {
2025        FrameExpect {
2026            width: Some(width),
2027            height: Some(height),
2028            ..Default::default()
2029        }
2030    }
2031
2032    /// Padding grows the frame by its insets and moves the content in by them.
2033    #[day_macros::test(day_core)]
2034    fn padding_insets() -> Case {
2035        Case::new()
2036            .proves_modifier("padding")
2037            .proves_modifier("wrapper_id")
2038            .page(|| {
2039                // `id` names the rectangle wherever it sits; the padding box is a wrapper, so
2040                // it is named with `wrapper_id`, the one ordered identity modifier.
2041                rectangle()
2042                    .fill(Color::rgb(1.0, 0.0, 0.0))
2043                    .id(day_core::id!(inner))
2044                    .frame(40.0, 20.0)
2045                    .padding(10.0)
2046                    .wrapper_id(day_core::id!(outer))
2047            })
2048            .drive(|d: Drive| async move {
2049                d.assert_frame("outer", size(60.0, 40.0)).await?;
2050                d.assert_frame(
2051                    "inner",
2052                    FrameExpect {
2053                        x: Some(10.0),
2054                        y: Some(10.0),
2055                        relative_to: Some("outer".into()),
2056                        ..Default::default()
2057                    },
2058                )
2059                .await
2060            })
2061    }
2062
2063    /// A minimum width widens a narrow piece; a maximum narrows a wide one.
2064    #[day_macros::test(day_core)]
2065    fn min_max_width() -> Case {
2066        Case::new()
2067            .proves_modifier("min_width")
2068            .proves_modifier("max_width")
2069            .page(|| {
2070                column((
2071                    // The widened box, not the label's own node.
2072                    label("Hi").min_width(120.0).wrapper_id(day_core::id!(min)),
2073                    label("A label long enough that it would run past eighty points")
2074                        .max_width(80.0)
2075                        .wrapper_id(day_core::id!(max)),
2076                ))
2077                .align(HAlign::Leading)
2078            })
2079            .drive(|d: Drive| async move {
2080                d.assert_frame(
2081                    "min",
2082                    FrameExpect {
2083                        width: Some(120.0),
2084                        ..Default::default()
2085                    },
2086                )
2087                .await?;
2088                d.assert_frame(
2089                    "max",
2090                    FrameExpect {
2091                        width: Some(80.0),
2092                        ..Default::default()
2093                    },
2094                )
2095                .await
2096            })
2097    }
2098
2099    /// A maximum height stops a scroll, which takes all the height it is offered, at the cap.
2100    #[day_macros::test(day_core)]
2101    fn max_height_caps_a_scroll() -> Case {
2102        Case::new()
2103            .proves_modifier("max_height")
2104            .page(|| {
2105                column((
2106                    scroll(column((
2107                        label("One"),
2108                        label("Two"),
2109                        label("Three"),
2110                        label("Four"),
2111                        label("Five"),
2112                        label("Six"),
2113                    )))
2114                    .max_height(60.0)
2115                    .wrapper_id(day_core::id!(capped)),
2116                    spacer(),
2117                ))
2118                .align(HAlign::Leading)
2119            })
2120            .drive(|d: Drive| async move {
2121                d.assert_frame(
2122                    "capped",
2123                    FrameExpect {
2124                        height: Some(60.0),
2125                        ..Default::default()
2126                    },
2127                )
2128                .await
2129            })
2130    }
2131
2132    /// An aspect ratio sets the height from the width.
2133    #[day_macros::test(day_core)]
2134    fn aspect_ratio_height() -> Case {
2135        Case::new()
2136            .proves_modifier("aspect_ratio")
2137            .page(|| {
2138                rectangle()
2139                    .fill(Color::rgb(0.0, 0.0, 1.0))
2140                    .aspect_ratio(2.0)
2141                    .id(day_core::id!(shape))
2142                    .width(100.0)
2143            })
2144            .drive(|d: Drive| async move { d.assert_frame("shape", size(100.0, 50.0)).await })
2145    }
2146
2147    /// A growing child takes the row's remaining width.
2148    #[day_macros::test(day_core)]
2149    fn grow_width() -> Case {
2150        Case::new()
2151            .proves_modifier("grow_w")
2152            .page(|| {
2153                row((
2154                    rectangle()
2155                        .fill(Color::rgb(1.0, 0.0, 0.0))
2156                        .frame(40.0, 20.0),
2157                    rectangle()
2158                        .fill(Color::rgb(0.0, 0.0, 1.0))
2159                        .height(20.0)
2160                        .grow_w()
2161                        .id(day_core::id!(grower)),
2162                ))
2163                .spacing(0.0)
2164                .width(200.0)
2165            })
2166            .drive(|d: Drive| async move {
2167                d.assert_frame(
2168                    "grower",
2169                    FrameExpect {
2170                        width: Some(160.0),
2171                        ..Default::default()
2172                    },
2173                )
2174                .await
2175            })
2176    }
2177
2178    /// An aligned overlay draws over its base, in the corner it names.
2179    #[day_macros::test(day_core)]
2180    fn overlay_aligned_corner() -> Case {
2181        Case::new()
2182            .proves_modifier("overlay_aligned")
2183            .requires(Cap::Snapshot)
2184            .page(|| {
2185                rectangle()
2186                    .fill(Color::rgb(1.0, 0.0, 0.0))
2187                    .frame(100.0, 60.0)
2188                    .overlay_aligned(
2189                        day_core::Alignment::BottomTrailing,
2190                        rectangle()
2191                            .fill(Color::rgb(0.0, 0.0, 1.0))
2192                            .frame(20.0, 20.0),
2193                    )
2194                    .id(day_core::id!(base))
2195            })
2196            .drive(|d: Drive| async move {
2197                d.sample_pixel("base", 0.95, 0.9, "#0000ff").await?;
2198                d.sample_pixel("base", 0.1, 0.1, "#ff0000").await
2199            })
2200    }
2201
2202    /// A background fills the piece's frame.
2203    #[day_macros::test(day_core)]
2204    fn background_fill() -> Case {
2205        Case::new()
2206            .proves_modifier("background")
2207            .requires(Cap::Snapshot)
2208            .page(|| {
2209                label("")
2210                    .frame(80.0, 40.0)
2211                    .background(Color::rgb(1.0, 0.0, 0.0))
2212                    .id(day_core::id!(filled))
2213            })
2214            .drive(|d: Drive| async move { d.sample_pixel("filled", 0.5, 0.5, "#ff0000").await })
2215    }
2216
2217    /// A corner radius cuts a background's corners and keeps its middle.
2218    #[day_macros::test(day_core)]
2219    fn corner_radius_cuts() -> Case {
2220        Case::new()
2221            .proves_modifier("corner_radius")
2222            .requires(Cap::Snapshot)
2223            .page(|| {
2224                zstack((
2225                    rectangle().fill(Color::WHITE),
2226                    label("")
2227                        .frame(80.0, 80.0)
2228                        .background(Color::rgb(0.0, 0.0, 1.0))
2229                        .corner_radius(30.0),
2230                ))
2231                .id(day_core::id!(ground))
2232                .frame(80.0, 80.0)
2233            })
2234            .drive(|d: Drive| async move {
2235                d.sample_pixel("ground", 0.5, 0.5, "#0000ff").await?;
2236                d.sample_pixel("ground", 0.03, 0.03, "#ffffff").await
2237            })
2238    }
2239
2240    /// Opacity zero leaves what is below showing.
2241    #[day_macros::test(day_core)]
2242    fn opacity_hides() -> Case {
2243        Case::new()
2244            .proves_duty("set_opacity")
2245            .proves_modifier("opacity")
2246            .requires(Cap::Snapshot)
2247            .page(|| {
2248                zstack((
2249                    rectangle().fill(Color::WHITE),
2250                    rectangle().fill(Color::rgb(1.0, 0.0, 0.0)).opacity(0.0),
2251                ))
2252                .id(day_core::id!(ground))
2253                .frame(80.0, 40.0)
2254            })
2255            .drive(|d: Drive| async move { d.sample_pixel("ground", 0.5, 0.5, "#ffffff").await })
2256    }
2257
2258    /// A translation moves what is drawn without moving the layout.
2259    #[day_macros::test(day_core)]
2260    fn translation_moves_drawing() -> Case {
2261        Case::new()
2262            .proves_modifier("translation")
2263            .requires(Cap::Snapshot)
2264            .page(|| {
2265                zstack((
2266                    rectangle().fill(Color::WHITE),
2267                    rectangle()
2268                        .fill(Color::rgb(0.0, 0.0, 1.0))
2269                        .frame(40.0, 40.0)
2270                        .translation(40.0, 0.0),
2271                ))
2272                .align(day_core::Alignment::Leading)
2273                .id(day_core::id!(ground))
2274                .frame(120.0, 40.0)
2275            })
2276            .drive(|d: Drive| async move {
2277                d.sample_pixel("ground", 0.5, 0.5, "#0000ff").await?;
2278                d.sample_pixel("ground", 0.1, 0.5, "#ffffff").await
2279            })
2280    }
2281
2282    /// A translation moves a plain view's drawing too, not only a canvas's.
2283    #[day_macros::test(day_core)]
2284    fn translation_moves_view() -> Case {
2285        Case::new()
2286            .proves_modifier("translation")
2287            .requires(Cap::Snapshot)
2288            .page(|| {
2289                zstack((
2290                    rectangle().fill(Color::WHITE),
2291                    label("")
2292                        .frame(40.0, 40.0)
2293                        .background(Color::rgb(0.0, 0.0, 1.0))
2294                        .translation(40.0, 0.0),
2295                ))
2296                .align(day_core::Alignment::Leading)
2297                .id(day_core::id!(ground))
2298                .frame(120.0, 40.0)
2299            })
2300            .drive(|d: Drive| async move {
2301                d.sample_pixel("ground", 0.5, 0.5, "#0000ff").await?;
2302                d.sample_pixel("ground", 0.1, 0.5, "#ffffff").await
2303            })
2304    }
2305
2306    /// A scale grows the drawing about its center without moving the layout.
2307    #[day_macros::test(day_core)]
2308    fn scale_grows_drawing() -> Case {
2309        Case::new()
2310            .proves_modifier("scale")
2311            .requires(Cap::Snapshot)
2312            .page(|| {
2313                zstack((
2314                    rectangle().fill(Color::WHITE),
2315                    label("")
2316                        .frame(40.0, 40.0)
2317                        .background(Color::rgb(0.0, 0.0, 1.0))
2318                        .scale(2.0),
2319                ))
2320                .id(day_core::id!(ground))
2321                .frame(120.0, 80.0)
2322            })
2323            .drive(|d: Drive| async move {
2324                // Unscaled, the square spans x 40..80 of 120; scaled twice it spans 20..100.
2325                d.sample_pixel("ground", 0.25, 0.5, "#0000ff").await?;
2326                d.sample_pixel("ground", 0.08, 0.5, "#ffffff").await
2327            })
2328    }
2329
2330    /// A rotation turns the drawing clockwise (y points down): a bar red on the left and blue
2331    /// on the right, turned 90°, is red on top and blue below.
2332    #[day_macros::test(day_core)]
2333    fn rotation_turns_clockwise() -> Case {
2334        Case::new()
2335            .proves_modifier("rotation")
2336            .requires(Cap::Snapshot)
2337            .page(|| {
2338                zstack((
2339                    rectangle().fill(Color::WHITE),
2340                    shape_group([
2341                        rectangle()
2342                            .fill(Color::rgb(1.0, 0.0, 0.0))
2343                            .at(0.0, 0.0, 0.5, 1.0),
2344                        rectangle()
2345                            .fill(Color::rgb(0.0, 0.0, 1.0))
2346                            .at(0.5, 0.0, 0.5, 1.0),
2347                    ])
2348                    .frame(80.0, 40.0)
2349                    .rotation(90.0),
2350                ))
2351                .id(day_core::id!(ground))
2352                .frame(120.0, 120.0)
2353            })
2354            .shot("default")
2355            .drive(|d: Drive| async move {
2356                d.sample_pixel("ground", 0.5, 0.4, "#ff0000").await?;
2357                d.sample_pixel("ground", 0.5, 0.6, "#0000ff").await
2358            })
2359    }
2360
2361    // ---- input: every op injects Day's own event, the stream a native recognizer delivers,
2362    // so these prove the routing and the handler, not that the platform's recognizer fires
2363    // (docs/testing.md "Known gaps").
2364
2365    /// A plain target for gestures: a filled rectangle of a known size.
2366    fn pad(id: day_core::ElementId) -> Decorated<ShapePiece> {
2367        rectangle()
2368            .fill(Color::rgb(0.2, 0.4, 0.9))
2369            .frame(120.0, 80.0)
2370            .id(id)
2371    }
2372
2373    /// A tap runs `.on_tap`'s work, once per tap.
2374    #[day_macros::test(day_core)]
2375    fn on_tap_runs() -> Case {
2376        Case::new()
2377            .proves_modifier("on_tap")
2378            .page(|| {
2379                let taps = Signal::new(0i64);
2380                column((
2381                    label(move || format!("taps {}", taps.get())).id(day_core::id!(taps)),
2382                    label("Tap me")
2383                        .padding(12.0)
2384                        .on_tap(move || taps.update(|n| *n += 1))
2385                        .id(day_core::id!(target)),
2386                ))
2387            })
2388            .drive(|d: Drive| async move {
2389                d.tap("target").await?;
2390                d.tap("target").await?;
2391                d.assert_text("taps", "taps 2").await
2392            })
2393    }
2394
2395    /// `.on_tap_at` hears where in the element the tap landed.
2396    #[day_macros::test(day_core)]
2397    fn on_tap_at_reports_point() -> Case {
2398        Case::new()
2399            .proves_modifier("on_tap_at")
2400            .page(|| {
2401                let at = Signal::new(String::from("none"));
2402                column((
2403                    label(move || format!("at {}", at.get())).id(day_core::id!(at)),
2404                    rectangle()
2405                        .fill(Color::rgb(0.2, 0.4, 0.9))
2406                        .frame(120.0, 80.0)
2407                        .on_tap_at(move |p| at.set(format!("{:.0},{:.0}", p.x, p.y)))
2408                        .id(day_core::id!(target)),
2409                ))
2410            })
2411            .drive(|d: Drive| async move {
2412                d.tap_at("target", 30.0, 20.0).await?;
2413                d.assert_text("at", "at 30,20").await
2414            })
2415    }
2416
2417    /// A drag reports its phases, and its translation from where it began.
2418    #[day_macros::test(day_core)]
2419    fn on_drag_reports_translation() -> Case {
2420        Case::new()
2421            .proves_duty("enable_gesture")
2422            .proves_modifier("on_drag")
2423            .page(|| {
2424                let state = Signal::new(String::from("idle"));
2425                column((
2426                    label(move || state.get()).id(day_core::id!(state)),
2427                    pad(day_core::id!(target)).on_drag(move |d: Drag| {
2428                        state.set(match d.phase {
2429                            DragPhase::Began => "began".into(),
2430                            DragPhase::Changed => "moving".into(),
2431                            DragPhase::Ended => {
2432                                format!("moved {:.0},{:.0}", d.translation.x, d.translation.y)
2433                            }
2434                        })
2435                    }),
2436                ))
2437            })
2438            .drive(|d: Drive| async move {
2439                d.drag("target", (10.0, 10.0), (50.0, 30.0)).await?;
2440                d.assert_text("state", "moved 40,20").await
2441            })
2442    }
2443
2444    /// Hover reports the pointer's place while it is over the element, and `None` on leaving.
2445    #[day_macros::test(day_core)]
2446    fn on_hover_enters_and_leaves() -> Case {
2447        Case::new()
2448            .proves_modifier("on_hover")
2449            .page(|| {
2450                let state = Signal::new(String::from("out"));
2451                column((
2452                    label(move || state.get()).id(day_core::id!(state)),
2453                    pad(day_core::id!(target)).on_hover(move |p| {
2454                        state.set(match p {
2455                            Some(p) => format!("over {:.0},{:.0}", p.x, p.y),
2456                            None => "out".into(),
2457                        })
2458                    }),
2459                ))
2460            })
2461            .drive(|d: Drive| async move {
2462                d.hover("target", Some((5.0, 6.0))).await?;
2463                d.assert_text("state", "over 5,6").await?;
2464                d.hover_leave("target").await?;
2465                d.assert_text("state", "out").await
2466            })
2467    }
2468
2469    /// A pan delivers incremental deltas that add up to the whole movement.
2470    #[day_macros::test(day_core)]
2471    fn on_pan_adds_deltas() -> Case {
2472        Case::new()
2473            .proves_modifier("on_pan")
2474            .page(|| {
2475                let total = Signal::new((0.0f64, 0.0f64));
2476                column((
2477                    label(move || {
2478                        let (x, y) = total.get();
2479                        format!("pan {x:.0},{y:.0}")
2480                    })
2481                    .id(day_core::id!(total)),
2482                    pad(day_core::id!(target)).on_pan(move |p: Pan| {
2483                        total.update(|(x, y)| {
2484                            *x += p.delta.x;
2485                            *y += p.delta.y;
2486                        })
2487                    }),
2488                ))
2489            })
2490            .drive(|d: Drive| async move {
2491                d.pan("target", 40.0, -20.0).await?;
2492                d.assert_text("total", "pan 40,-20").await
2493            })
2494    }
2495
2496    /// A pinch's scale is cumulative from where it began; its end carries the final scale.
2497    #[day_macros::test(day_core)]
2498    fn on_pinch_reports_scale() -> Case {
2499        Case::new()
2500            .proves_modifier("on_pinch")
2501            .page(|| {
2502                let scale = Signal::new(String::from("none"));
2503                column((
2504                    label(move || format!("scale {}", scale.get())).id(day_core::id!(scale)),
2505                    pad(day_core::id!(target)).on_pinch(move |p: Pinch| {
2506                        if p.phase == DragPhase::Ended {
2507                            scale.set(format!("{:.1}", p.scale));
2508                        }
2509                    }),
2510                ))
2511            })
2512            .drive(|d: Drive| async move {
2513                d.pinch("target", 2.5).await?;
2514                d.assert_text("scale", "scale 2.5").await
2515            })
2516    }
2517
2518    /// A focusable element takes focus, and the keys pressed while it holds it reach `.on_key`.
2519    #[day_macros::test(day_core)]
2520    fn on_key_reaches_focused() -> Case {
2521        Case::new()
2522            .proves_duty("focus")
2523            .proves_duty("set_focusable")
2524            .proves_modifier("on_key")
2525            .proves_modifier("focusable")
2526            .page(|| {
2527                let last = Signal::new(String::from("none"));
2528                column((
2529                    label(move || format!("key {}", last.get())).id(day_core::id!(last)),
2530                    pad(day_core::id!(target))
2531                        .focusable()
2532                        .on_key(move |k| last.set(k.key.clone())),
2533                ))
2534            })
2535            .drive(|d: Drive| async move {
2536                // Focus, then a key to whatever holds focus: the route a real press takes.
2537                d.focus("target").await?;
2538                d.assert_focused("target", true).await?;
2539                d.key(None, "ArrowRight").await?;
2540                d.assert_text("last", "key ArrowRight").await
2541            })
2542    }
2543
2544    /// A focus binding moves focus when the app sets it, and follows the field when it is
2545    /// focused natively.
2546    #[day_macros::test(day_core)]
2547    fn focused_binding_moves_focus() -> Case {
2548        Case::new()
2549            .proves_modifier("focused")
2550            .page(|| {
2551                let want = Signal::new(false);
2552                column((
2553                    label(move || format!("focused {}", want.get())).id(day_core::id!(state)),
2554                    button("Focus")
2555                        .action(move || want.set(true))
2556                        .id(day_core::id!(go)),
2557                    text_field(Signal::new(String::new()))
2558                        .focused(want)
2559                        .id(day_core::id!(field)),
2560                ))
2561            })
2562            .drive(|d: Drive| async move {
2563                d.assert_focused("field", false).await?;
2564                d.tap("go").await?;
2565                d.assert_focused("field", true).await?;
2566                d.assert_text("state", "focused true").await
2567            })
2568    }
2569
2570    /// Destroy a focused input and its containing page without first moving focus to a
2571    /// button. Win32 GTK must detach the IME filter before the input loses its native root.
2572    #[day_macros::test(day_core)]
2573    fn focused_inputs_survive_page_removal() -> Case {
2574        Case::new()
2575            .proves_duty("focus")
2576            .page(|| {
2577                let shown = Signal::new(true);
2578                column((
2579                    button("Toggle page")
2580                        .action(move || shown.set(!shown.get()))
2581                        .id(day_core::id!(toggle)),
2582                    text_field(Signal::new(String::new())).id(day_core::id!(survivor)),
2583                    when(
2584                        move || shown.get(),
2585                        || {
2586                            column((
2587                                text_field(Signal::new(String::new())).id(day_core::id!(entry)),
2588                            ))
2589                            .background(Color::rgb(0.5, 0.5, 0.5))
2590                        },
2591                    ),
2592                ))
2593            })
2594            .drive(|d: Drive| async move {
2595                for _ in 0..4 {
2596                    d.focus("entry").await?;
2597                    d.assert_focused("entry", true).await?;
2598                    // Scripted activation doesn't move native focus to the button first.
2599                    d.tap("toggle").await?;
2600                    d.assert_missing("entry").await?;
2601                    d.wait_idle().await?;
2602                    d.tap("toggle").await?;
2603                }
2604                d.focus("survivor").await?;
2605                d.tap("toggle").await?;
2606                d.assert_focused("survivor", true).await?;
2607                d.input("survivor", "still editable").await?;
2608                d.assert_text("survivor", "still editable").await
2609            })
2610    }
2611
2612    // ---- layout and the remaining modifiers -------------------------------------------------
2613
2614    /// `.height` fixes the height and leaves the width to the layout.
2615    #[day_macros::test(day_core)]
2616    fn height_fixes_height() -> Case {
2617        Case::new()
2618            .proves_modifier("height")
2619            .page(|| {
2620                column((rectangle()
2621                    .fill(Color::rgb(1.0, 0.0, 0.0))
2622                    .id(day_core::id!(bar))
2623                    .height(37.0),))
2624                .width(100.0)
2625            })
2626            .drive(|d: Drive| async move {
2627                d.assert_frame(
2628                    "bar",
2629                    FrameExpect {
2630                        height: Some(37.0),
2631                        ..Default::default()
2632                    },
2633                )
2634                .await
2635            })
2636    }
2637
2638    /// `.grow_h` takes the height its column has left, in a column and in a row alike.
2639    #[day_macros::test(day_core)]
2640    fn grow_vertical_fills_column() -> Case {
2641        Case::new()
2642            .proves_modifier("grow_h")
2643            .page(|| {
2644                row((
2645                    column((
2646                        rectangle()
2647                            .fill(Color::rgb(1.0, 0.0, 0.0))
2648                            .id(day_core::id!(top))
2649                            .frame(40.0, 20.0),
2650                        rectangle()
2651                            .fill(Color::rgb(0.0, 0.0, 1.0))
2652                            .id(day_core::id!(rest))
2653                            .width(40.0)
2654                            .grow_h(),
2655                    ))
2656                    .spacing(0.0)
2657                    .height(120.0),
2658                    rectangle()
2659                        .fill(Color::rgb(0.0, 1.0, 0.0))
2660                        .id(day_core::id!(both))
2661                        .width(30.0)
2662                        .grow_h(),
2663                ))
2664                .spacing(0.0)
2665                .align(VAlign::Top)
2666                .height(120.0)
2667            })
2668            .drive(|d: Drive| async move {
2669                d.assert_frame(
2670                    "rest",
2671                    FrameExpect {
2672                        height: Some(100.0),
2673                        ..Default::default()
2674                    },
2675                )
2676                .await?;
2677                d.assert_frame(
2678                    "both",
2679                    FrameExpect {
2680                        height: Some(120.0),
2681                        ..Default::default()
2682                    },
2683                )
2684                .await
2685            })
2686    }
2687
2688    /// A grid cell spans the columns it asks for, and aligns within its cell as asked.
2689    #[day_macros::test(day_core)]
2690    fn grid_span_and_align() -> Case {
2691        Case::new()
2692            .proves_modifier("grid_span")
2693            .proves_modifier("grid_align")
2694            .page(|| {
2695                let cell = |id: day_core::ElementId, w: f64| {
2696                    rectangle()
2697                        .fill(Color::rgb(0.2, 0.4, 0.9))
2698                        .id(id)
2699                        .frame(w, 20.0)
2700                };
2701                grid((
2702                    grid_row((cell(day_core::id!(a), 40.0), cell(day_core::id!(b), 60.0))),
2703                    grid_row((cell(day_core::id!(wide), 30.0)
2704                        .grid_align(day_core::Alignment::TopTrailing)
2705                        .grid_span(2),)),
2706                ))
2707                .spacing(0.0)
2708                .align(day_core::Alignment::TopLeading)
2709            })
2710            .drive(|d: Drive| async move {
2711                // The spanning cell's rect is both columns, 100 wide; trailing puts its
2712                // 30-wide content at x 70 from the first column's start.
2713                d.assert_frame(
2714                    "wide",
2715                    FrameExpect {
2716                        x: Some(70.0),
2717                        relative_to: Some("a".into()),
2718                        ..Default::default()
2719                    },
2720                )
2721                .await
2722            })
2723    }
2724
2725    /// `.overlay` draws its piece centered over the content.
2726    #[day_macros::test(day_core)]
2727    fn overlay_centers() -> Case {
2728        Case::new()
2729            .proves_modifier("overlay")
2730            .requires(Cap::Snapshot)
2731            .page(|| {
2732                rectangle()
2733                    .fill(Color::rgb(1.0, 0.0, 0.0))
2734                    .frame(90.0, 90.0)
2735                    .overlay(
2736                        rectangle()
2737                            .fill(Color::rgb(0.0, 0.0, 1.0))
2738                            .frame(30.0, 30.0),
2739                    )
2740                    .id(day_core::id!(base))
2741            })
2742            .drive(|d: Drive| async move {
2743                d.sample_pixel("base", 0.5, 0.5, "#0000ff").await?;
2744                d.sample_pixel("base", 0.1, 0.1, "#ff0000").await
2745            })
2746    }
2747
2748    /// `.transform` takes a whole transform: here a translation and a scale together.
2749    #[day_macros::test(day_core)]
2750    fn transform_applies_whole() -> Case {
2751        Case::new()
2752            .proves_duty("set_transform")
2753            .proves_modifier("transform")
2754            .requires(Cap::Snapshot)
2755            .page(|| {
2756                zstack((
2757                    rectangle().fill(Color::WHITE),
2758                    label("")
2759                        .frame(20.0, 20.0)
2760                        .background(Color::rgb(0.0, 0.0, 1.0))
2761                        .transform(Transform {
2762                            tx: 30.0,
2763                            sx: 2.0,
2764                            sy: 2.0,
2765                            ..Transform::default()
2766                        }),
2767                ))
2768                .id(day_core::id!(ground))
2769                .frame(120.0, 60.0)
2770            })
2771            .drive(|d: Drive| async move {
2772                // Centered at x 60, moved to 90, scaled to 40 wide: 70..110 of 120.
2773                d.sample_pixel("ground", 0.75, 0.5, "#0000ff").await?;
2774                d.sample_pixel("ground", 0.5, 0.5, "#ffffff").await
2775            })
2776    }
2777
2778    /// An implicit animation still lands on the final value: a square slid across by an
2779    /// animated translation ends where the translation says.
2780    #[day_macros::test(day_core)]
2781    fn animation_lands_on_final() -> Case {
2782        Case::new()
2783            .proves_modifier("animation")
2784            .requires(Cap::Snapshot)
2785            .page(|| {
2786                let moved = Signal::new(false);
2787                column((
2788                    button("Move")
2789                        .action(move || moved.set(true))
2790                        .id(day_core::id!(move_button)),
2791                    zstack((
2792                        rectangle().fill(Color::WHITE),
2793                        label("")
2794                            .frame(40.0, 40.0)
2795                            .background(Color::rgb(0.0, 0.0, 1.0))
2796                            .translation(move || if moved.get() { 80.0 } else { 0.0 }, 0.0)
2797                            .animation(day_spec::AnimSpec::linear(150)),
2798                    ))
2799                    .align(day_core::Alignment::Leading)
2800                    .id(day_core::id!(ground))
2801                    .frame(120.0, 40.0),
2802                ))
2803            })
2804            .drive(|d: Drive| async move {
2805                d.sample_pixel("ground", 0.1, 0.5, "#0000ff").await?;
2806                d.tap("move_button").await?;
2807                d.wait_idle().await?;
2808                d.pause(0.4).await?;
2809                d.sample_pixel("ground", 0.85, 0.5, "#0000ff").await?;
2810                d.sample_pixel("ground", 0.1, 0.5, "#ffffff").await
2811            })
2812    }
2813
2814    /// Each way of naming an element finds it: a fixed id, a derived one, a keyed one.
2815    #[day_macros::test(day_core)]
2816    fn ids_name_elements() -> Case {
2817        Case::new()
2818            .proves_modifier("id")
2819            .proves_modifier("id_of")
2820            .page(|| {
2821                let n = Signal::new(1i64);
2822                column((
2823                    label("Fixed").id(day_core::id!(fixed)),
2824                    label("Derived").id_of(move || day_core::id!(derived, n.get())),
2825                    label("Keyed").id(day_core::id!(keyed, 7)),
2826                    button("Next")
2827                        .action(move || n.set(2))
2828                        .id(day_core::id!(next)),
2829                ))
2830            })
2831            .drive(|d: Drive| async move {
2832                d.assert_text("fixed", "Fixed").await?;
2833                d.assert_text("keyed:7", "Keyed").await?;
2834                d.assert_text("derived:1", "Derived").await?;
2835                d.tap("next").await?;
2836                d.assert_missing("derived:1").await?;
2837                d.assert_text("derived:2", "Derived").await
2838            })
2839    }
2840
2841    /// `.tweak` runs with the realized node, and `.native_ref` holds it while the piece lives.
2842    #[day_macros::test(day_core)]
2843    fn tweak_and_native_ref_see_node() -> Case {
2844        Case::new()
2845            .proves_modifier("tweak")
2846            .proves_modifier("native_ref")
2847            .page(|| {
2848                let tweaked = Signal::new(false);
2849                let r = NativeRef::new();
2850                let held = r.clone();
2851                column((
2852                    label(move || format!("tweaked {}", tweaked.get())).id(day_core::id!(tweaked)),
2853                    label(move || format!("held {}", held.node().is_some()))
2854                        .id(day_core::id!(held)),
2855                    label("Target")
2856                        .tweak(move |_node| tweaked.set(true))
2857                        .native_ref(&r),
2858                ))
2859            })
2860            .drive(|d: Drive| async move {
2861                d.assert_text("tweaked", "tweaked true").await?;
2862                d.assert_text("held", "held true").await
2863            })
2864    }
2865
2866    /// `.modifier` applies a reusable modifier: here, a closure that pads.
2867    #[day_macros::test(day_core)]
2868    fn modifier_applies() -> Case {
2869        Case::new()
2870            .proves_modifier("modifier")
2871            .page(|| {
2872                let padded = |content: AnyPiece| content.padding(10.0).any();
2873                column((label("Inner")
2874                    .id(day_core::id!(inner))
2875                    .modifier(padded)
2876                    .id(day_core::id!(outer)),))
2877                .align(HAlign::Leading)
2878            })
2879            .drive(|d: Drive| async move {
2880                d.assert_frame(
2881                    "inner",
2882                    FrameExpect {
2883                        x: Some(10.0),
2884                        y: Some(10.0),
2885                        relative_to: Some("outer".into()),
2886                        ..Default::default()
2887                    },
2888                )
2889                .await
2890            })
2891    }
2892
2893    /// An announcement reaches the screen reader (the toolkit's `announce`) and the run.
2894    #[day_macros::test(day_core)]
2895    fn announce_reaches_reader() -> Case {
2896        Case::new()
2897            .proves_duty("announce")
2898            .proves_cap(Cap::Announce)
2899            .page(|| {
2900                button("Save")
2901                    .action(|| day_core::announce("Saved"))
2902                    .id(day_core::id!(save))
2903            })
2904            .drive(|d: Drive| async move {
2905                d.tap("save").await?;
2906                d.assert_announced("Saved").await
2907            })
2908    }
2909}
2910
2911#[cfg(test)]
2912mod transfer_tests {
2913    use super::*;
2914    use day_spec::transfer::*;
2915
2916    #[test]
2917    fn transfer_callbacks_restore_owner_scope_and_reject_after_disposal() {
2918        let owner = Scope::child();
2919        let foreign = Scope::child();
2920        foreign.provide(22u32);
2921        let (source, target) = owner.enter(|| {
2922            owner.provide(11u32);
2923            let check = || assert_eq!(day_core::environment::<u32>(), Some(11));
2924            (
2925                scoped_drag_source(Rc::new(move |_| {
2926                    check();
2927                    Some(Offer::default())
2928                })),
2929                scoped_drop_target(Target {
2930                    types: vec![],
2931                    accept: Rc::new(move |_| {
2932                        check();
2933                        Operation::Copy
2934                    }),
2935                    receive: Rc::new(move |_| {
2936                        check();
2937                        true
2938                    }),
2939                }),
2940            )
2941        });
2942        let location = || Location {
2943            position: day_spec::Point::new(40., 20.),
2944            types: vec![],
2945            allowed: vec![Operation::Copy],
2946            local: true,
2947        };
2948        foreign.enter(|| {
2949            assert!(source(day_spec::Point::new(0., 0.)).is_some());
2950            assert!(target.deliver(location(), Offer::default()));
2951            assert_eq!(day_core::environment::<u32>(), Some(22));
2952            owner.dispose();
2953            assert!(source(day_spec::Point::new(0., 0.)).is_none());
2954            assert_eq!(target.proposal(&location()), Operation::None);
2955            assert!(!(target.receive)(Drop {
2956                local: true,
2957                position: day_spec::Point::new(0., 0.),
2958                operation: Operation::Copy,
2959                items: vec![]
2960            }));
2961        });
2962        foreign.dispose();
2963    }
2964}