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}