Skip to main content

day_core/
tree.rs

1// Copyright © The Daybrite Project
2// SPDX-License-Identifier: MPL-2.0
3
4//! The realized tree: nodes own native handles (or are layout-only), a reactive scope, and
5//! layout state. One `Tree<B>` per process, installed thread-local; bindings and event
6//! handlers reach it through [`with_tree`], and tree methods never run user code, so the
7//! single-borrow discipline holds (§3.3, §8.3).
8
9use std::any::Any;
10use std::cell::{Cell, RefCell};
11use std::collections::{HashMap, VecDeque};
12use std::rc::Rc;
13
14use day_reactive::Scope;
15use day_spec::*;
16use slotmap::{Key, KeyData, SlotMap, new_key_type};
17
18use crate::layout::{Alignment, CrossAlign, Layout, PassThrough};
19
20new_key_type! {
21    /// Realized-node key. `NodeId` (the spec-boundary id) is its FFI encoding.
22    pub struct RNode;
23}
24
25pub fn rnode_to_id(n: RNode) -> NodeId {
26    NodeId(n.data().as_ffi())
27}
28pub fn id_to_rnode(id: NodeId) -> RNode {
29    RNode::from(KeyData::from_ffi(id.0))
30}
31
32/// Read-only layout facts a parent may consult about a child (§7.2 ChildRef).
33#[derive(Clone, Copy, Debug, Default, PartialEq)]
34pub struct Flex {
35    /// Wants to fill the horizontal / vertical axis when offered space.
36    pub grow_w: bool,
37    pub grow_h: bool,
38    /// Sized after its rigid siblings in a column, against the height they leave (a
39    /// `.max_height` cap): it gives way to them on a short screen, but unlike `grow_h` it never
40    /// makes the column fill what it is offered.
41    pub yields_h: bool,
42    /// Takes all remaining main-axis space in a stack.
43    pub is_spacer: bool,
44    /// Layout-transparent group (`when`/`each` anchors): stacks lay out its children inline.
45    pub is_group: bool,
46    /// Grid facts (docs/grid.md): consulted only by `GridLayout`; inert everywhere else.
47    pub grid: GridFacts,
48    /// The node is the platform's whole list row (a native `kinds::LIST_ITEM`): it sizes the
49    /// row's height itself, so a list cell gives it only the side margins (docs/list.md "Row
50    /// metrics").
51    pub whole_row: bool,
52}
53
54/// Per-node grid facts (docs/grid.md), carried on [`Flex`]: the shipped form of the §7.2
55/// ChildRef facts surface. Set at build time by `grid_row` and the `.grid_span`/`.grid_align`
56/// modifiers; only `GridLayout` reads them.
57#[derive(Clone, Copy, Debug, Default, PartialEq)]
58pub struct GridFacts {
59    /// The node is a `grid_row` carrier: its children are that row's cells.
60    pub is_row: bool,
61    /// Row-only: vertical alignment override for the row's cells.
62    pub row_valign: Option<CrossAlign>,
63    /// Cell-only: columns spanned (0 = unset = 1).
64    pub col_span: u16,
65    /// Cell-only: alignment override within the cell rect.
66    pub align: Option<Alignment>,
67}
68
69/// Cached last-applied props for the dayscript element index (§14.2).
70#[derive(Clone, Debug, Default)]
71pub struct NodeProbe {
72    pub text: String,
73    pub value: f64,
74    pub flag: bool,
75    pub selected: i64,
76    /// A picker's option labels, so `text` can follow the selection through either patch:
77    /// the label is what the control shows, and the index alone cannot reconstruct it.
78    pub options: Vec<String>,
79    pub enabled: bool,
80    /// Native keyboard focus, mirrored from `Event::FocusChanged` (docs/focus.md).
81    pub focused: bool,
82}
83
84pub struct NodeData<H> {
85    pub kind: PieceKind,
86    pub handle: Option<H>,
87    pub parent: RNode,
88    pub children: Vec<RNode>,
89    pub layout: Rc<dyn Layout>,
90    pub flex: Flex,
91    pub scope: Scope,
92    pub id: Option<String>,
93    /// The id a recycled cell's node carried before subtree recycling parked it
94    /// (docs/list.md). A pooled row must stop answering lookups while it is hidden, but its
95    /// next bind may show the same row content again (a slot write of an unchanged value,
96    /// which fires no reactive `id_of` and re-runs no static `.id()`), so the parked id is what
97    /// `restore_subtree_ids` hands back on rebind.
98    pub parked_id: Option<String>,
99    /// Accumulated accessibility annotations (§13): merged from the piece default, `.a11y()`,
100    /// and `.id()`. Stored so each `set_a11y` re-applies the full picture and `a11y_audit`
101    /// (§14.2) can diff the native tree against Day's own expectation.
102    pub a11y: day_spec::A11yProps,
103    // --- layout state (§7.4) ---
104    pub cache: Vec<((u64, u64), Size)>,
105    /// The last `(size, first baseline)` this node answered (docs/baseline.md). One slot, not a
106    /// list: a baseline is asked for at the size the row already settled on, so the query
107    /// repeats at one size per pass. Invalidated with `needs_measure`, alongside `cache`.
108    pub baseline_cache: Option<((u64, u64), Option<f64>)>,
109    pub probe: NodeProbe,
110    pub needs_measure: bool,
111    /// Placement dirt reaches the root even through measure boundaries. Recycled cell
112    /// subtrees are detached from the window tree, so their sweep must see this bit.
113    pub needs_layout: bool,
114    pub last_native_frame: Option<Rect>,
115    pub is_boundary: bool,
116    /// Scroll-content size reported by `ScrollLayout` (§7.6); scroll nodes only. Cached so
117    /// `scroll_to_target` can compose edge targets (bottom = content minus viewport).
118    pub scroll_content: Option<Size>,
119    /// Node-scoped implicit animation (`.animation(anim)`, §8.4): when set, this node's property
120    /// patches and frame changes animate even outside a `with_animation`. The ambient animation
121    /// (`with_animation`) takes precedence when both are present.
122    pub implicit_anim: Option<day_spec::AnimSpec>,
123    /// A `.tweak` closure (or a per-toolkit ext modifier, which routes through it) ran against
124    /// this node's handle (docs/tweaks.md). Read by `set_node_selectable`: a backing swap would
125    /// discard that work, which warrants a loud warning rather than silent loss.
126    pub tweaked: bool,
127}
128
129/// An event handler registered on a realized node.
130pub type EventHandler = Rc<dyn Fn(&Event)>;
131
132/// The recording/telemetry observer installed via [`set_event_observer`] (§14.6): called with
133/// every dispatched `(NodeId, Event)`, in queue order, before the app receives it.
134pub type EventObserver = dyn Fn(NodeId, &Event);
135
136/// `TreeOps::open_window_root`'s answer: the tree-level face of
137/// [`day_spec::WindowOpenReply`], carrying the adopted (or parked) root node.
138#[derive(Clone, Copy, PartialEq, Eq, Debug)]
139pub enum WindowRootReply {
140    /// The native window exists; `root` is its adopted boundary root, laid out already.
141    Open(RNode),
142    /// Native creation is in flight; `root` is parked, with no handle and no layout, until
143    /// `TreeOps::adopt_window_root` completes it.
144    Pending(RNode),
145    /// The toolkit cannot open windows; fall back to the cover tier.
146    Unsupported,
147}
148
149/// One realized window: an adopted boundary root plus the content size it lays out at.
150///
151/// `windows[0]` starts as the app's first window (the `ready` root container) and later entries
152/// are opened through `open_window_root` (docs/windows.md), but the first slot is not
153/// privileged: it can close like any other, after which `windows[0]` is the oldest
154/// window still open, which is what `root()` then answers. The list is never emptied; see
155/// `remove_window_root`.
156struct WindowEntry {
157    root: RNode,
158    size: Size,
159}
160
161/// A node's context menu as the app gave it: fixed items, or a provider asked per summon.
162#[derive(Clone)]
163enum ContextMenuModel {
164    Items(Vec<day_spec::MenuItem>),
165    Provider(day_spec::ContextMenuFn),
166}
167
168pub struct Tree<B: Toolkit> {
169    mounted_id_families: std::collections::BTreeSet<String>,
170    pub toolkit: B,
171    nodes: SlotMap<RNode, NodeData<B::Handle>>,
172    windows: Vec<WindowEntry>,
173    layout_dirty: bool,
174    handlers: HashMap<RNode, Vec<EventHandler>>,
175    /// The gesture kinds each node asked the toolkit for (`enable_gesture`): where a gesture
176    /// addressed to a descendant is redirected (see `dispatch_to_node`).
177    gestures: HashMap<RNode, Vec<day_spec::GestureKind>>,
178    /// A decorated piece's own node, mapped to the outermost node its modifiers wrapped it in
179    /// (`set_element_outer`): how far a gesture addressed to that node may climb.
180    element_outer: HashMap<RNode, RNode>,
181    // (kind, handle): the kind rides along so the drain can offer a satellite piece its
182    // `release` hook before the backend frees the handle (§15.2).
183    release_queue: Vec<(day_spec::PieceKind, B::Handle)>,
184    /// Recycling-list state keyed by list node (docs/list.md, §10).
185    lists: HashMap<RNode, crate::list::ListState>,
186    trees: HashMap<RNode, crate::tree_driver::TreeState>,
187    /// Each node's context menu as the app gave it, keyed by the node the modifier named (and
188    /// the native root it landed on, where that differs): what the dayscript `context_menu`
189    /// step reads, since the toolkit keeps its own copy in native form.
190    context_menus: HashMap<RNode, ContextMenuModel>,
191    /// Count of nodes carrying an implicit `.animation` (§8.4). Gates the `resolve_anim` ancestor
192    /// walk: zero ⇒ non-`with_animation` patches skip it entirely (the common, O(1) path).
193    implicit_anim_count: usize,
194}
195
196impl<B: Toolkit> Tree<B> {
197    /// Drop the element ids of `anchor`'s whole subtree: the recycle write shared by tree
198    /// and list cells (a pooled cell keeps its nodes; only the dayscript identity must go).
199    /// Park every id under `anchor` so the (pooled, hidden) cell stops answering lookups. The
200    /// ids are kept, not dropped: `restore_subtree_ids` brings them back on the next bind.
201    fn clear_subtree_ids(&mut self, anchor: RNode) {
202        let mut stack = vec![anchor];
203        while let Some(n) = stack.pop() {
204            if let Some(d) = self.nodes.get_mut(n) {
205                if let Some(id) = d.id.take() {
206                    d.parked_id = Some(id);
207                }
208                stack.extend(d.children.iter().copied());
209            }
210        }
211    }
212
213    /// The inverse of `clear_subtree_ids`, run when a pooled cell is bound again: every node
214    /// that still has no id takes its parked one back. A node whose reactive `id_of` already
215    /// re-fired keeps the fresh id, and the parked copy is dropped either way.
216    fn restore_subtree_ids(&mut self, anchor: RNode) {
217        let mut stack = vec![anchor];
218        while let Some(n) = stack.pop() {
219            if let Some(d) = self.nodes.get_mut(n) {
220                if let Some(id) = d.parked_id.take()
221                    && d.id.is_none()
222                {
223                    d.id = Some(id);
224                }
225                stack.extend(d.children.iter().copied());
226            }
227        }
228    }
229
230    pub fn new(toolkit: B, root_handle: B::Handle, window_size: Size) -> Self {
231        let mut nodes = SlotMap::with_key();
232        let root = nodes.insert(NodeData {
233            kind: kinds::CONTAINER,
234            handle: Some(root_handle),
235            parent: RNode::null(),
236            children: Vec::new(),
237            layout: Rc::new(PassThrough),
238            flex: Flex::default(),
239            scope: Scope::root(),
240            id: None,
241            parked_id: None,
242            a11y: Default::default(),
243            cache: Vec::new(),
244            baseline_cache: None,
245            probe: NodeProbe::default(),
246            needs_measure: true,
247            needs_layout: true,
248            last_native_frame: None,
249            scroll_content: None,
250            implicit_anim: None,
251            tweaked: false,
252            is_boundary: true,
253        });
254        Tree {
255            mounted_id_families: Default::default(),
256            toolkit,
257            nodes,
258            windows: vec![WindowEntry {
259                root,
260                size: window_size,
261            }],
262            layout_dirty: true,
263            handlers: HashMap::new(),
264            gestures: HashMap::new(),
265            element_outer: HashMap::new(),
266            release_queue: Vec::new(),
267            lists: HashMap::new(),
268            trees: HashMap::new(),
269            context_menus: HashMap::new(),
270            implicit_anim_count: 0,
271        }
272    }
273
274    /// Create a node whose native handle is a foreign cell adopted from a recycling list host:
275    /// the same "wrap an externally-owned handle" trick the window root uses (docs/list.md).
276    pub(crate) fn create_cell_anchor(
277        &mut self,
278        handle: B::Handle,
279        scope: Scope,
280        layout: Rc<dyn Layout>,
281    ) -> RNode {
282        self.nodes.insert(NodeData {
283            kind: kinds::LIST_CELL,
284            handle: Some(handle),
285            parent: RNode::null(),
286            children: Vec::new(),
287            layout,
288            flex: Flex::default(),
289            scope,
290            id: None,
291            parked_id: None,
292            a11y: Default::default(),
293            cache: Vec::new(),
294            baseline_cache: None,
295            probe: NodeProbe::default(),
296            needs_measure: true,
297            needs_layout: true,
298            last_native_frame: None,
299            scroll_content: None,
300            implicit_anim: None,
301            tweaked: false,
302            is_boundary: true,
303        })
304    }
305
306    pub fn root(&self) -> RNode {
307        self.windows[0].root
308    }
309
310    pub(crate) fn node(&self, n: RNode) -> Option<&NodeData<B::Handle>> {
311        self.nodes.get(n)
312    }
313    pub(crate) fn node_mut(&mut self, n: RNode) -> Option<&mut NodeData<B::Handle>> {
314        self.nodes.get_mut(n)
315    }
316
317    /// The animation intent for a change to `node` (§8.4): the ambient `with_animation` if one is
318    /// in scope, else the nearest `.animation` on `node` or an ancestor (SwiftUI-style
319    /// propagation). `None` ⇒ the change applies instantly. The ancestor walk is skipped entirely
320    /// when no implicit animations exist anywhere (`implicit_anim_count == 0`).
321    pub(crate) fn resolve_anim(&self, node: RNode) -> Option<day_spec::AnimSpec> {
322        // Reduced motion (the user's setting, or a scripted run's fast mode): every property
323        // and frame animation lands at its destination (docs/accessibility.md).
324        if crate::reduce_motion_now() {
325            return None;
326        }
327        if let Some(a) = crate::anim::current_anim() {
328            return Some(a);
329        }
330        if self.implicit_anim_count == 0 {
331            return None;
332        }
333        let mut cur = node;
334        loop {
335            let n = self.nodes.get(cur)?;
336            if n.implicit_anim.is_some() {
337                return n.implicit_anim;
338            }
339            if n.parent == cur {
340                return None;
341            }
342            cur = n.parent;
343        }
344    }
345
346    /// Nearest ancestor (or self) with a native handle.
347    fn native_ancestor(&self, mut n: RNode) -> RNode {
348        loop {
349            let Some(node) = self.nodes.get(n) else {
350                // Stale node: fall back to the primary window's root (harmless, since the
351                // caller is about to no-op against it).
352                return self.windows[0].root;
353            };
354            if node.handle.is_some() {
355                return n;
356            }
357            n = node.parent;
358        }
359    }
360
361    /// In-order native descendants of `container` (its native children, not descending into them).
362    fn native_descendants(&self, container: RNode, out: &mut Vec<RNode>) {
363        let Some(node) = self.nodes.get(container) else {
364            return;
365        };
366        for &c in &node.children {
367            match self.nodes.get(c) {
368                Some(cd) if cd.handle.is_some() => out.push(c),
369                Some(_) => self.native_descendants(c, out),
370                None => {}
371            }
372        }
373    }
374
375    /// Index that `child`'s first native node occupies (or will occupy) among `ancestor`'s
376    /// native children: an in-order walk counting native roots before reaching `child`'s subtree.
377    fn native_index_for(&self, ancestor: RNode, target: RNode) -> usize {
378        fn walk<B: Toolkit>(tree: &Tree<B>, n: RNode, target: RNode, count: &mut usize) -> bool {
379            if n == target {
380                return true;
381            }
382            let Some(node) = tree.nodes.get(n) else {
383                return false;
384            };
385            if node.handle.is_some() && n != target {
386                // A native node counts as one slot; do not descend (its children are inside it).
387                *count += 1;
388                return false;
389            }
390            for &c in &node.children {
391                if walk(tree, c, target, count) {
392                    return true;
393                }
394            }
395            false
396        }
397        let mut count = 0;
398        let Some(anc) = self.nodes.get(ancestor) else {
399            return 0;
400        };
401        for &c in &anc.children {
402            if c == target || self.subtree_contains(c, target) {
403                // Count native roots in this subtree before target.
404                let mut cnt = count;
405                walk(self, c, target, &mut cnt);
406                return cnt;
407            }
408            let mut roots = Vec::new();
409            match self.nodes.get(c) {
410                Some(cd) if cd.handle.is_some() => count += 1,
411                Some(_) => {
412                    self.native_descendants(c, &mut roots);
413                    count += roots.len();
414                }
415                None => {}
416            }
417        }
418        count
419    }
420
421    fn subtree_contains(&self, root: RNode, target: RNode) -> bool {
422        if root == target {
423            return true;
424        }
425        let Some(node) = self.nodes.get(root) else {
426            return false;
427        };
428        node.children
429            .iter()
430            .any(|&c| self.subtree_contains(c, target))
431    }
432
433    /// Attach `child` under `parent` at child-list `index`, wiring native insertion.
434    fn attach_impl(&mut self, parent: RNode, child: RNode, index: usize) {
435        {
436            let p = self
437                .nodes
438                .get_mut(parent)
439                .expect("attach to missing parent");
440            let idx = index.min(p.children.len());
441            p.children.insert(idx, child);
442        }
443        self.nodes
444            .get_mut(child)
445            .expect("attach missing child")
446            .parent = parent;
447        // Native wiring: every native root inside `child`'s subtree inserts under the nearest
448        // native ancestor at its in-order position.
449        let ancestor = self.native_ancestor(parent);
450        let anc_handle = self.nodes[ancestor]
451            .handle
452            .clone()
453            .expect("native ancestor");
454        let mut roots = Vec::new();
455        match self.nodes.get(child) {
456            Some(cd) if cd.handle.is_some() => roots.push(child),
457            Some(_) => self.native_descendants(child, &mut roots),
458            None => {}
459        }
460        for r in roots {
461            let idx = self.native_index_for(ancestor, r);
462            let h = self.nodes[r].handle.clone().unwrap();
463            self.toolkit.insert(&anc_handle, &h, idx);
464        }
465        self.mark_needs_measure_impl(parent);
466    }
467
468    fn remove_subtree_impl(&mut self, node: RNode) {
469        // Detach native roots from their native ancestor, queue every handle for release,
470        // drop handler entries, then remove the node records.
471        let parent = self
472            .nodes
473            .get(node)
474            .map(|n| n.parent)
475            .unwrap_or(RNode::null());
476        if let Some(p) = self.nodes.get_mut(parent) {
477            p.children.retain(|&c| c != node);
478        }
479        // A realized node may be owned outside a window (for example an audio session).
480        // It has no native parent until inserted. native_ancestor(null) falls back to the
481        // primary root, which must not receive a remove for a widget it never contained.
482        let anc_handle = if parent.is_null() {
483            None
484        } else {
485            let ancestor = self.native_ancestor(parent);
486            self.nodes.get(ancestor).and_then(|n| n.handle.clone())
487        };
488        let mut roots = Vec::new();
489        match self.nodes.get(node) {
490            Some(nd) if nd.handle.is_some() => roots.push(node),
491            Some(_) => self.native_descendants(node, &mut roots),
492            None => {}
493        }
494        if let Some(anc_handle) = anc_handle {
495            for r in &roots {
496                let h = self.nodes[*r].handle.clone().unwrap();
497                self.toolkit.remove(&anc_handle, &h);
498            }
499        }
500        self.collect_and_release(node);
501        if parent.is_null() {
502            self.layout_dirty = true;
503        } else {
504            self.mark_needs_measure_impl(parent);
505        }
506    }
507
508    /// Remove `start` and its whole day subtree from the node table: dispose list-cell
509    /// scopes, drop handler entries, queue every handle for release, decrement the
510    /// implicit-animation count. Shared by [`Self::remove_subtree_impl`] and
511    /// `remove_window_root` so the bookkeeping cannot drift between them.
512    fn collect_and_release(&mut self, start: RNode) {
513        let mut stack = vec![start];
514        while let Some(n) = stack.pop() {
515            // A list node's per-cell row subtrees live in scopes owned by the list machinery
516            // (not the node tree): dispose them with the node, or their bindings (e.g. a
517            // localized row label) outlive the list and patch freed native cells on the
518            // next locale/theme change (a use-after-free on raw-pointer backends like Qt).
519            self.context_menus.remove(&n);
520            if let Some(list) = self.lists.remove(&n) {
521                for (_, cell) in list.cells {
522                    cell.scope.dispose();
523                    // The cell's day subtree lives outside the node tree (anchored to a
524                    // native cell, not a list child): remove it with the list, or its nodes
525                    // linger as zombies: stale element ids that hijack `find_by_id`, and
526                    // handlers whose captured signals are disposed so every press no-ops.
527                    stack.push(cell.anchor);
528                }
529            }
530            // Tree cells follow the identical rule (docs/tree.md).
531            if let Some(tree) = self.trees.remove(&n) {
532                for (_, cell) in tree.cells {
533                    cell.scope.dispose();
534                    stack.push(cell.anchor);
535                }
536            }
537            let Some(data) = self.nodes.remove(n) else {
538                continue;
539            };
540            self.handlers.remove(&n);
541            self.gestures.remove(&n);
542            self.element_outer.remove(&n);
543            if data.implicit_anim.is_some() {
544                self.implicit_anim_count = self.implicit_anim_count.saturating_sub(1);
545            }
546            if let Some(h) = data.handle {
547                // …but a cell anchor's handle is not day's to free: it is the native list host's
548                // cell, borrowed through `adopt` (§15.3), and the host frees its cell pool
549                // when the host is released. Queuing it here deletes it a second time: heap
550                // corruption on the raw-pointer backends (xaml/qt). Dropping the handle still
551                // balances whatever `adopt` retained (AppKit/UIKit/GTK/Android refcounts).
552                if data.kind != kinds::LIST_CELL {
553                    self.release_queue.push((data.kind, h));
554                }
555            }
556            stack.extend(data.children);
557        }
558    }
559
560    fn mark_needs_measure_impl(&mut self, node: RNode) {
561        let mut cur = node;
562        let mut measure = true;
563        while let Some(n) = self.nodes.get_mut(cur) {
564            n.needs_layout = true;
565            if measure {
566                n.needs_measure = true;
567                n.cache.clear();
568            }
569            if n.is_boundary {
570                measure = false;
571            }
572            if n.parent.is_null() {
573                break;
574            }
575            cur = n.parent;
576        }
577        self.layout_dirty = true;
578    }
579
580    /// Forget every cached measurement under `node`, `node` included. The opposite walk from
581    /// [`Self::mark_needs_measure_impl`]: a cell re-laid at a new width (`list_layout_cell_width`,
582    /// `tree_layout_cell_width`) has to measure its whole row afresh, or a `grow` child keeps the
583    /// size it was given at the old width, is placed at that width again, and its own children
584    /// stay put; a trailing control then sits past the row's new edge.
585    fn invalidate_subtree(&mut self, node: RNode) {
586        let mut stack = vec![node];
587        while let Some(cur) = stack.pop() {
588            if let Some(n) = self.nodes.get_mut(cur) {
589                n.needs_measure = true;
590                n.cache.clear();
591                stack.extend(n.children.iter().copied());
592            }
593        }
594    }
595
596    /// Lay the list cell `key`'s row out at `width` × its `RowHeight`: the one place a list row
597    /// is placed, shared by the first-approximation pass (`list_layout_cell`) and the backend's
598    /// correction (`list_layout_cell_width`).
599    fn layout_list_cell_at(&mut self, node: RNode, key: usize, width: f64) {
600        let Some(state) = self.lists.get(&node) else {
601            return;
602        };
603        let (anchor, stale, header) = match state.cells.get(&key) {
604            Some(b) if b.active => (b.anchor, b.laid_width != Some(width), b.header),
605            _ => return,
606        };
607        // A section header sizes to its content in every list: a `Uniform` pitch is the
608        // items' (docs/list.md "Sections").
609        let row_height = if header {
610            day_spec::props::RowHeight::Automatic
611        } else {
612            state.driver.row_height
613        };
614        // A new width: nothing measured at the old one may survive, or a `grow` child keeps it.
615        // The same width again (the dirty-cell sweep re-lays every bound cell on every pass)
616        // keeps the row's measurement cache; re-measuring every row's text each pass is what
617        // made a 500-row list's binds take whole seconds (the sweep runs once per bind).
618        if stale {
619            self.invalidate_subtree(anchor);
620            if let Some(b) = self
621                .lists
622                .get_mut(&node)
623                .and_then(|s| s.cells.get_mut(&key))
624            {
625                b.laid_width = Some(width);
626            }
627        }
628        let height = match row_height {
629            day_spec::props::RowHeight::Uniform(h) => h,
630            day_spec::props::RowHeight::Automatic => {
631                crate::layout::measure_node(self, anchor, Proposal::new(Some(width), None)).height
632            }
633        };
634        let previous = self
635            .lists
636            .get_mut(&node)
637            .and_then(|s| s.cells.get_mut(&key))
638            .and_then(|b| b.laid_height.replace(height));
639        // A self-sizing row the host already sized has a new height: say so, or the native row
640        // keeps the old one and its content clips or leaves a gap. The first layout needs no
641        // patch: the host reads `cell_height` right after `bind_row` returns.
642        if previous.is_some_and(|p| (p - height).abs() > 0.25)
643            && let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone())
644        {
645            self.toolkit.update(
646                &handle,
647                kinds::LIST,
648                &day_spec::props::ListPatch::RowSizeInvalidated { cell: key, height } as &dyn Any,
649                None,
650            );
651        }
652        crate::layout::place_node(
653            self,
654            anchor,
655            Rect::new(0.0, 0.0, width, height),
656            Point::ZERO,
657            true,
658        );
659        // Placed: the dirty-cell sweep need not lay this row again until something in it
660        // changes; placement dirt reaches this anchor even through a nested measure boundary.
661        // Left set, the sweep re-laid every bound cell of every list on every layout pass.
662        self.nodes[anchor].needs_measure = false;
663    }
664
665    /// The tree twin of [`Self::layout_list_cell_at`]: lay tree cell `key`'s row out at `width`
666    /// × its `RowHeight`, keeping the row's measurement cache unless the width changed.
667    fn layout_tree_cell_at(&mut self, node: RNode, key: usize, width: f64) {
668        let Some(state) = self.trees.get(&node) else {
669            return;
670        };
671        let (anchor, stale) = match state.cells.get(&key) {
672            Some(b) => (b.anchor, b.laid_width != Some(width)),
673            None => return,
674        };
675        let row_height = state.driver.row_height;
676        let native_height = state.cells.get(&key).and_then(|b| b.native_height);
677        if stale {
678            self.invalidate_subtree(anchor);
679            if let Some(b) = self
680                .trees
681                .get_mut(&node)
682                .and_then(|s| s.cells.get_mut(&key))
683            {
684                b.laid_width = Some(width);
685            }
686        }
687        let height = native_height.unwrap_or_else(|| match row_height {
688            day_spec::props::RowHeight::Uniform(h) => h,
689            day_spec::props::RowHeight::Automatic => {
690                crate::layout::measure_node(self, anchor, Proposal::new(Some(width), None)).height
691            }
692        });
693        if let Some(b) = self
694            .trees
695            .get_mut(&node)
696            .and_then(|s| s.cells.get_mut(&key))
697        {
698            b.laid_height = Some(height);
699        }
700        crate::layout::place_node(
701            self,
702            anchor,
703            Rect::new(0.0, 0.0, width, height),
704            Point::ZERO,
705            true,
706        );
707        self.nodes[anchor].needs_measure = false;
708    }
709
710    fn layout_now(&mut self) {
711        // Every window lays out independently at its own size; the release queue drains
712        // once, after all of them (a released handle may be referenced by no window).
713        for i in 0..self.windows.len() {
714            let (root, size) = (self.windows[i].root, self.windows[i].size);
715            let p = Proposal::exact(size);
716            crate::layout::measure_node(self, root, p);
717            crate::layout::place_node(self, root, Rect::from_size(size), Point::ZERO, true);
718        }
719        // Bound list cells live outside the window trees: their anchors are parentless
720        // boundaries, laid out at bind time, so the window pass above never reaches them.
721        // Placement dirt reaches a cell's anchor even when measurement invalidation stops at
722        // a nested fixed frame. Sweep the marked cells so newly inserted descendants receive
723        // frames and a row label that grew mid-edit does not keep its stale frame.
724        let dirty_cells: Vec<(RNode, usize)> = self
725            .lists
726            .iter()
727            .flat_map(|(list, state)| {
728                state.cells.iter().filter_map(|(key, cell)| {
729                    if !cell.active {
730                        return None;
731                    }
732                    self.nodes
733                        .get(cell.anchor)
734                        .filter(|n| n.needs_layout)
735                        .map(|_| (*list, *key))
736                })
737            })
738            .collect();
739        let dirty_tree_cells: Vec<(RNode, usize)> = self
740            .trees
741            .iter()
742            .flat_map(|(tree, state)| {
743                state.cells.iter().filter_map(|(key, cell)| {
744                    self.nodes
745                        .get(cell.anchor)
746                        .filter(|n| n.needs_layout)
747                        .map(|_| (*tree, *key))
748                })
749            })
750            .collect();
751        for (tree, key) in dirty_tree_cells {
752            self.tree_layout_cell(tree, key);
753        }
754        for (list, key) in dirty_cells {
755            self.list_layout_cell(list, key);
756        }
757        let queue = std::mem::take(&mut self.release_queue);
758        for (kind, h) in queue {
759            // The piece's own teardown runs first, while the handle is still valid: it may read
760            // the native view to unregister an observer. `release` then frees it (§15.2).
761            self.toolkit.release_piece(kind, &h);
762            self.toolkit.release(h);
763        }
764    }
765}
766
767// ---------------------------------------------------------------------------
768// Object-safe tree surface for pieces / bindings / handlers
769// ---------------------------------------------------------------------------
770
771/// A programmatic scroll destination (§7.6, docs/scroll.md). Edges are axis extremes
772/// (`Top`/`Bottom` vertical, `Leading`/`Trailing` horizontal, i.e. start/end in layout direction);
773/// `Offset` pins the viewport origin to a content-space point (clamped by the platform);
774/// `Id` reveals the element with that dayscript id inside its nearest enclosing scroll.
775#[derive(Clone, Debug, PartialEq)]
776pub enum ScrollTarget {
777    Top,
778    Bottom,
779    Leading,
780    Trailing,
781    Offset(Point),
782    Id(String),
783}
784
785pub trait TreeOps {
786    // The parameter list mirrors NodeData's fields one-to-one; grouping them into a
787    // params struct would just move the same list behind a constructor.
788    #[allow(clippy::too_many_arguments)]
789    fn create_node(
790        &mut self,
791        kind: PieceKind,
792        props: &dyn Any,
793        layout: Rc<dyn Layout>,
794        flex: Flex,
795        native: bool,
796        is_boundary: bool,
797        scope: Scope,
798    ) -> RNode;
799    fn attach(&mut self, parent: RNode, child: RNode);
800    /// Move an existing subtree without disposing its scope or native handles.
801    fn reparent(&mut self, child: RNode, parent: RNode) -> bool;
802    fn native_window_tabs(&self) -> bool;
803    fn native_document_tabs(&self) -> bool;
804    fn group_windows(&mut self, roots: &[RNode]);
805    fn window_tab_order(&self, root: RNode) -> Vec<day_spec::NodeId>;
806
807    fn attach_at(&mut self, parent: RNode, child: RNode, index: usize);
808    fn reorder_children(&mut self, parent: RNode, order: Vec<RNode>);
809    fn remove_subtree(&mut self, node: RNode);
810    /// Whether `node` still exists (diagnostics: stale-id probes).
811    fn node_exists(&self, node: RNode) -> bool;
812    /// Whether the platform renders in dark appearance (see `Toolkit::dark_mode`).
813    fn dark_mode(&mut self) -> bool;
814    /// Whether the user asked for less motion (see `Toolkit::reduce_motion`).
815    fn reduce_motion(&mut self) -> bool;
816    /// Apply an app-level appearance override (see `Toolkit::set_appearance`).
817    fn set_appearance(&mut self, dark: Option<bool>);
818    /// Put a badge on the app icon (see `Toolkit::set_app_badge`, docs/badge.md).
819    fn set_app_badge(&mut self, badge: &day_spec::AppBadge);
820    fn on_event(&mut self, node: RNode, h: EventHandler);
821    fn handlers_for(&self, node: RNode) -> Vec<EventHandler>;
822    /// The node itself if it has a native view, else the one a chain of single-child
823    /// layout-only wrappers leads down to: the descent `enable_gesture` and `set_focusable`
824    /// make, for a modifier that has to reach the view (`Decorate::on_key`'s key mark).
825    fn native_view_of(&self, node: RNode) -> Option<RNode>;
826    /// The layout-only wrappers directly above `node` (single child, no native view), nearest
827    /// first: the same element as `node`, where an id or a modifier often sits.
828    fn wrappers_above(&self, node: RNode) -> Vec<RNode>;
829    /// The node a `kind` gesture addressed to `node` is delivered to: `node` itself when it
830    /// enabled that gesture, else the nearest node above it, within the same element, that did
831    /// (where a `.padding(..).on_tap(..)` registered its handler), else `None`. The element's
832    /// nodes are those its modifiers wrapped it in (`set_element_outer`), native containers
833    /// such as `.background(..)` included, plus any layout-only wrappers directly above. What
834    /// makes a dayscript step by id reach the handler the way a native gesture on the inner
835    /// view reaches the recognizer on its superview.
836    fn gesture_target(&self, node: RNode, kind: day_spec::GestureKind) -> Option<RNode>;
837    /// Record that a decorated piece's own node `inner` sits inside wrappers up to `outer`: the
838    /// span one element's modifiers built, which `gesture_target` climbs. A no-op when the two
839    /// are the same node.
840    fn set_element_outer(&mut self, inner: RNode, outer: RNode);
841    fn set_id(&mut self, node: RNode, id: String);
842    /// Families attached at least once, including subsequently disposed/recycled nodes.
843    fn mounted_id_families(&self) -> Vec<String>;
844    /// Merge non-default grid cell facts onto a node's [`Flex`]: what the `.grid_span`/
845    /// `.grid_align` modifiers call (docs/grid.md). Called at build time, before the first layout.
846    fn set_grid_facts(&mut self, node: RNode, facts: GridFacts);
847    /// Give a wrapper that takes its size from `inner` (an overlay host) the inner node's
848    /// [`Flex::whole_row`], so a decorated platform row still reads as one to its list cell.
849    fn forward_whole_row(&mut self, wrapper: RNode, inner: RNode);
850    fn set_a11y(&mut self, node: RNode, a11y: A11yProps);
851    /// Speak `text` through the screen reader (`Toolkit::announce`, docs/accessibility.md).
852    fn announce(&mut self, text: &str, urgent: bool);
853    /// Attach a native gesture recognizer to `node` (docs/shapes.md): the backend then emits
854    /// `Event::Tap/LongPress/Drag` for it. The node must have a native handle.
855    fn enable_gesture(&mut self, node: RNode, kind: day_spec::GestureKind);
856    /// Move native keyboard focus to (or away from) `node` (docs/focus.md).
857    fn focus_node(&mut self, node: RNode, focused: bool);
858    /// Opt a container into the platform's focus system (docs/focus.md); what
859    /// `Decorate::focusable` calls. The node must have a native view somewhere under it.
860    fn set_focusable(&mut self, node: RNode, focusable: bool);
861    /// Mirror a `FocusChanged` event into the node's dayscript probe (pump-only).
862    fn set_probe_focused(&mut self, node: RNode, focused: bool);
863    /// Record which row of a nav host is current, on the `kinds::NAV` host that `.id()` tags
864    /// (docs/navigation.md). The host's `NavPatch::Select` carries a resident-page index
865    /// (visit order, which only coincides with row order when every page is built up front), so
866    /// the nav host reports the row index here instead. `None` = nothing selected (`-1`).
867    fn set_probe_selected(&mut self, node: RNode, selected: Option<usize>);
868    /// Record an external piece's current value in the node's dayscript probe, both the
869    /// numeric form (`assert_value`) and its display text (`assert_text`). The core patch
870    /// inspection only knows the builtin patch types, so a satellite piece (a stepper, a
871    /// rating) reports its own state through this instead (docs/extending.md).
872    fn set_probe_value(&mut self, node: RNode, value: f64, text: String);
873    fn set_app_menu(&mut self, items: Vec<day_spec::MenuItem>);
874    fn set_context_menu(&mut self, node: RNode, items: Vec<day_spec::MenuItem>);
875    /// Edit `root`'s window toolbar (docs/toolbars.md). `root` is a window root: the primary
876    /// root or one returned by `open_window_root`.
877    /// `false` when the toolkit did not apply it (the root has no native handle yet, or the
878    /// toolkit has no bar for it).
879    fn edit_window_toolbar(&mut self, root: RNode, ops: Vec<day_spec::ToolbarOp>) -> bool;
880    /// Apply a targeted change to one item of `root`'s toolbar; `false` as above.
881    fn patch_window_toolbar(&mut self, root: RNode, patch: day_spec::ToolbarPatch) -> bool;
882    /// Programmatic scroll (§7.6, docs/scroll.md): resolve `target` against a scroll node's
883    /// content/viewport and drive `Toolkit::scroll_to`. Returns false when `node` isn't a
884    /// realized scroll, or a supported native navigation menu with a top/bottom target
885    /// (the caller reports the miss; dayscript retries).
886    fn scroll_to_target(&mut self, node: RNode, target: &ScrollTarget, animated: bool) -> bool;
887    /// Scroll the nearest enclosing scroll ancestor so `node`'s frame is visible (minimal
888    /// scroll, `scrollRectToVisible` semantics). False when no scroll ancestor exists.
889    fn scroll_reveal(&mut self, node: RNode, animated: bool) -> bool;
890    fn patch(&mut self, node: RNode, patch: Box<dyn Any>, affects_size: bool);
891    fn replay(&mut self, node: RNode, ops: Vec<DrawOp>);
892    fn request_frame(&mut self, root: RNode, cb: day_spec::FrameCallback) -> day_spec::CancelFrame;
893    /// Set (or clear) a node's implicit `.animation` (§8.4): subsequent property patches and frame
894    /// changes on this node (or its descendants) animate with `anim` even outside a
895    /// `with_animation`.
896    fn set_implicit_anim(&mut self, node: RNode, anim: Option<day_spec::AnimSpec>);
897    /// Apply an animatable opacity (0..1) to `node`'s native handle (§8.4), animating if an
898    /// animation is in scope. No-op if the node has no handle.
899    fn set_node_opacity(&mut self, node: RNode, opacity: f64);
900    /// Apply an animatable [`Transform`] to `node`'s native handle (§8.4): the cheap movement/
901    /// scaling channel that never relayouts. No-op if the node has no handle.
902    fn set_node_transform(&mut self, node: RNode, t: day_spec::Transform);
903    /// Make `node`'s text user-selectable (the `.selectable()` modifier). One-shot and unmanaged;
904    /// No-op if the node has no handle.
905    fn set_node_selectable(&mut self, node: RNode, selectable: bool);
906    /// Apply a text field's [`InputTraits`](day_spec::InputTraits) (docs/textfield.md).
907    /// Idempotent; called again with the whole set when a reactive member changes. No-op if
908    /// the node has no handle.
909    fn set_node_input_traits(&mut self, node: RNode, traits: &day_spec::InputTraits);
910    /// Shape the pointer over `node` (the `.cursor()` modifier, docs/cursor.md). Idempotent;
911    /// called again when a reactive cursor changes. No-op if the node has no handle.
912    fn set_node_cursor(&mut self, node: RNode, cursor: day_spec::Cursor);
913    /// Make `node` move its window when dragged, like a title bar (`.window_drag_region()`,
914    /// docs/window-chrome.md).
915    fn set_node_drag_region(&mut self, node: RNode, drag: bool);
916    /// Record that a `.tweak` closure ran against `node`'s current handle (docs/tweaks.md), so
917    /// a later backing swap (`set_node_selectable` on a toolkit that rebuilds the widget) can
918    /// warn about the discarded work instead of losing it silently.
919    fn note_node_tweaked(&mut self, node: RNode) {
920        let _ = node;
921    }
922    fn mark_needs_measure(&mut self, node: RNode);
923    fn mark_layout_dirty(&mut self);
924    /// Forget every measurement Day holds and re-lay every bound list and tree row: the system
925    /// text size changed ([`crate::note_text_size_changed`]), so every text measurement is
926    /// stale, and a cached one would keep each label at its old size.
927    fn invalidate_all_measurements(&mut self);
928    fn layout_if_needed(&mut self);
929    fn set_window_size(&mut self, s: Size);
930    /// Report the app's current route to the backend (`Toolkit::set_route`); web-dom mirrors
931    /// it into the URL hash (docs/navigation.md). Called by the turn-end route sync when the
932    /// route actually changed.
933    fn set_route(&mut self, route: &str);
934    fn child_count(&self, node: RNode) -> usize;
935    fn first_child(&self, node: RNode) -> Option<RNode>;
936    /// `node`'s parent, `None` at a root: the window root, or a list cell's anchor, whose
937    /// parent is the native cell rather than a Day node.
938    fn parent_of(&self, node: RNode) -> Option<RNode>;
939    fn node_kind(&self, node: RNode) -> Option<PieceKind>;
940    /// The app-authored `.id()` string on `node` (`find_by_id`'s inverse), or `None` for an id-less
941    /// or disposed node. Backs the free [`id_of`], the recorder's NodeId → id lookup (§14.6).
942    fn node_id(&self, node: RNode) -> Option<String>;
943    /// A clone of the node's native handle boxed as `Any` (None for layout-only or disposed
944    /// nodes). TreeOps is object-safe, so the generic `Toolkit::Handle` can't appear here;
945    /// toolkit ext modules downcast to their concrete Handle type. This is the tweaks door
946    /// (docs/tweaks.md): cloning is cheap on every backend (a retain / gobject ref / GlobalRef
947    /// clone / Copy pointer) and the clone never outlives the native widget's own refcounting.
948    fn node_handle_any(&self, node: RNode) -> Option<Box<dyn Any>>;
949    fn node_frame(&self, node: RNode) -> Option<Rect>;
950    fn node_probe(&self, node: RNode) -> Option<NodeProbe>;
951    /// The node's accumulated accessibility annotations (§13): `a11y_audit`'s expectation.
952    fn node_a11y(&self, node: RNode) -> Option<A11yProps>;
953    /// The node's actual native state (`a11y_audit` diffs it against `node_a11y`, and
954    /// `assert_native` against the probe).
955    fn read_native(&self, node: RNode) -> Option<day_spec::NativeSnapshot>;
956    /// For every node with an `.id()` and a native handle: `(id, kind, expected, actual)`, the
957    /// raw material for the `a11y_audit` step (§14.2). Comparison/policy lives in day-script.
958    fn a11y_nodes(&self) -> Vec<(String, PieceKind, A11yProps, day_spec::NativeSnapshot)>;
959    fn find_by_id(&self, id: &str) -> Option<RNode>;
960    /// Show/hide the sidebar pane of the navigation host `host`: what the sidebar affordance
961    /// a `nav(Sidebar)` contributes for itself drives. `false` when the toolkit has no
962    /// pane to toggle there (or no sidebar concept at all). Per host, so a second window's
963    /// button collapses its own sidebar (docs/toolbars.md, docs/navigation.md).
964    fn toggle_sidebar(&mut self, _host: RNode) -> bool {
965        false
966    }
967    /// Press the platform's back affordance (`Toolkit::native_back`): the native path a
968    /// real tap takes, which `nav_back()` (Day's rail) never runs. `false` when nothing is
969    /// popped by it.
970    fn native_back(&mut self) -> bool {
971        false
972    }
973    fn prepare_snapshot(
974        &mut self,
975        root: Option<RNode>,
976        revision: u32,
977    ) -> Result<day_spec::capture::Readiness, String>;
978    fn snapshot(&mut self) -> Result<Vec<u8>, String>;
979    /// Where the primary window's root sits in [`Self::snapshot`]'s image, in points (see
980    /// `Toolkit::snapshot_origin`); `None` when the toolkit does not say.
981    fn snapshot_origin(&mut self) -> Option<Point>;
982    /// The same capture with the window's own chrome (see `Toolkit::snapshot_window_chrome`).
983    fn snapshot_chrome(&mut self) -> Result<Vec<u8>, String>;
984
985    // --- raster images (docs/images.md) -------------------------------------------------
986    // Request-shaped, like `present`: the answer arrives as `Event::ImageDecoded` /
987    // `Event::ImageEncoded` and resolves the waiter in `crate::image`.
988
989    /// Decode encoded bytes into a native image under `id` (see `Toolkit::decode_image`).
990    fn decode_image(&mut self, req: u64, id: day_spec::BitmapId, bytes: &[u8]);
991    /// What a decoded image is (see `Toolkit::image_info`).
992    fn image_info(&mut self, id: day_spec::BitmapId) -> Option<day_spec::BitmapInfo>;
993    /// An image's metadata beyond its pixels (see `Toolkit::image_properties`).
994    fn image_properties(&mut self, id: day_spec::BitmapId) -> Option<day_spec::ImageProperties>;
995    /// Encode a decoded image back to bytes (see `Toolkit::encode_image`).
996    fn encode_image(&mut self, req: u64, id: day_spec::BitmapId, spec: &day_spec::EncodeSpec);
997    /// Which formats this toolkit encodes (see `Toolkit::encode_formats`).
998    fn image_encode_formats(&mut self) -> Vec<day_spec::ImageFormat>;
999    /// Release a decoded image (see `Toolkit::release_image`).
1000    fn release_image(&mut self, id: day_spec::BitmapId);
1001    /// Render a canvas drawing into a bitmap under `id` (see `Toolkit::render_bitmap`); the
1002    /// answer arrives as `Event::BitmapRendered`.
1003    fn render_bitmap(
1004        &mut self,
1005        req: u64,
1006        id: day_spec::BitmapId,
1007        ops: &[day_spec::DrawOp],
1008        size: Size,
1009        scale: f64,
1010    );
1011    /// Render canvas drawings into a PDF (see `Toolkit::render_pdf`); the answer arrives as
1012    /// `Event::PdfRendered`.
1013    fn render_pdf(&mut self, req: u64, pages: &[day_spec::PdfPage], info: &day_spec::PdfInfo);
1014    /// Whether the toolkit can rasterize its own window at all (`Cap::Snapshot`).
1015    fn window_image_support(&mut self) -> day_spec::Support;
1016    /// The platform's font families (see `Toolkit::font_families`); cached by day-core.
1017    fn font_families(&mut self) -> Vec<day_spec::FontFamilyInfo>;
1018    /// Measure one line of canvas text (see `Toolkit::measure_text`).
1019    fn measure_text(
1020        &mut self,
1021        text: &str,
1022        size: f64,
1023        font: &day_spec::CanvasFont,
1024    ) -> Option<day_spec::TextMetrics>;
1025    /// Whether native transitions have settled (see `Toolkit::ui_idle`).
1026    fn ui_idle(&mut self) -> bool;
1027    fn root_node(&self) -> RNode;
1028
1029    // --- secondary windows (docs/windows.md) -------------------------------------------
1030
1031    /// Open a native window and adopt its content container as an additional boundary
1032    /// root. Inserts the root record, asks `Toolkit::open_window`, and answers per the
1033    /// toolkit's reply: `Open(root)` = live now (handle installed, window entry pushed),
1034    /// `Pending(root)` = native creation is in flight (the record is parked without a
1035    /// handle until [`Self::adopt_window_root`]), `Unsupported` = the placeholder was
1036    /// removed again; present the content as a cover instead.
1037    fn open_window_root(&mut self, options: &WindowOptions, kind: WindowKind) -> WindowRootReply;
1038    /// Complete a `Pending` open: adopt `raw` as the window's content container, install
1039    /// it on the parked root, and start laying out at `size`. `false` = the root is gone
1040    /// (closed before completion); the caller should drop the native window again.
1041    fn adopt_window_root(&mut self, root: RNode, raw: day_spec::RawHandle, size: Size) -> bool;
1042    /// Remove an (already childless) window root: bookkeeping + handle release + window
1043    /// entry removal. Never the primary. Idempotent; also valid for a parked Pending root.
1044    fn remove_window_root(&mut self, root: RNode);
1045    /// A secondary window's content size changed (its `WindowResized` handler).
1046    fn set_root_size(&mut self, root: RNode, s: Size);
1047
1048    /// Ask the toolkit to size the window whose content root is `root` to `size`
1049    /// (`WindowOptions::size_to_fit`). Also updates Day's own layout size for that root, so the
1050    /// content is re-laid at the size the window actually became.
1051    fn fit_window(&mut self, root: RNode, size: Size);
1052    /// Register an extra layout root: a node that stays attached in the tree (native
1053    /// re-homing needs the parent link) but lays out independently at its own reported
1054    /// size: the cover-fallback surface (docs/windows.md). The primary root's PassThrough
1055    /// layout only descends into its first child, so a second top-level surface must drive
1056    /// its own layout entry. `set_root_size` resizes it; drop it before subtree removal.
1057    fn add_extra_layout_root(&mut self, node: RNode, size: Size);
1058    /// Unregister an extra layout root (the entry only; the node itself is removed by the
1059    /// ordinary `remove_subtree`).
1060    fn drop_extra_layout_root(&mut self, node: RNode);
1061    /// Ask the platform to close the window whose root is `root` (async; the platform
1062    /// confirms with `Event::WindowClosed`).
1063    fn close_native_window(&mut self, root: RNode);
1064    /// End the app: the last primary window has closed (docs/windows.md close policy).
1065    fn quit_app(&mut self);
1066    /// Bring the window whose root is `root` to front and make it key.
1067    fn focus_native_window(&mut self, root: RNode);
1068    /// Retitle the window whose root is `root`.
1069    fn set_native_window_title(&mut self, root: RNode, title: &str);
1070    /// Apply a property change to the window whose root is `root` (docs/windows.md "Window
1071    /// properties"). Ignored for a root with no native handle yet.
1072    fn apply_window(&mut self, root: RNode, change: &day_spec::WindowChange);
1073    /// Install the app's Dock menu (docs/menus.md).
1074    fn set_dock_menu(&mut self, items: &[day_spec::MenuItem]);
1075    /// Show the app's status items (docs/status-item.md).
1076    fn set_status_items(&mut self, items: &[day_spec::StatusItemSpec]);
1077    /// Show progress on the app's Dock or taskbar icon.
1078    fn set_app_progress(&mut self, progress: day_spec::AppProgress);
1079    /// Show or hide the macOS Dock icon.
1080    fn set_dock_visible(&mut self, visible: bool);
1081    /// Keep the process running when its last window closes (day-core decides when to quit).
1082    fn set_keep_running(&mut self, keep: bool);
1083    /// The outer frame of the window whose root is `root` (docs/windows.md).
1084    fn native_window_frame(&self, root: RNode) -> Option<day_spec::Rect>;
1085    /// The attached displays.
1086    fn monitors(&self) -> Vec<day_spec::Monitor>;
1087    /// Replace the app's run-time launcher shortcuts.
1088    fn set_launcher_shortcuts(&mut self, shortcuts: &[day_spec::LauncherShortcut]);
1089    /// Snapshot the window whose root is `root` (the primary answers `snapshot`).
1090    fn snapshot_of(&mut self, root: RNode) -> Result<Vec<u8>, String>;
1091    /// Toolkit capability probe (pieces pick presentation with it, e.g. `Cap::NavSplit`).
1092    fn capability(&self, cap: Cap) -> Support;
1093    /// Attach pull recognition to a mounted scroll view.
1094    fn attach_refresh(&mut self, node: RNode, host: NodeId);
1095    fn set_presented(&mut self, node: RNode, visible: bool);
1096    /// Does the running backend deliver this lifecycle phase (docs/lifecycle.md)?
1097    fn supports_lifecycle(&self, phase: day_spec::Lifecycle) -> bool;
1098    /// Present a native modal for request `req` (docs/dialogs.md).
1099    fn present(&mut self, req: u64, spec: &present::PresentSpec);
1100    /// Dismiss the modal for `req` (programmatic resolve while it is still up).
1101    fn dismiss(&mut self, req: u64);
1102    /// Open `url` in the platform's default handler (what the `link` piece calls).
1103    fn open_url(&mut self, url: &str);
1104    fn share_support(&self) -> Support;
1105    fn share_url(&mut self, url: &str, title: &str) -> bool;
1106    fn application_handlers(
1107        &mut self,
1108        query: &day_spec::applications::HandlerQuery,
1109    ) -> Result<day_spec::applications::ApplicationHandlers, day_spec::applications::ApplicationError>;
1110    fn open_url_with_application(
1111        &mut self,
1112        url: &str,
1113        application: &str,
1114        completion: day_spec::applications::OpenApplicationCompletion,
1115    );
1116
1117    /// Re-send the union of every mounted `defers_system_gestures` request (docs/cover.md).
1118    fn defer_system_gestures(&mut self, edges: day_spec::Edges);
1119    /// Re-send whether any mounted `status_bar_hidden` request asks to hide the status bar
1120    /// (docs/cover.md).
1121    fn set_status_bar_hidden(&mut self, hidden: bool);
1122
1123    // Recycling list methods (docs/list.md, §10). Called by day-core's `ListSource` closures
1124    // (via `with_tree`) when the native list pulls rows; never nested inside another borrow.
1125    // (`len`/`token_at` read the piece's snapshot directly and don't need the tree.)
1126    fn install_list(&mut self, node: RNode, driver: crate::list::ListDriver);
1127    /// Decide whether row `key`'s cell must be built (returns a fresh anchor) or rebound.
1128    /// `header`: the row about to bind is a section header (docs/list.md "Sections"); a cell
1129    /// that held the other kind is rebuilt.
1130    fn list_prepare_cell(
1131        &mut self,
1132        node: RNode,
1133        key: usize,
1134        cell: RawHandle,
1135        header: bool,
1136    ) -> crate::list::CellStep;
1137    /// Record a freshly built row (or section header) for a cell.
1138    fn list_store_cell(
1139        &mut self,
1140        node: RNode,
1141        key: usize,
1142        anchor: RNode,
1143        built: crate::list::BuiltRow,
1144        header: bool,
1145    );
1146    /// Lay the row out inside its cell bounds (row content width × the RowHeight).
1147    fn list_layout_cell(&mut self, node: RNode, key: usize);
1148    /// Re-lay the row at the cell's actual width (the list's width is only the first
1149    /// approximation: a native row's chrome, GtkListView's themed `row` node, makes the cell
1150    /// narrower, and a row laid at the list's width holds the whole list wider than its frame).
1151    fn list_layout_cell_width(&mut self, node: RNode, key: usize, width: f64);
1152    /// The height the cell's row was last laid at (`ListSource::cell_height`); 0 before its
1153    /// first layout.
1154    fn list_cell_height(&self, node: RNode, key: usize) -> f64;
1155    /// The physical-cell keys of every bound cell of the list at `node` (for a bulk re-layout
1156    /// after the list's own width changed).
1157    /// Active cells eligible for width-change layout; excludes cells parked by `recycle`.
1158    fn list_cell_keys(&self, node: RNode) -> Vec<usize>;
1159    /// A pooled cell left the visible row set (an emulated backend hides rows past a shrunk
1160    /// source instead of destroying them): clear the cell subtree's element ids so stale rows
1161    /// stop answering dayscript lookups. The cell itself stays pooled; the next bind re-ids it.
1162    fn list_recycle_cell(&mut self, node: RNode, key: usize);
1163    /// The host discarded cell `key` of list `node` for good: dispose the row built in it.
1164    fn list_release_cell(&mut self, node: RNode, key: usize);
1165    /// Cell `key` is the host's pinned header copy (`ListSource::bind_pinned`): park its ids, now
1166    /// and on every rebind, so lookups find the header row in the list.
1167    fn list_pin_cell(&mut self, node: RNode, key: usize);
1168    /// Apply a data change: the native host re-queries the source.
1169    fn list_reload(&mut self, node: RNode);
1170    fn list_splice(&mut self, node: RNode, deltas: Vec<day_spec::props::RowDelta>);
1171    fn set_undo_state(&mut self, state: &day_spec::UndoState);
1172    fn set_edit_state(&mut self, state: &day_spec::EditState);
1173    fn modifiers(&mut self) -> day_spec::Modifiers;
1174    /// Imperatively scroll the native list so its last row is fully visible (no-op if empty).
1175    fn list_scroll_to_end(&mut self, node: RNode);
1176    /// Imperatively scroll the native list so row `row` is visible (clamped; no-op if empty).
1177    fn list_scroll_to_row(&mut self, node: RNode, row: usize);
1178    /// Programmatically sync the list's selected rows (empty = clear). The toolkit applies
1179    /// without re-emitting a selection event.
1180    fn list_set_selected(&mut self, node: RNode, rows: Vec<usize>);
1181    /// Enter or leave the list's native editing mode (`ListPatch::Editing`); a toolkit
1182    /// without one ignores it.
1183    fn list_set_editing(&mut self, node: RNode, on: bool);
1184    /// The list's driver, for the guard → commit path `list_try_reorder` runs outside the
1185    /// borrow (`None` when `node` hosts no list).
1186    fn list_driver(&mut self, node: RNode) -> Option<std::rc::Rc<crate::list::ListDriver>>;
1187
1188    // Hierarchical tree methods (docs/tree.md): the list methods' shape, token-addressed. Called
1189    // by day-core's `TreeSource` closures (via `with_tree`) when the native tree pulls
1190    // rows; never nested inside another borrow.
1191    fn install_tree(&mut self, node: RNode, driver: crate::tree_driver::TreeDriver);
1192    /// Decide whether the cell for `key` must be built (fresh anchor) or rebound.
1193    fn tree_prepare_cell(
1194        &mut self,
1195        node: RNode,
1196        key: usize,
1197        cell: RawHandle,
1198    ) -> crate::tree_driver::TreeCellStep;
1199    /// Record a freshly built row for a cell.
1200    fn tree_store_cell(
1201        &mut self,
1202        node: RNode,
1203        key: usize,
1204        anchor: RNode,
1205        built: crate::tree_driver::TreeBuiltRow,
1206    );
1207    /// Lay the row out inside its cell bounds (row content width × the RowHeight).
1208    fn tree_layout_cell(&mut self, node: RNode, key: usize);
1209    /// Re-lay the row at the cell's actual width (indentation makes tree cells per-row wide).
1210    fn tree_layout_cell_width(&mut self, node: RNode, key: usize, width: f64);
1211    fn tree_layout_cell_size(&mut self, node: RNode, key: usize, size: Size);
1212    /// The height the cell's row was last laid at (`TreeSource::cell_height`); 0 before its
1213    /// first layout.
1214    fn tree_cell_height(&self, node: RNode, key: usize) -> f64;
1215    /// The cell went back to the host's reuse pool: clear the row subtree's element ids, so
1216    /// a collapsed-away row stops answering `find_by_id` (dayscript `assert_missing`) and a
1217    /// stale id can never hijack a lookup. The next bind re-sets the live row's id.
1218    fn tree_recycle_cell(&mut self, node: RNode, key: usize);
1219    /// Apply a data change: the native host re-queries the source (expansion and selection
1220    /// survive by token).
1221    fn tree_reload(&mut self, node: RNode);
1222    /// Programmatically disclose/collapse one row (no `TreeExpanded` echo).
1223    fn tree_set_expanded(&mut self, node: RNode, token: u64, expanded: bool);
1224    /// Programmatically sync the tree's selected rows (no selection-event echo).
1225    fn tree_set_selected(&mut self, node: RNode, tokens: Vec<u64>);
1226    /// Scroll this row into view, realizing it if needed (ancestors already expanded).
1227    fn tree_reveal(&mut self, node: RNode, token: u64);
1228    /// The tree's driver, for the guard → commit path `tree_try_move` runs outside the
1229    /// borrow (`None` when `node` hosts no tree).
1230    fn tree_driver(&mut self, node: RNode) -> Option<std::rc::Rc<crate::tree_driver::TreeDriver>>;
1231    /// Install native transfer behavior on a realized host.
1232    fn set_drag_source(&mut self, node: RNode, source: day_spec::transfer::Source);
1233    fn set_drop_target(&mut self, node: RNode, target: day_spec::transfer::Target);
1234    /// Install a summon-time context-menu provider on `node` (docs/menus.md).
1235    fn set_context_menu_fn(&mut self, node: RNode, f: day_spec::ContextMenuFn);
1236    /// The context menu the app gave `node` (or its nearest ancestor that has one), as a
1237    /// provider; what the dayscript `context_menu` step chooses from. Returned rather than
1238    /// called, because a provider is app code: it may select what it summons over and flush,
1239    /// which re-enters the tree, so the caller asks it once the tree borrow has ended.
1240    fn context_menu_of(&mut self, node: RNode) -> Option<day_spec::ContextMenuFn>;
1241}
1242
1243impl<B: Toolkit> TreeOps for Tree<B> {
1244    fn set_presented(&mut self, node: RNode, visible: bool) {
1245        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
1246            self.toolkit.set_presented(&handle, visible);
1247        }
1248    }
1249
1250    fn attach_refresh(&mut self, node: RNode, host: NodeId) {
1251        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
1252            self.toolkit.attach_refresh(&handle, host);
1253        }
1254    }
1255
1256    fn capability(&self, cap: Cap) -> Support {
1257        self.toolkit.capability(cap)
1258    }
1259
1260    fn supports_lifecycle(&self, phase: day_spec::Lifecycle) -> bool {
1261        self.toolkit.supports_lifecycle(phase)
1262    }
1263
1264    fn present(&mut self, req: u64, spec: &present::PresentSpec) {
1265        self.toolkit.present(req, spec);
1266    }
1267
1268    fn dismiss(&mut self, req: u64) {
1269        self.toolkit.dismiss(req);
1270    }
1271
1272    fn open_url(&mut self, url: &str) {
1273        self.toolkit.open_url(url);
1274    }
1275    fn share_support(&self) -> Support {
1276        self.toolkit.share_support()
1277    }
1278    fn share_url(&mut self, url: &str, title: &str) -> bool {
1279        self.toolkit.share_url(url, title)
1280    }
1281
1282    fn application_handlers(
1283        &mut self,
1284        query: &day_spec::applications::HandlerQuery,
1285    ) -> Result<day_spec::applications::ApplicationHandlers, day_spec::applications::ApplicationError>
1286    {
1287        self.toolkit.application_handlers(query)
1288    }
1289
1290    fn open_url_with_application(
1291        &mut self,
1292        url: &str,
1293        application: &str,
1294        completion: day_spec::applications::OpenApplicationCompletion,
1295    ) {
1296        self.toolkit
1297            .open_url_with_application(url, application, completion);
1298    }
1299
1300    fn set_appearance(&mut self, dark: Option<bool>) {
1301        self.toolkit.set_appearance(dark);
1302    }
1303
1304    fn set_app_badge(&mut self, badge: &day_spec::AppBadge) {
1305        self.toolkit.set_app_badge(badge);
1306    }
1307
1308    fn defer_system_gestures(&mut self, edges: day_spec::Edges) {
1309        self.toolkit.defer_system_gestures(edges);
1310    }
1311
1312    fn set_status_bar_hidden(&mut self, hidden: bool) {
1313        self.toolkit.set_status_bar_hidden(hidden);
1314    }
1315
1316    fn create_node(
1317        &mut self,
1318        kind: PieceKind,
1319        props: &dyn Any,
1320        layout: Rc<dyn Layout>,
1321        flex: Flex,
1322        native: bool,
1323        is_boundary: bool,
1324        scope: Scope,
1325    ) -> RNode {
1326        let mut probe = NodeProbe {
1327            enabled: true,
1328            ..Default::default()
1329        };
1330        {
1331            use day_spec::props::*;
1332            if let Some(p) = props.downcast_ref::<LabelProps>() {
1333                probe.text = p.text.clone();
1334            } else if let Some(p) = props.downcast_ref::<NavMenuProps>() {
1335                probe.selected = p.selected.map(|i| i as i64).unwrap_or(-1);
1336                probe.value = probe.selected as f64;
1337            } else if let Some(p) = props.downcast_ref::<ButtonProps>() {
1338                probe.text = p.title.clone();
1339                probe.enabled = p.enabled;
1340                // The selected state reads like a toggle's (`assert_on`).
1341                probe.flag = p.selected;
1342            } else if let Some(p) = props.downcast_ref::<ToggleProps>() {
1343                probe.flag = p.on;
1344                probe.enabled = p.enabled;
1345            } else if let Some(p) = props.downcast_ref::<SliderProps>() {
1346                probe.value = p.value;
1347                probe.enabled = p.enabled;
1348            } else if let Some(p) = props.downcast_ref::<TextFieldProps>() {
1349                probe.text = p.text.clone();
1350                probe.enabled = p.enabled;
1351            } else if let Some(p) = props.downcast_ref::<ProgressProps>() {
1352                // `flag` marks indeterminate; `value` holds the determinate fraction.
1353                probe.flag = p.value.is_none();
1354                probe.value = p.value.unwrap_or(0.0);
1355            } else if let Some(p) = props.downcast_ref::<PickerProps>() {
1356                probe.selected = p.selected as i64;
1357                probe.value = p.selected as f64;
1358                // A picker's text is the option it shows: what `assert_text` should see
1359                // (the index is `assert_value`'s answer). Kept current by both patches below.
1360                probe.text = p.options.get(p.selected).cloned().unwrap_or_default();
1361                probe.options = p.options.clone();
1362                probe.enabled = p.enabled;
1363            } else if let Some(p) = props.downcast_ref::<TextAreaProps>() {
1364                probe.text = p.text.clone();
1365            }
1366        }
1367        let node = self.nodes.insert(NodeData {
1368            kind,
1369            handle: None,
1370            parent: RNode::null(),
1371            children: Vec::new(),
1372            layout,
1373            flex,
1374            scope,
1375            id: None,
1376            parked_id: None,
1377            a11y: Default::default(),
1378            cache: Vec::new(),
1379            baseline_cache: None,
1380            probe,
1381            needs_measure: true,
1382            needs_layout: true,
1383            last_native_frame: None,
1384            scroll_content: None,
1385            implicit_anim: None,
1386            tweaked: false,
1387            is_boundary,
1388        });
1389        if native {
1390            let h = self.toolkit.realize(kind, props, rnode_to_id(node));
1391            self.nodes[node].handle = Some(h);
1392        }
1393        node
1394    }
1395
1396    fn attach(&mut self, parent: RNode, child: RNode) {
1397        let index = self
1398            .nodes
1399            .get(parent)
1400            .map(|p| p.children.len())
1401            .unwrap_or(0);
1402        self.attach_impl(parent, child, index);
1403    }
1404
1405    fn native_window_tabs(&self) -> bool {
1406        self.toolkit.native_window_tabs()
1407    }
1408    fn native_document_tabs(&self) -> bool {
1409        self.toolkit.native_document_tabs()
1410    }
1411    fn group_windows(&mut self, roots: &[RNode]) {
1412        let hosts = roots
1413            .iter()
1414            .filter_map(|r| self.nodes.get(*r)?.handle.clone())
1415            .collect::<Vec<_>>();
1416        self.toolkit.group_windows(&hosts);
1417    }
1418    fn window_tab_order(&self, root: RNode) -> Vec<day_spec::NodeId> {
1419        self.nodes
1420            .get(root)
1421            .and_then(|n| n.handle.as_ref())
1422            .map(|h| self.toolkit.window_tab_order(h))
1423            .unwrap_or_default()
1424    }
1425    fn reparent(&mut self, child: RNode, parent: RNode) -> bool {
1426        if !self.nodes.contains_key(child) || !self.nodes.contains_key(parent) {
1427            return false;
1428        }
1429        let old = self.nodes[child].parent;
1430        if old == parent {
1431            return true;
1432        }
1433        // Reject cycles and window roots: only content may migrate.
1434        if self.windows.iter().any(|w| w.root == child) {
1435            return false;
1436        }
1437        let mut up = parent;
1438        while let Some(n) = self.nodes.get(up) {
1439            if up == child {
1440                return false;
1441            }
1442            up = n.parent;
1443        }
1444        let ancestor = self.native_ancestor(old);
1445        let handle = self.nodes.get(ancestor).and_then(|n| n.handle.clone());
1446        let mut roots = Vec::new();
1447        if self.nodes[child].handle.is_some() {
1448            roots.push(child);
1449        } else {
1450            self.native_descendants(child, &mut roots);
1451        }
1452        if let Some(h) = handle {
1453            for r in roots {
1454                // roots contains only nodes with handles, collected above without mutation.
1455                self.toolkit
1456                    .remove(&h, self.nodes[r].handle.as_ref().unwrap());
1457            }
1458        }
1459        if let Some(p) = self.nodes.get_mut(old) {
1460            p.children.retain(|n| *n != child);
1461        }
1462        self.mark_needs_measure_impl(old);
1463        self.attach(parent, child);
1464        self.layout_dirty = true;
1465        true
1466    }
1467
1468    fn attach_at(&mut self, parent: RNode, child: RNode, index: usize) {
1469        self.attach_impl(parent, child, index);
1470    }
1471
1472    fn reorder_children(&mut self, parent: RNode, order: Vec<RNode>) {
1473        // Nothing moved, and the resync below is not free. `each` re-runs its diff whenever the
1474        // source closure's tracked reads wake it, which is far more often than the order changes:
1475        // a projection that reads its store coarsely re-runs for every keystroke in a field that
1476        // store also feeds. Re-inserting every native descendant on each of those is churn on the
1477        // backends that can re-parent cheaply, and on the ones whose `move_child` detaches first
1478        // (android, arkui) it also drops the keyboard focus of a text field inside the subtree
1479        // (docs/focus.md): the field a user was typing into loses focus per character.
1480        if self.nodes.get(parent).is_some_and(|p| p.children == order) {
1481            return;
1482        }
1483        if let Some(p) = self.nodes.get_mut(parent) {
1484            p.children = order;
1485        }
1486        // Full native resync of the nearest native ancestor: rebuild in-order positions.
1487        let ancestor = self.native_ancestor(parent);
1488        let anc_handle = self.nodes[ancestor]
1489            .handle
1490            .clone()
1491            .expect("native ancestor");
1492        let mut desired = Vec::new();
1493        self.native_descendants(ancestor, &mut desired);
1494        for (i, r) in desired.iter().enumerate() {
1495            let h = self.nodes[*r].handle.clone().unwrap();
1496            self.toolkit.move_child(&anc_handle, &h, i);
1497        }
1498        self.mark_needs_measure_impl(parent);
1499    }
1500
1501    fn remove_subtree(&mut self, node: RNode) {
1502        // Window roots have no native parent to detach from; they go through
1503        // `remove_window_root` (docs/windows.md), never here.
1504        debug_assert!(
1505            !self.windows.iter().any(|w| w.root == node),
1506            "remove_subtree called with a window root"
1507        );
1508        self.remove_subtree_impl(node);
1509    }
1510
1511    fn on_event(&mut self, node: RNode, h: EventHandler) {
1512        self.handlers.entry(node).or_default().push(h);
1513    }
1514
1515    fn handlers_for(&self, node: RNode) -> Vec<EventHandler> {
1516        self.handlers.get(&node).cloned().unwrap_or_default()
1517    }
1518    fn native_view_of(&self, node: RNode) -> Option<RNode> {
1519        let mut cur = node;
1520        for _ in 0..16 {
1521            let n = self.nodes.get(cur)?;
1522            if n.handle.is_some() {
1523                return Some(cur);
1524            }
1525            match n.children.as_slice() {
1526                [only] => cur = *only,
1527                _ => return None,
1528            }
1529        }
1530        None
1531    }
1532    fn gesture_target(&self, node: RNode, kind: day_spec::GestureKind) -> Option<RNode> {
1533        let has = |n: RNode| self.gestures.get(&n).is_some_and(|k| k.contains(&kind));
1534        if has(node) {
1535            return Some(node);
1536        }
1537        // Climb the element's own wrappers, native ones included. An element nested in another
1538        // (`.any()` and more modifiers) extends the span to the enclosing element's outer node.
1539        let mut cur = node;
1540        let mut bound = self.element_outer.get(&node).copied();
1541        while let Some(outer) = bound {
1542            if cur == outer {
1543                bound = self.element_outer.get(&outer).copied();
1544                continue;
1545            }
1546            let Some(parent) = self.nodes.get(cur).map(|n| n.parent) else {
1547                break;
1548            };
1549            if parent == cur || !self.nodes.contains_key(parent) {
1550                break;
1551            }
1552            cur = parent;
1553            if has(cur) {
1554                return Some(cur);
1555            }
1556        }
1557        self.wrappers_above(cur).into_iter().find(|w| has(*w))
1558    }
1559
1560    fn set_element_outer(&mut self, inner: RNode, outer: RNode) {
1561        if inner != outer {
1562            self.element_outer.insert(inner, outer);
1563        }
1564    }
1565
1566    fn wrappers_above(&self, node: RNode) -> Vec<RNode> {
1567        let mut out = Vec::new();
1568        let mut cur = node;
1569        for _ in 0..16 {
1570            let Some(parent) = self.nodes.get(cur).map(|n| n.parent) else {
1571                break;
1572            };
1573            match self.nodes.get(parent) {
1574                Some(p) if p.handle.is_none() && p.children.len() == 1 && parent != cur => {
1575                    out.push(parent);
1576                    cur = parent;
1577                }
1578                _ => break,
1579            }
1580        }
1581        out
1582    }
1583    fn node_exists(&self, node: RNode) -> bool {
1584        self.nodes.contains_key(node)
1585    }
1586    fn dark_mode(&mut self) -> bool {
1587        self.toolkit.dark_mode()
1588    }
1589    fn reduce_motion(&mut self) -> bool {
1590        self.toolkit.reduce_motion()
1591    }
1592
1593    fn mounted_id_families(&self) -> Vec<String> {
1594        self.mounted_id_families.iter().cloned().collect()
1595    }
1596    fn set_id(&mut self, node: RNode, id: String) {
1597        if self.nodes.contains_key(node) {
1598            self.mounted_id_families
1599                .insert(crate::element_id::family_of(&id).to_owned());
1600        }
1601        if let Some(n) = self.nodes.get_mut(node) {
1602            n.id = Some(id.clone());
1603            n.a11y.merge(&A11yProps {
1604                identifier: Some(id),
1605                ..Default::default()
1606            });
1607            if let Some(h) = n.handle.clone() {
1608                self.toolkit.set_a11y(&h, &n.a11y);
1609            }
1610        }
1611    }
1612
1613    fn forward_whole_row(&mut self, wrapper: RNode, inner: RNode) {
1614        let whole = self.nodes.get(inner).is_some_and(|n| n.flex.whole_row);
1615        if let Some(n) = self.nodes.get_mut(wrapper) {
1616            n.flex.whole_row = whole;
1617        }
1618    }
1619
1620    fn set_grid_facts(&mut self, node: RNode, facts: GridFacts) {
1621        if let Some(n) = self.nodes.get_mut(node) {
1622            let g = &mut n.flex.grid;
1623            if facts.is_row {
1624                g.is_row = true;
1625            }
1626            if facts.row_valign.is_some() {
1627                g.row_valign = facts.row_valign;
1628            }
1629            if facts.col_span != 0 {
1630                g.col_span = facts.col_span;
1631            }
1632            if facts.align.is_some() {
1633                g.align = facts.align;
1634            }
1635        }
1636    }
1637
1638    fn announce(&mut self, text: &str, urgent: bool) {
1639        self.toolkit.announce(text, urgent);
1640    }
1641
1642    fn set_a11y(&mut self, node: RNode, a11y: A11yProps) {
1643        if let Some(n) = self.nodes.get_mut(node) {
1644            // Merge onto whatever's already recorded (piece default role, an earlier `.a11y`/`.id`)
1645            // and re-apply the full picture; backends set each present field idempotently (§13).
1646            n.a11y.merge(&a11y);
1647            // A canvas has no text of its own: what it tells a screen reader (its value, else
1648            // its label) is its text, so `assert_text` reads a drawn readout the way it reads
1649            // a label.
1650            if n.kind == kinds::CANVAS
1651                && let Some(text) = n.a11y.value.clone().or_else(|| n.a11y.label.clone())
1652            {
1653                n.probe.text = text;
1654            }
1655            if let Some(h) = n.handle.clone() {
1656                self.toolkit.set_a11y(&h, &n.a11y);
1657            }
1658        }
1659    }
1660
1661    fn enable_gesture(&mut self, node: RNode, kind: day_spec::GestureKind) {
1662        let kinds = self.gestures.entry(node).or_default();
1663        if !kinds.contains(&kind) {
1664            kinds.push(kind);
1665        }
1666        // `.on_tap`/`.on_drag` often land on a layout-only wrapper (`.frame()`, `.padding()`,
1667        // `.grow()` produce one) that has no native view to carry a recognizer. Descend
1668        // through single-child layout-only nodes to the nearest native descendant and attach
1669        // there, but deliver events against the original node, where the modifier registered
1670        // its handler.
1671        let mut cur = node;
1672        for _ in 0..16 {
1673            let Some(n) = self.nodes.get(cur) else { return };
1674            if let Some(h) = n.handle.clone() {
1675                if kind == day_spec::GestureKind::Tap && n.kind == kinds::BUTTON {
1676                    log::warn!(
1677                        "tap gesture attached to native button {:?} (id: {:?}): \
1678                         use .action(...) instead of .on_tap(...) or .on_tap_at(...). \
1679                         A separate gesture recognizer can delay pressed feedback and \
1680                         bypass native keyboard, accessibility, and disabled behavior",
1681                        rnode_to_id(cur),
1682                        n.id,
1683                    );
1684                }
1685                self.toolkit.enable_gesture(&h, rnode_to_id(node), kind);
1686                return;
1687            }
1688            match n.children.as_slice() {
1689                [only] => cur = *only,
1690                _ => break,
1691            }
1692        }
1693        log::warn!(
1694            "enable_gesture({kind:?}) found no native view under the target node — \
1695             the gesture will not fire natively (attach it to a piece with a native handle)"
1696        );
1697    }
1698
1699    fn focus_node(&mut self, node: RNode, focused: bool) {
1700        // The wrapper-descent `set_focusable` makes: an id (and so a `focus:` step) often lands
1701        // on a layout-only wrapper (`.frame`, `.padding`), and focusing it did nothing at all.
1702        let mut cur = node;
1703        for _ in 0..16 {
1704            let Some(n) = self.nodes.get(cur) else { return };
1705            if let Some(h) = n.handle.clone() {
1706                self.toolkit.focus(&h, rnode_to_id(node), focused);
1707                return;
1708            }
1709            match n.children.as_slice() {
1710                [only] => cur = *only,
1711                _ => return,
1712            }
1713        }
1714    }
1715
1716    fn set_focusable(&mut self, node: RNode, focusable: bool) {
1717        // Same wrapper-descent as `enable_gesture`: `.focusable()` often lands on a
1718        // layout-only wrapper; the duty needs the nearest native view, events keep the
1719        // original node.
1720        let mut cur = node;
1721        for _ in 0..16 {
1722            let Some(n) = self.nodes.get(cur) else { return };
1723            if let Some(h) = n.handle.clone() {
1724                self.toolkit.set_focusable(&h, rnode_to_id(node), focusable);
1725                return;
1726            }
1727            match n.children.as_slice() {
1728                [only] => cur = *only,
1729                _ => break,
1730            }
1731        }
1732        log::warn!(
1733            "set_focusable found no native view under the target node — \
1734             the piece will not join the focus order"
1735        );
1736    }
1737
1738    fn set_probe_focused(&mut self, node: RNode, focused: bool) {
1739        if let Some(n) = self.nodes.get_mut(node) {
1740            n.probe.focused = focused;
1741        }
1742        // The layout-only wrappers directly above a native view are the same element: an id
1743        // (`.frame(..).id(..)`) and a modifier often sit on one, while the toolkit reports
1744        // focus on the view itself. Mirror the state up through them, the inverse of the
1745        // descent `set_focusable` and `focus_node` make.
1746        for w in self.wrappers_above(node) {
1747            if let Some(p) = self.nodes.get_mut(w) {
1748                p.probe.focused = focused;
1749            }
1750        }
1751        // And down through the single-child layout-only chain to the native view: the
1752        // toolkit reports focus against the node that enabled it (a `.focusable()` or
1753        // `.focused(..)` wrapper), while `.id` names the piece's own node below it
1754        // (docs/api-style.md "Modifier classes"), which is what a dayscript `assert_focused`
1755        // resolves. Both are the same element.
1756        let mut cur = node;
1757        for _ in 0..16 {
1758            let Some(n) = self.nodes.get(cur) else { break };
1759            if n.handle.is_some() && cur != node {
1760                if let Some(p) = self.nodes.get_mut(cur) {
1761                    p.probe.focused = focused;
1762                }
1763                break;
1764            }
1765            let next = match n.children.as_slice() {
1766                [only] => *only,
1767                _ => break,
1768            };
1769            if let Some(p) = self.nodes.get_mut(next) {
1770                p.probe.focused = focused;
1771            }
1772            cur = next;
1773        }
1774        // Remember which node has it, beyond the per-node flag: keys follow focus
1775        // (docs/menus.md), so dayscript's `key:` step needs the same answer the platform gives.
1776        // A loss only clears the record if this node is still the one holding it; gains land
1777        // before losses in the pump, so a hand-off has already named the new owner.
1778        FOCUSED.with(|f| match focused {
1779            true => f.set(Some(node)),
1780            false if f.get() == Some(node) => f.set(None),
1781            false => {}
1782        });
1783    }
1784
1785    fn set_probe_selected(&mut self, node: RNode, selected: Option<usize>) {
1786        if let Some(n) = self.nodes.get_mut(node) {
1787            // Both fields, as a picker does: `assert_value` reads `value` for a number and
1788            // `assert_selected` reads `selected`, and a nav host answers either.
1789            n.probe.selected = selected.map(|i| i as i64).unwrap_or(-1);
1790            n.probe.value = n.probe.selected as f64;
1791        }
1792    }
1793
1794    fn set_probe_value(&mut self, node: RNode, value: f64, text: String) {
1795        if let Some(n) = self.nodes.get_mut(node) {
1796            n.probe.value = value;
1797            n.probe.text = text;
1798        }
1799    }
1800
1801    fn set_app_menu(&mut self, items: Vec<day_spec::MenuItem>) {
1802        collect_menu_families(&items, &mut self.mounted_id_families);
1803        self.toolkit.set_app_menu(&items);
1804    }
1805
1806    fn edit_window_toolbar(&mut self, root: RNode, ops: Vec<day_spec::ToolbarOp>) -> bool {
1807        let Some(h) = self.nodes.get(root).and_then(|n| n.handle.clone()) else {
1808            return false;
1809        };
1810        for op in &ops {
1811            if let day_spec::ToolbarOp::Insert { item, .. } = op {
1812                self.mounted_id_families
1813                    .insert(crate::element_id::family_of(&item.id).to_owned());
1814            }
1815        }
1816        self.toolkit.edit_toolbar(&h, &ops)
1817    }
1818
1819    fn patch_window_toolbar(&mut self, root: RNode, patch: day_spec::ToolbarPatch) -> bool {
1820        let Some(h) = self.nodes.get(root).and_then(|n| n.handle.clone()) else {
1821            return false;
1822        };
1823        self.toolkit.update_toolbar(&h, &patch);
1824        true
1825    }
1826
1827    fn set_context_menu(&mut self, node: RNode, items: Vec<day_spec::MenuItem>) {
1828        // The modifier often sits on a handle-less layout wrapper (`.padding`/`.frame` build
1829        // those): the native affordance needs a real view, so fall through to the first
1830        // native root under the node. Silently dropping the menu here is what made
1831        // `label(..).padding(..).context_menu(..)` dead on every backend.
1832        let target = if self.nodes.get(node).is_some_and(|n| n.handle.is_some()) {
1833            Some(node)
1834        } else {
1835            let mut roots = Vec::new();
1836            self.native_descendants(node, &mut roots);
1837            roots.first().copied()
1838        };
1839        let Some(t) = target else {
1840            log::warn!("context_menu on a subtree with no native view — menu dropped");
1841            return;
1842        };
1843        collect_menu_families(&items, &mut self.mounted_id_families);
1844        let model = ContextMenuModel::Items(items.clone());
1845        self.context_menus.insert(node, model.clone());
1846        self.context_menus.insert(t, model);
1847        if let Some(h) = self.nodes.get(t).and_then(|n| n.handle.clone()) {
1848            self.toolkit.set_context_menu(&h, rnode_to_id(t), &items);
1849        }
1850    }
1851
1852    fn scroll_to_target(&mut self, node: RNode, target: &ScrollTarget, animated: bool) -> bool {
1853        // `Id` routes through reveal: the element names the scroll implicitly.
1854        if let ScrollTarget::Id(id) = target {
1855            let Some(el) = self.find_by_id(id) else {
1856                return false;
1857            };
1858            return self.scroll_reveal(el, animated);
1859        }
1860        let Some(n) = self.nodes.get(node) else {
1861            return false;
1862        };
1863        if n.kind == kinds::NAV_MENU {
1864            return match (n.handle.clone(), target) {
1865                (Some(h), ScrollTarget::Top | ScrollTarget::Bottom) => self
1866                    .toolkit
1867                    .nav_scroll_to_edge(&h, matches!(target, ScrollTarget::Bottom)),
1868                _ => false,
1869            };
1870        }
1871        if n.kind != kinds::SCROLL {
1872            return false;
1873        }
1874        let Some(h) = n.handle.clone() else {
1875            return false;
1876        };
1877        let viewport = n.last_native_frame.map(|f| f.size).unwrap_or(Size::ZERO);
1878        let content = n.scroll_content.unwrap_or(viewport);
1879        // Compose a content-space rect whose minimal reveal lands on the target
1880        // (`Toolkit::scroll_to` is scrollRectToVisible semantics on every backend).
1881        let rect = match target {
1882            ScrollTarget::Top | ScrollTarget::Leading => Rect::new(0.0, 0.0, 1.0, 1.0),
1883            ScrollTarget::Bottom => Rect::new(0.0, (content.height - 1.0).max(0.0), 1.0, 1.0),
1884            ScrollTarget::Trailing => Rect::new((content.width - 1.0).max(0.0), 0.0, 1.0, 1.0),
1885            // A viewport-sized rect: minimal reveal pins the viewport origin to the point
1886            // (exactly, when the point is within the scrollable range).
1887            ScrollTarget::Offset(p) => Rect::new(p.x, p.y, viewport.width, viewport.height),
1888            ScrollTarget::Id(_) => unreachable!("routed to scroll_reveal above"),
1889        };
1890        self.toolkit
1891            .scroll_to(&h, rect, animated && !crate::reduce_motion_now());
1892        true
1893    }
1894
1895    fn scroll_reveal(&mut self, node: RNode, animated: bool) -> bool {
1896        // The element's frame is relative to its nearest REALIZED native ancestor (§7);
1897        // accumulate native origins up to (not including) the enclosing scroll, which puts
1898        // the rect in the scroll's content space, which is what Toolkit::scroll_to expects.
1899        let Some(mut rect) = self.nodes.get(node).and_then(|n| n.last_native_frame) else {
1900            return false;
1901        };
1902        let mut anc = match self.nodes.get(node) {
1903            Some(n) => n.parent,
1904            None => return false,
1905        };
1906        loop {
1907            let Some(a) = self.nodes.get(anc) else {
1908                return false; // walked off the root: no scroll ancestor
1909            };
1910            if a.kind == kinds::SCROLL {
1911                let Some(h) = a.handle.clone() else {
1912                    return false;
1913                };
1914                self.toolkit
1915                    .scroll_to(&h, rect, animated && !crate::reduce_motion_now());
1916                return true;
1917            }
1918            if a.handle.is_some()
1919                && let Some(f) = a.last_native_frame
1920            {
1921                rect.origin.x += f.origin.x;
1922                rect.origin.y += f.origin.y;
1923            }
1924            anc = a.parent;
1925        }
1926    }
1927
1928    fn patch(&mut self, node: RNode, patch: Box<dyn Any>, affects_size: bool) {
1929        {
1930            use day_spec::props::*;
1931            if let Some(n) = self.nodes.get_mut(node) {
1932                if let Some(p) = patch.downcast_ref::<LabelPatch>() {
1933                    // Both text-carrying variants: a `.markdown()` label re-parses through
1934                    // `Runs`, and a probe that only tracked `Text` would report the string the
1935                    // label was born with forever (docs/text-runs.md).
1936                    match p {
1937                        LabelPatch::Text(t) => n.probe.text = t.clone(),
1938                        LabelPatch::Runs(t, _) => n.probe.text = t.clone(),
1939                        LabelPatch::Font(_) | LabelPatch::Color(_) => {}
1940                    }
1941                } else if let Some(p) = patch.downcast_ref::<ButtonPatch>() {
1942                    match p {
1943                        ButtonPatch::Title(t) => n.probe.text = t.clone(),
1944                        ButtonPatch::Content(c) => n.probe.text = c.title.clone(),
1945                        ButtonPatch::Enabled(e) => n.probe.enabled = *e,
1946                        ButtonPatch::Selected(on) => n.probe.flag = *on,
1947                        // The style is a look, not something a probe asserts on.
1948                        ButtonPatch::Style(_) => {}
1949                    }
1950                } else if let Some(p) = patch.downcast_ref::<TogglePatch>() {
1951                    match p {
1952                        TogglePatch::On(v) => n.probe.flag = *v,
1953                        TogglePatch::Enabled(e) => n.probe.enabled = *e,
1954                    }
1955                } else if let Some(p) = patch.downcast_ref::<SliderPatch>() {
1956                    match p {
1957                        SliderPatch::Value(v) => n.probe.value = *v,
1958                        SliderPatch::Enabled(e) => n.probe.enabled = *e,
1959                    }
1960                } else if let Some(p) = patch.downcast_ref::<PickerPatch>() {
1961                    match p {
1962                        PickerPatch::Selected(i) => {
1963                            n.probe.selected = *i as i64;
1964                            n.probe.value = *i as f64;
1965                        }
1966                        PickerPatch::Options(opts) => n.probe.options = opts.clone(),
1967                        PickerPatch::Enabled(e) => n.probe.enabled = *e,
1968                    }
1969                    let i = n.probe.selected.max(0) as usize;
1970                    n.probe.text = n.probe.options.get(i).cloned().unwrap_or_default();
1971                } else if let Some(TextAreaPatch::SetText(t)) =
1972                    patch.downcast_ref::<TextAreaPatch>()
1973                {
1974                    n.probe.text = t.clone();
1975                } else if let Some(ProgressPatch::Value(v)) = patch.downcast_ref::<ProgressPatch>()
1976                {
1977                    n.probe.flag = v.is_none();
1978                    n.probe.value = v.unwrap_or(0.0);
1979                } else if let Some(p) = patch.downcast_ref::<TextFieldPatch>() {
1980                    match p {
1981                        TextFieldPatch::Text { text, .. } => n.probe.text = text.clone(),
1982                        TextFieldPatch::Enabled(e) => n.probe.enabled = *e,
1983                        _ => {}
1984                    }
1985                } else if let Some(NavMenuPatch::Selected(sel)) =
1986                    patch.downcast_ref::<NavMenuPatch>()
1987                {
1988                    n.probe.selected = sel.map(|i| i as i64).unwrap_or(-1);
1989                    n.probe.value = n.probe.selected as f64;
1990                }
1991            }
1992        }
1993        let Some(n) = self.nodes.get(node) else {
1994            return;
1995        };
1996        let kind = n.kind;
1997        if let Some(h) = n.handle.clone() {
1998            let anim = self.resolve_anim(node);
1999            self.toolkit.update(&h, kind, patch.as_ref(), anim.as_ref());
2000        }
2001        if affects_size {
2002            self.mark_needs_measure_impl(node);
2003        }
2004    }
2005
2006    fn set_implicit_anim(&mut self, node: RNode, anim: Option<day_spec::AnimSpec>) {
2007        let had = self
2008            .nodes
2009            .get(node)
2010            .map(|n| n.implicit_anim.is_some())
2011            .unwrap_or(false);
2012        if let Some(n) = self.nodes.get_mut(node) {
2013            n.implicit_anim = anim;
2014        } else {
2015            return;
2016        }
2017        match (had, anim.is_some()) {
2018            (false, true) => self.implicit_anim_count += 1,
2019            (true, false) => self.implicit_anim_count = self.implicit_anim_count.saturating_sub(1),
2020            _ => {}
2021        }
2022    }
2023
2024    fn set_node_opacity(&mut self, node: RNode, opacity: f64) {
2025        let Some(h) = self.nodes.get(node).and_then(|n| n.handle.clone()) else {
2026            return;
2027        };
2028        let anim = self.resolve_anim(node);
2029        self.toolkit.set_opacity(&h, opacity, anim.as_ref());
2030    }
2031
2032    fn set_node_transform(&mut self, node: RNode, t: day_spec::Transform) {
2033        let Some(n) = self.nodes.get(node) else {
2034            return;
2035        };
2036        let Some(h) = n.handle.clone() else {
2037            return;
2038        };
2039        // The node's laid-out size, passed so backends resolve the transform's anchor to a pixel
2040        // pivot without querying the (possibly not-yet-allocated) native widget (§8.4).
2041        let size = n.last_native_frame.map(|f| f.size).unwrap_or(Size::ZERO);
2042        let anim = self.resolve_anim(node);
2043        self.toolkit.set_transform(&h, t, size, anim.as_ref());
2044    }
2045
2046    fn set_node_selectable(&mut self, node: RNode, selectable: bool) {
2047        let Some(h) = self.nodes.get(node).and_then(|n| n.handle.clone()) else {
2048            return;
2049        };
2050        // A toolkit may have to rebuild the widget as a selection-capable class (UIKit's
2051        // label → read-only text view); adopt the replacement so patches and layout follow it.
2052        if let Some(new) = self.toolkit.set_selectable(&h, selectable)
2053            && let Some(n) = self.nodes.get_mut(node)
2054        {
2055            if n.tweaked {
2056                log::warn!(
2057                    "`.selectable()` rebuilt this widget as a different native class, \
2058                     discarding an earlier tweak's changes — apply `.selectable()` BEFORE the \
2059                     tweak so it runs against the widget that ships (docs/tweaks.md)."
2060                );
2061            }
2062            n.handle = Some(new);
2063        }
2064    }
2065
2066    fn set_node_input_traits(&mut self, node: RNode, traits: &day_spec::InputTraits) {
2067        let Some(h) = self.nodes.get(node).and_then(|n| n.handle.clone()) else {
2068            return;
2069        };
2070        // A toolkit whose secure field is its own class rebuilds the widget (AppKit, WinUI);
2071        // adopt the replacement so patches, layout and focus follow it.
2072        if let Some(new) = self.toolkit.set_input_traits(&h, traits)
2073            && let Some(n) = self.nodes.get_mut(node)
2074        {
2075            if n.tweaked {
2076                log::warn!(
2077                    "a text field's `.secure()` rebuilt this widget as a different native \
2078                     class, discarding an earlier tweak's changes (docs/tweaks.md)."
2079                );
2080            }
2081            n.handle = Some(new);
2082            n.last_native_frame = None;
2083            self.mark_needs_measure_impl(node);
2084        }
2085    }
2086
2087    fn set_node_cursor(&mut self, node: RNode, cursor: day_spec::Cursor) {
2088        // The wrapper descent `enable_gesture` makes: `.padding(…).cursor(…)` lands on a
2089        // layout-only wrapper with no view of its own, and the cursor belongs on the view it
2090        // wraps.
2091        let Some(h) = self
2092            .native_view_of(node)
2093            .and_then(|n| self.nodes.get(n))
2094            .and_then(|n| n.handle.clone())
2095        else {
2096            return;
2097        };
2098        self.toolkit.set_cursor(&h, cursor);
2099    }
2100
2101    fn set_node_drag_region(&mut self, node: RNode, drag: bool) {
2102        // Same descent: `row(…).padding(8.0).window_drag_region()` marks the row.
2103        let Some(h) = self
2104            .native_view_of(node)
2105            .and_then(|n| self.nodes.get(n))
2106            .and_then(|n| n.handle.clone())
2107        else {
2108            log::warn!(
2109                "window_drag_region found no native view under the target node; give the \
2110                 region a piece with a native view (a background, a row)"
2111            );
2112            return;
2113        };
2114        self.toolkit.set_drag_region(&h, drag);
2115    }
2116
2117    fn note_node_tweaked(&mut self, node: RNode) {
2118        if let Some(n) = self.nodes.get_mut(node) {
2119            n.tweaked = true;
2120        }
2121    }
2122
2123    fn request_frame(&mut self, root: RNode, cb: day_spec::FrameCallback) -> day_spec::CancelFrame {
2124        if let Some(h) = self.nodes.get(root).and_then(|n| n.handle.clone()) {
2125            self.toolkit.request_frame(&h, cb)
2126        } else {
2127            Box::new(|| {})
2128        }
2129    }
2130
2131    fn replay(&mut self, node: RNode, ops: Vec<DrawOp>) {
2132        let Some(n) = self.nodes.get(node) else {
2133            return;
2134        };
2135        let size = n.last_native_frame.map(|f| f.size).unwrap_or(Size::ZERO);
2136        if let Some(h) = n.handle.clone() {
2137            self.toolkit.replay(&h, &ops, size);
2138        }
2139    }
2140
2141    fn mark_needs_measure(&mut self, node: RNode) {
2142        self.mark_needs_measure_impl(node);
2143    }
2144
2145    fn mark_layout_dirty(&mut self) {
2146        self.layout_dirty = true;
2147    }
2148
2149    fn invalidate_all_measurements(&mut self) {
2150        for (_, n) in self.nodes.iter_mut() {
2151            n.needs_measure = true;
2152            n.needs_layout = true;
2153            n.cache.clear();
2154        }
2155        // Cells keep a row's measurements while its width holds; forgetting the width makes the
2156        // dirty-cell sweep measure each bound row afresh, and a self-sizing row whose height
2157        // changes reports it (`ListPatch::RowSizeInvalidated`).
2158        for state in self.lists.values_mut() {
2159            for cell in state.cells.values_mut() {
2160                cell.laid_width = None;
2161            }
2162        }
2163        for state in self.trees.values_mut() {
2164            for cell in state.cells.values_mut() {
2165                cell.laid_width = None;
2166            }
2167        }
2168        self.layout_dirty = true;
2169    }
2170
2171    fn layout_if_needed(&mut self) {
2172        if !self.layout_dirty {
2173            return;
2174        }
2175        self.layout_dirty = false;
2176        self.layout_now();
2177    }
2178
2179    fn set_route(&mut self, route: &str) {
2180        self.toolkit.set_route(route);
2181    }
2182
2183    fn set_window_size(&mut self, s: Size) {
2184        // The primary window (`WINDOW_NODE` routing); secondary windows resize through
2185        // `set_root_size` with their own root.
2186        if s != self.windows[0].size {
2187            self.windows[0].size = s;
2188            let root = self.windows[0].root;
2189            self.mark_needs_measure_impl(root);
2190        }
2191    }
2192
2193    fn child_count(&self, node: RNode) -> usize {
2194        self.nodes.get(node).map(|n| n.children.len()).unwrap_or(0)
2195    }
2196
2197    fn first_child(&self, node: RNode) -> Option<RNode> {
2198        self.nodes
2199            .get(node)
2200            .and_then(|n| n.children.first().copied())
2201    }
2202
2203    fn parent_of(&self, node: RNode) -> Option<RNode> {
2204        self.nodes
2205            .get(node)
2206            .map(|n| n.parent)
2207            .filter(|p| !p.is_null() && *p != node)
2208    }
2209
2210    fn node_kind(&self, node: RNode) -> Option<PieceKind> {
2211        self.nodes.get(node).map(|n| n.kind)
2212    }
2213
2214    fn node_id(&self, node: RNode) -> Option<String> {
2215        self.nodes.get(node).and_then(|n| n.id.clone())
2216    }
2217
2218    fn node_handle_any(&self, node: RNode) -> Option<Box<dyn Any>> {
2219        self.nodes
2220            .get(node)
2221            .and_then(|n| n.handle.clone())
2222            .map(|h| Box::new(h) as Box<dyn Any>)
2223    }
2224
2225    fn node_frame(&self, node: RNode) -> Option<Rect> {
2226        self.nodes.get(node).and_then(|n| n.last_native_frame)
2227    }
2228
2229    fn node_probe(&self, node: RNode) -> Option<NodeProbe> {
2230        self.nodes.get(node).map(|n| n.probe.clone())
2231    }
2232
2233    fn node_a11y(&self, node: RNode) -> Option<A11yProps> {
2234        self.nodes.get(node).map(|n| n.a11y.clone())
2235    }
2236
2237    fn read_native(&self, node: RNode) -> Option<day_spec::NativeSnapshot> {
2238        let n = self.nodes.get(node)?;
2239        let h = n.handle.as_ref()?;
2240        Some(self.toolkit.read_native(h))
2241    }
2242
2243    fn a11y_nodes(&self) -> Vec<(String, PieceKind, A11yProps, day_spec::NativeSnapshot)> {
2244        self.nodes
2245            .values()
2246            .filter_map(|n| {
2247                let id = n.id.clone()?;
2248                let h = n.handle.as_ref()?;
2249                Some((id, n.kind, n.a11y.clone(), self.toolkit.read_native(h)))
2250            })
2251            .collect()
2252    }
2253
2254    fn find_by_id(&self, id: &str) -> Option<RNode> {
2255        // A settings page can exist both in the app sidebar and in its focused window/cover.
2256        // Slot order can otherwise mix fields from one form with the Save button of another.
2257        let focused = crate::windows::focused_window()
2258            .map(|w| id_to_rnode(crate::windows::window_node_id(&w)))
2259            .unwrap_or_else(|| self.root());
2260        let mut fallback = None;
2261        for (key, node) in &self.nodes {
2262            if node.id.as_deref() != Some(id) {
2263                continue;
2264            }
2265            fallback.get_or_insert(key);
2266            let mut ancestor = key;
2267            while let Some(node) = self.nodes.get(ancestor) {
2268                // Covers are children of the primary root but own a separate layout root.
2269                // Stop at the nearest root so a closing cover cannot masquerade as primary.
2270                if self.windows.iter().any(|w| w.root == ancestor) {
2271                    if ancestor == focused {
2272                        return Some(key);
2273                    }
2274                    break;
2275                }
2276                ancestor = if node.kind == kinds::LIST_CELL && node.parent.is_null() {
2277                    // Recycling-list/tree cells are independent layout anchors, not children
2278                    // in the owner's layout. Follow their logical owner for window lookup.
2279                    self.lists
2280                        .iter()
2281                        .find_map(|(&owner, state)| {
2282                            state
2283                                .cells
2284                                .values()
2285                                .any(|c| c.anchor == ancestor)
2286                                .then_some(owner)
2287                        })
2288                        .or_else(|| {
2289                            self.trees.iter().find_map(|(&owner, state)| {
2290                                state
2291                                    .cells
2292                                    .values()
2293                                    .any(|c| c.anchor == ancestor)
2294                                    .then_some(owner)
2295                            })
2296                        })
2297                        .unwrap_or(node.parent)
2298                } else {
2299                    node.parent
2300                };
2301            }
2302        }
2303        fallback
2304    }
2305
2306    fn toggle_sidebar(&mut self, host: RNode) -> bool {
2307        let Some(h) = self.nodes.get(host).and_then(|n| n.handle.as_ref()) else {
2308            return false;
2309        };
2310        self.toolkit.toggle_sidebar(h)
2311    }
2312
2313    fn native_back(&mut self) -> bool {
2314        self.toolkit.native_back()
2315    }
2316
2317    fn prepare_snapshot(
2318        &mut self,
2319        root: Option<RNode>,
2320        revision: u32,
2321    ) -> Result<day_spec::capture::Readiness, String> {
2322        let host = match root {
2323            Some(root) if self.windows[0].root != root => Some(
2324                self.nodes
2325                    .get(root)
2326                    .and_then(|n| n.handle.clone())
2327                    .ok_or("window is gone")?,
2328            ),
2329            _ => None,
2330        };
2331        self.toolkit.prepare_snapshot(host.as_ref(), revision)
2332    }
2333
2334    fn snapshot(&mut self) -> Result<Vec<u8>, String> {
2335        self.toolkit.snapshot_window()
2336    }
2337
2338    fn snapshot_origin(&mut self) -> Option<Point> {
2339        let root = self.windows[0].root;
2340        let h = self.nodes.get(root).and_then(|n| n.handle.clone())?;
2341        self.toolkit.snapshot_origin(&h)
2342    }
2343
2344    fn snapshot_chrome(&mut self) -> Result<Vec<u8>, String> {
2345        self.toolkit.snapshot_window_chrome()
2346    }
2347
2348    fn decode_image(&mut self, req: u64, id: day_spec::BitmapId, bytes: &[u8]) {
2349        self.toolkit.decode_image(req, id, bytes);
2350    }
2351
2352    fn image_info(&mut self, id: day_spec::BitmapId) -> Option<day_spec::BitmapInfo> {
2353        self.toolkit.image_info(id)
2354    }
2355
2356    fn image_properties(&mut self, id: day_spec::BitmapId) -> Option<day_spec::ImageProperties> {
2357        self.toolkit.image_properties(id)
2358    }
2359
2360    fn encode_image(&mut self, req: u64, id: day_spec::BitmapId, spec: &day_spec::EncodeSpec) {
2361        self.toolkit.encode_image(req, id, spec);
2362    }
2363
2364    fn image_encode_formats(&mut self) -> Vec<day_spec::ImageFormat> {
2365        self.toolkit.encode_formats()
2366    }
2367
2368    fn release_image(&mut self, id: day_spec::BitmapId) {
2369        self.toolkit.release_image(id);
2370    }
2371
2372    fn render_bitmap(
2373        &mut self,
2374        req: u64,
2375        id: day_spec::BitmapId,
2376        ops: &[day_spec::DrawOp],
2377        size: Size,
2378        scale: f64,
2379    ) {
2380        self.toolkit.render_bitmap(req, id, ops, size, scale);
2381    }
2382
2383    fn render_pdf(&mut self, req: u64, pages: &[day_spec::PdfPage], info: &day_spec::PdfInfo) {
2384        self.toolkit.render_pdf(req, pages, info);
2385    }
2386
2387    fn window_image_support(&mut self) -> day_spec::Support {
2388        self.toolkit.capability(day_spec::Cap::Snapshot)
2389    }
2390
2391    fn font_families(&mut self) -> Vec<day_spec::FontFamilyInfo> {
2392        self.toolkit.font_families()
2393    }
2394
2395    fn measure_text(
2396        &mut self,
2397        text: &str,
2398        size: f64,
2399        font: &day_spec::CanvasFont,
2400    ) -> Option<day_spec::TextMetrics> {
2401        self.toolkit.measure_text(text, size, font)
2402    }
2403
2404    fn ui_idle(&mut self) -> bool {
2405        self.toolkit.ui_idle()
2406    }
2407
2408    fn root_node(&self) -> RNode {
2409        self.windows[0].root
2410    }
2411
2412    fn open_window_root(&mut self, options: &WindowOptions, kind: WindowKind) -> WindowRootReply {
2413        // The same "wrap an externally-owned handle" record as the primary root and the
2414        // list cell anchors; the handle arrives now (desktop) or at adoption (mobile).
2415        let root = self.nodes.insert(NodeData {
2416            kind: kinds::CONTAINER,
2417            handle: None,
2418            parent: RNode::null(),
2419            children: Vec::new(),
2420            layout: Rc::new(PassThrough),
2421            flex: Flex::default(),
2422            scope: Scope::root(),
2423            id: None,
2424            parked_id: None,
2425            a11y: Default::default(),
2426            cache: Vec::new(),
2427            baseline_cache: None,
2428            probe: NodeProbe::default(),
2429            needs_measure: true,
2430            needs_layout: true,
2431            last_native_frame: None,
2432            scroll_content: None,
2433            implicit_anim: None,
2434            tweaked: false,
2435            is_boundary: true,
2436        });
2437        match self.toolkit.open_window(rnode_to_id(root), options, kind) {
2438            day_spec::WindowOpenReply::Open(h) => {
2439                self.nodes[root].handle = Some(h);
2440                self.windows.push(WindowEntry {
2441                    root,
2442                    size: options.size,
2443                });
2444                self.layout_dirty = true;
2445                WindowRootReply::Open(root)
2446            }
2447            day_spec::WindowOpenReply::Pending => WindowRootReply::Pending(root),
2448            day_spec::WindowOpenReply::Unsupported => {
2449                self.nodes.remove(root);
2450                WindowRootReply::Unsupported
2451            }
2452        }
2453    }
2454
2455    fn adopt_window_root(&mut self, root: RNode, raw: day_spec::RawHandle, size: Size) -> bool {
2456        let Some(node) = self.nodes.get_mut(root) else {
2457            return false; // closed before the native side finished; the caller drops the window
2458        };
2459        let handle = self.toolkit.adopt(raw);
2460        node.handle = Some(handle);
2461        node.needs_measure = true;
2462        self.windows.push(WindowEntry { root, size });
2463        self.layout_dirty = true;
2464        true
2465    }
2466
2467    fn remove_window_root(&mut self, root: RNode) {
2468        // Any window may go, including the first one opened (docs/windows.md close policy);
2469        // the app's life is the life of its primary windows, not of `windows[0]` specifically.
2470        //
2471        // The last entry is kept, because `root()` has to answer for the whole tree and callers
2472        // are in no position to handle "no windows". Reaching that point means the last window
2473        // has closed, which is the app exiting: its content is already gone (teardown removes
2474        // the children first) and the empty shell outlives nothing.
2475        if self.windows.len() <= 1 {
2476            self.collect_and_release(root);
2477            self.layout_dirty = true;
2478            return;
2479        }
2480        self.windows.retain(|w| w.root != root);
2481        // No native detach: a window root has no native parent; the platform window is
2482        // already closed (or the backend releases it with the content handle).
2483        self.collect_and_release(root);
2484        self.layout_dirty = true;
2485    }
2486
2487    fn set_root_size(&mut self, root: RNode, s: Size) {
2488        if let Some(w) = self.windows.iter_mut().find(|w| w.root == root)
2489            && w.size != s
2490        {
2491            w.size = s;
2492            self.mark_needs_measure_impl(root);
2493        }
2494    }
2495
2496    fn fit_window(&mut self, root: RNode, size: Size) {
2497        if let Some(h) = self.nodes.get(root).and_then(|n| n.handle.clone()) {
2498            self.toolkit.fit_window(&h, size);
2499        }
2500        self.set_root_size(root, size);
2501    }
2502
2503    fn add_extra_layout_root(&mut self, node: RNode, size: Size) {
2504        if !self.windows.iter().any(|w| w.root == node) {
2505            self.windows.push(WindowEntry { root: node, size });
2506            self.layout_dirty = true;
2507        }
2508    }
2509
2510    fn drop_extra_layout_root(&mut self, node: RNode) {
2511        debug_assert!(
2512            self.windows.first().map(|w| w.root) != Some(node),
2513            "drop_extra_layout_root called with the primary root"
2514        );
2515        self.windows.retain(|w| w.root != node);
2516    }
2517
2518    fn close_native_window(&mut self, root: RNode) {
2519        if let Some(h) = self.nodes.get(root).and_then(|n| n.handle.clone()) {
2520            self.toolkit.close_window(&h);
2521        }
2522    }
2523
2524    fn quit_app(&mut self) {
2525        self.toolkit.quit_app();
2526    }
2527
2528    fn focus_native_window(&mut self, root: RNode) {
2529        if let Some(h) = self.nodes.get(root).and_then(|n| n.handle.clone()) {
2530            self.toolkit.focus_window(&h);
2531        }
2532    }
2533
2534    fn set_native_window_title(&mut self, root: RNode, title: &str) {
2535        if let Some(h) = self.nodes.get(root).and_then(|n| n.handle.clone()) {
2536            self.toolkit.set_window_title(&h, title);
2537        }
2538    }
2539
2540    fn apply_window(&mut self, root: RNode, change: &day_spec::WindowChange) {
2541        if let Some(h) = self.nodes.get(root).and_then(|n| n.handle.clone()) {
2542            self.toolkit.apply_window(&h, change);
2543        }
2544    }
2545
2546    fn set_dock_menu(&mut self, items: &[day_spec::MenuItem]) {
2547        self.toolkit.set_dock_menu(items);
2548    }
2549
2550    fn set_status_items(&mut self, items: &[day_spec::StatusItemSpec]) {
2551        self.toolkit.set_status_items(items);
2552    }
2553
2554    fn set_app_progress(&mut self, progress: day_spec::AppProgress) {
2555        self.toolkit.set_app_progress(progress);
2556    }
2557
2558    fn set_dock_visible(&mut self, visible: bool) {
2559        self.toolkit.set_dock_visible(visible);
2560    }
2561
2562    fn set_keep_running(&mut self, keep: bool) {
2563        self.toolkit.set_keep_running(keep);
2564    }
2565
2566    fn native_window_frame(&self, root: RNode) -> Option<day_spec::Rect> {
2567        let h = self.nodes.get(root).and_then(|n| n.handle.clone())?;
2568        self.toolkit.window_frame(&h)
2569    }
2570
2571    fn monitors(&self) -> Vec<day_spec::Monitor> {
2572        self.toolkit.monitors()
2573    }
2574
2575    fn set_launcher_shortcuts(&mut self, shortcuts: &[day_spec::LauncherShortcut]) {
2576        self.toolkit.set_launcher_shortcuts(shortcuts);
2577    }
2578
2579    fn snapshot_of(&mut self, root: RNode) -> Result<Vec<u8>, String> {
2580        if self.windows[0].root == root {
2581            return self.toolkit.snapshot_window();
2582        }
2583        match self.nodes.get(root).and_then(|n| n.handle.clone()) {
2584            Some(h) => self.toolkit.snapshot_window_of(&h),
2585            None => Err("window is gone".into()),
2586        }
2587    }
2588
2589    fn install_list(&mut self, node: RNode, driver: crate::list::ListDriver) {
2590        let driver = Rc::new(driver);
2591        let sections = driver
2592            .sections
2593            .is_some()
2594            .then(|| crate::list::register_sections(node, driver.clone()));
2595        self.lists.insert(
2596            node,
2597            crate::list::ListState {
2598                driver: driver.clone(),
2599                cells: HashMap::new(),
2600                sections: sections.clone(),
2601            },
2602        );
2603        let source = crate::list::make_source(node, driver, sections);
2604        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
2605            self.toolkit.attach_list(&handle, source);
2606        }
2607    }
2608
2609    fn list_prepare_cell(
2610        &mut self,
2611        node: RNode,
2612        key: usize,
2613        cell: RawHandle,
2614        header: bool,
2615    ) -> crate::list::CellStep {
2616        // A cell that held the other kind (a header's cell reused for an item, or the reverse)
2617        // cannot be rebound: its subtree is the wrong piece. Dispose it as a release would and
2618        // build afresh in the same native cell.
2619        if let Some(state) = self.lists.get_mut(&node)
2620            && state.cells.get(&key).is_some_and(|b| b.header != header)
2621            && let Some(old) = state.cells.remove(&key)
2622        {
2623            old.scope.dispose();
2624            self.remove_subtree_impl(old.anchor);
2625        }
2626        if let Some(state) = self.lists.get_mut(&node)
2627            && let Some(bound) = state.cells.get_mut(&key)
2628        {
2629            bound.active = true;
2630            let (rebind, anchor, pinned) = (bound.rebind.clone(), bound.anchor, bound.pinned);
2631            // A pooled cell coming back: its ids were parked when it was recycled, and a
2632            // rebind to unchanged content would never re-set them (see `parked_id`). A pinned
2633            // header copy keeps its ids parked.
2634            if !pinned {
2635                self.restore_subtree_ids(anchor);
2636            }
2637            return crate::list::CellStep::Rebind { rebind, anchor };
2638        }
2639        // First use of this cell: adopt the native cell and anchor a fresh subtree under it,
2640        // inside the platform's row margins (docs/list.md "Row metrics").
2641        let mut metrics = match self.nodes.get(node).and_then(|n| n.handle.clone()) {
2642            Some(list) => self.toolkit.list_row_metrics(&list),
2643            None => day_spec::RowMetrics::default(),
2644        };
2645        // A section header lines up with the rows (the side insets) but is no row: its own
2646        // padding sets its height, and the platform's least row height is a touch target's,
2647        // which a header is not.
2648        if header {
2649            metrics.insets.top = 0.0;
2650            metrics.insets.bottom = 0.0;
2651            metrics.min_height = 0.0;
2652        }
2653        let handle = self.toolkit.adopt(cell);
2654        let anchor = self.create_cell_anchor(
2655            handle,
2656            Scope::child(),
2657            Rc::new(crate::layout::CellMetrics(metrics)),
2658        );
2659        crate::list::CellStep::Build { anchor }
2660    }
2661
2662    fn list_store_cell(
2663        &mut self,
2664        node: RNode,
2665        key: usize,
2666        anchor: RNode,
2667        built: crate::list::BuiltRow,
2668        header: bool,
2669    ) {
2670        if let Some(state) = self.lists.get_mut(&node) {
2671            state.cells.insert(
2672                key,
2673                crate::list::BoundCell {
2674                    anchor,
2675                    active: true,
2676                    scope: built.scope,
2677                    rebind: built.rebind,
2678                    native_width: None,
2679                    laid_width: None,
2680                    laid_height: None,
2681                    header,
2682                    pinned: false,
2683                },
2684            );
2685        }
2686    }
2687
2688    fn list_layout_cell(&mut self, node: RNode, key: usize) {
2689        // The width the backend last granted this cell, once it has said so (GtkListView's themed
2690        // `row` node pads the cell, so it is narrower than the list); until then the list's own
2691        // content width, which is right wherever the cell IS the row. Every path that re-lays a
2692        // row (a rebind, the dirty-cell sweep, a data change) comes through here, so a width
2693        // the backend corrected once stays corrected.
2694        let native = self
2695            .lists
2696            .get(&node)
2697            .and_then(|s| s.cells.get(&key))
2698            .and_then(|c| c.native_width);
2699        let width = native.unwrap_or_else(|| {
2700            self.nodes
2701                .get(node)
2702                .and_then(|n| n.last_native_frame)
2703                .map(|f| f.size.width)
2704                .unwrap_or(self.windows[0].size.width)
2705        });
2706        self.layout_list_cell_at(node, key, width);
2707    }
2708
2709    fn list_layout_cell_width(&mut self, node: RNode, key: usize, width: f64) {
2710        // The backend's word on the cell's width (`ListSource::layout_cell`), remembered so the
2711        // next first-approximation pass does not undo it.
2712        if let Some(cell) = self
2713            .lists
2714            .get_mut(&node)
2715            .and_then(|s| s.cells.get_mut(&key))
2716        {
2717            cell.native_width = Some(width);
2718        }
2719        self.layout_list_cell_at(node, key, width);
2720    }
2721
2722    fn list_cell_height(&self, node: RNode, key: usize) -> f64 {
2723        self.lists
2724            .get(&node)
2725            .and_then(|s| s.cells.get(&key))
2726            .filter(|b| b.active)
2727            .and_then(|b| b.laid_height)
2728            .unwrap_or(0.0)
2729    }
2730
2731    fn list_cell_keys(&self, node: RNode) -> Vec<usize> {
2732        self.lists
2733            .get(&node)
2734            .map(|s| {
2735                s.cells
2736                    .iter()
2737                    .filter_map(|(key, cell)| cell.active.then_some(*key))
2738                    .collect()
2739            })
2740            .unwrap_or_default()
2741    }
2742
2743    fn list_reload(&mut self, node: RNode) {
2744        if let Some(map) = self.lists.get(&node).and_then(|s| s.sections.as_ref()) {
2745            map.invalidate();
2746        }
2747        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
2748            self.toolkit.update(
2749                &handle,
2750                kinds::LIST,
2751                &day_spec::props::ListPatch::Reload as &dyn Any,
2752                None,
2753            );
2754        }
2755    }
2756
2757    fn list_splice(&mut self, node: RNode, deltas: Vec<day_spec::props::RowDelta>) {
2758        // A sectioned list's deltas name items, and the headers around them move too: the
2759        // host re-queries instead (docs/list.md "Sections").
2760        if self.lists.get(&node).is_some_and(|s| s.sections.is_some()) {
2761            self.list_reload(node);
2762            return;
2763        }
2764        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
2765            self.toolkit.update(
2766                &handle,
2767                kinds::LIST,
2768                &day_spec::props::ListPatch::Splice(deltas) as &dyn Any,
2769                None,
2770            );
2771        }
2772    }
2773
2774    fn set_undo_state(&mut self, state: &day_spec::UndoState) {
2775        self.toolkit.set_undo_state(state);
2776    }
2777
2778    fn set_edit_state(&mut self, state: &day_spec::EditState) {
2779        self.toolkit.set_edit_state(state);
2780    }
2781
2782    fn modifiers(&mut self) -> day_spec::Modifiers {
2783        self.toolkit.modifiers()
2784    }
2785
2786    fn list_scroll_to_end(&mut self, node: RNode) {
2787        // Empty list: nothing to scroll to (the row count is read straight from the piece's
2788        // snapshot, with no tree access, so this guard is cheap and backend-independent).
2789        if self.lists.get(&node).map(|s| (s.driver.len)()).unwrap_or(0) == 0 {
2790            return;
2791        }
2792        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
2793            self.toolkit.update(
2794                &handle,
2795                kinds::LIST,
2796                &day_spec::props::ListPatch::ScrollToEnd as &dyn Any,
2797                None,
2798            );
2799        }
2800    }
2801
2802    fn list_scroll_to_row(&mut self, node: RNode, row: usize) {
2803        let len = self.lists.get(&node).map(|s| (s.driver.len)()).unwrap_or(0);
2804        if len == 0 {
2805            return;
2806        }
2807        let mut row = row.min(len - 1);
2808        if let Some(map) = self.lists.get(&node).and_then(|s| s.sections.as_ref()) {
2809            row = map.display_of(row).unwrap_or(row);
2810        }
2811        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
2812            self.toolkit.update(
2813                &handle,
2814                kinds::LIST,
2815                &day_spec::props::ListPatch::ScrollToRow(row) as &dyn Any,
2816                None,
2817            );
2818        }
2819    }
2820
2821    fn list_set_editing(&mut self, node: RNode, on: bool) {
2822        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
2823            self.toolkit.update(
2824                &handle,
2825                kinds::LIST,
2826                &day_spec::props::ListPatch::Editing(on) as &dyn Any,
2827                None,
2828            );
2829        }
2830    }
2831
2832    fn list_set_selected(&mut self, node: RNode, rows: Vec<usize>) {
2833        let rows = match self.lists.get(&node).and_then(|s| s.sections.as_ref()) {
2834            Some(map) => rows.iter().filter_map(|i| map.display_of(*i)).collect(),
2835            None => rows,
2836        };
2837        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
2838            self.toolkit.update(
2839                &handle,
2840                kinds::LIST,
2841                &day_spec::props::ListPatch::Selected(rows) as &dyn Any,
2842                None,
2843            );
2844        }
2845    }
2846
2847    fn list_driver(&mut self, node: RNode) -> Option<std::rc::Rc<crate::list::ListDriver>> {
2848        self.lists.get(&node).map(|s| s.driver.clone())
2849    }
2850
2851    fn install_tree(&mut self, node: RNode, driver: crate::tree_driver::TreeDriver) {
2852        let driver = Rc::new(driver);
2853        self.trees.insert(
2854            node,
2855            crate::tree_driver::TreeState {
2856                driver: driver.clone(),
2857                cells: HashMap::new(),
2858            },
2859        );
2860        let source = crate::tree_driver::make_tree_source(node, driver);
2861        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
2862            self.toolkit.attach_tree(&handle, source);
2863        }
2864    }
2865
2866    fn tree_prepare_cell(
2867        &mut self,
2868        node: RNode,
2869        key: usize,
2870        cell: RawHandle,
2871    ) -> crate::tree_driver::TreeCellStep {
2872        if let Some(state) = self.trees.get(&node)
2873            && let Some(bound) = state.cells.get(&key)
2874        {
2875            let (rebind, anchor) = (bound.rebind.clone(), bound.anchor);
2876            // Same rule as `list_prepare_cell`: give a pooled cell its parked ids back.
2877            self.restore_subtree_ids(anchor);
2878            return crate::tree_driver::TreeCellStep::Rebind { rebind, anchor };
2879        }
2880        // First use of this cell: adopt the native cell and anchor a fresh subtree under it.
2881        // The centering anchor layout, not the list's PassThrough: tree rows have a fixed
2882        // Uniform height and content that hugs, so the content centers in the cell.
2883        let handle = self.toolkit.adopt(cell);
2884        let anchor =
2885            self.create_cell_anchor(handle, Scope::child(), Rc::new(crate::layout::CellCenter));
2886        crate::tree_driver::TreeCellStep::Build { anchor }
2887    }
2888
2889    fn tree_store_cell(
2890        &mut self,
2891        node: RNode,
2892        key: usize,
2893        anchor: RNode,
2894        built: crate::tree_driver::TreeBuiltRow,
2895    ) {
2896        if let Some(state) = self.trees.get_mut(&node) {
2897            state.cells.insert(
2898                key,
2899                crate::tree_driver::TreeBoundCell {
2900                    anchor,
2901                    scope: built.scope,
2902                    rebind: built.rebind,
2903                    native_width: None,
2904                    native_height: None,
2905                    laid_width: None,
2906                    laid_height: None,
2907                },
2908            );
2909        }
2910    }
2911
2912    fn tree_layout_cell(&mut self, node: RNode, key: usize) {
2913        // The width the backend last granted this cell, once it has said so (indentation makes
2914        // every tree cell's width its own); until then the tree's own content width. Every path
2915        // that re-lays a row comes through here, so a width the backend corrected stays corrected.
2916        let native = self
2917            .trees
2918            .get(&node)
2919            .and_then(|s| s.cells.get(&key))
2920            .and_then(|c| c.native_width);
2921        let width = native.unwrap_or_else(|| {
2922            self.nodes
2923                .get(node)
2924                .and_then(|n| n.last_native_frame)
2925                .map(|f| f.size.width)
2926                .unwrap_or(self.windows[0].size.width)
2927        });
2928        self.layout_tree_cell_at(node, key, width);
2929    }
2930
2931    fn tree_layout_cell_width(&mut self, node: RNode, key: usize, width: f64) {
2932        // The backend's word on the cell's width (`TreeSource::layout_cell`), remembered so the
2933        // next first-approximation pass does not undo it.
2934        if let Some(cell) = self
2935            .trees
2936            .get_mut(&node)
2937            .and_then(|s| s.cells.get_mut(&key))
2938        {
2939            cell.native_width = Some(width);
2940        }
2941        self.layout_tree_cell_at(node, key, width);
2942    }
2943
2944    fn tree_cell_height(&self, node: RNode, key: usize) -> f64 {
2945        self.trees
2946            .get(&node)
2947            .and_then(|s| s.cells.get(&key))
2948            .and_then(|b| b.laid_height)
2949            .unwrap_or(0.0)
2950    }
2951
2952    fn tree_layout_cell_size(&mut self, node: RNode, key: usize, size: Size) {
2953        let changed = self
2954            .trees
2955            .get_mut(&node)
2956            .and_then(|s| s.cells.get_mut(&key))
2957            .and_then(|cell| {
2958                let changed = cell.native_height != Some(size.height);
2959                cell.native_height = Some(size.height);
2960                changed.then_some(cell.anchor)
2961            });
2962        if let Some(anchor) = changed {
2963            self.invalidate_subtree(anchor);
2964        }
2965        self.tree_layout_cell_width(node, key, size.width);
2966    }
2967
2968    fn tree_recycle_cell(&mut self, node: RNode, key: usize) {
2969        let Some(anchor) = self
2970            .trees
2971            .get(&node)
2972            .and_then(|s| s.cells.get(&key))
2973            .map(|b| b.anchor)
2974        else {
2975            return;
2976        };
2977        self.clear_subtree_ids(anchor);
2978    }
2979
2980    fn list_recycle_cell(&mut self, node: RNode, key: usize) {
2981        let Some(anchor) = self
2982            .lists
2983            .get_mut(&node)
2984            .and_then(|s| s.cells.get_mut(&key))
2985            .map(|b| {
2986                b.active = false;
2987                // The next row this cell shows is laid afresh by its `bind_row`, and the host
2988                // reads `cell_height` right after; a height left over from the previous row
2989                // would make that first layout look like a change and raise a spurious
2990                // `RowSizeInvalidated`.
2991                b.laid_height = None;
2992                b.anchor
2993            })
2994        else {
2995            return;
2996        };
2997        self.clear_subtree_ids(anchor);
2998    }
2999
3000    fn list_release_cell(&mut self, node: RNode, key: usize) {
3001        // Unknown cell (never bound, or released twice): nothing to do.
3002        let Some(cell) = self.lists.get_mut(&node).and_then(|s| s.cells.remove(&key)) else {
3003            return;
3004        };
3005        // The same teardown `collect_and_release` gives every cell when the list goes
3006        // (docs/list.md): the row's reactive graph, then its nodes. The anchor's own handle
3007        // is the host's cell and is only dropped, never queued for release (`LIST_CELL`);
3008        // the handles under it are Day's and go through the release queue as usual.
3009        cell.scope.dispose();
3010        self.remove_subtree_impl(cell.anchor);
3011    }
3012
3013    fn list_pin_cell(&mut self, node: RNode, key: usize) {
3014        let Some(anchor) = self
3015            .lists
3016            .get_mut(&node)
3017            .and_then(|s| s.cells.get_mut(&key))
3018            .map(|b| {
3019                b.pinned = true;
3020                b.anchor
3021            })
3022        else {
3023            return;
3024        };
3025        self.clear_subtree_ids(anchor);
3026    }
3027
3028    fn tree_reload(&mut self, node: RNode) {
3029        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
3030            self.toolkit.update(
3031                &handle,
3032                kinds::TREE,
3033                &day_spec::props::TreePatch::Reload as &dyn Any,
3034                None,
3035            );
3036        }
3037    }
3038
3039    fn tree_set_expanded(&mut self, node: RNode, token: u64, expanded: bool) {
3040        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
3041            self.toolkit.update(
3042                &handle,
3043                kinds::TREE,
3044                &day_spec::props::TreePatch::Expand(token, expanded) as &dyn Any,
3045                None,
3046            );
3047        }
3048    }
3049
3050    fn tree_set_selected(&mut self, node: RNode, tokens: Vec<u64>) {
3051        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
3052            self.toolkit.update(
3053                &handle,
3054                kinds::TREE,
3055                &day_spec::props::TreePatch::Selected(tokens) as &dyn Any,
3056                None,
3057            );
3058        }
3059    }
3060
3061    fn tree_reveal(&mut self, node: RNode, token: u64) {
3062        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
3063            self.toolkit.update(
3064                &handle,
3065                kinds::TREE,
3066                &day_spec::props::TreePatch::Reveal(token) as &dyn Any,
3067                None,
3068            );
3069        }
3070    }
3071
3072    fn tree_driver(&mut self, node: RNode) -> Option<std::rc::Rc<crate::tree_driver::TreeDriver>> {
3073        self.trees.get(&node).map(|s| s.driver.clone())
3074    }
3075
3076    fn set_drag_source(&mut self, node: RNode, source: day_spec::transfer::Source) {
3077        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
3078            self.toolkit.set_drag_source(&handle, source);
3079        }
3080    }
3081    fn set_drop_target(&mut self, node: RNode, target: day_spec::transfer::Target) {
3082        if let Some(handle) = self.nodes.get(node).and_then(|n| n.handle.clone()) {
3083            self.toolkit.set_drop_target(&handle, target);
3084        }
3085    }
3086    fn set_context_menu_fn(&mut self, node: RNode, f: day_spec::ContextMenuFn) {
3087        // Match static menus: padding/frame decorators are layout-only, but the provider
3088        // must be installed on the native root beneath them, not silently discarded.
3089        let target = if self.nodes.get(node).is_some_and(|n| n.handle.is_some()) {
3090            Some(node)
3091        } else {
3092            let mut roots = Vec::new();
3093            self.native_descendants(node, &mut roots);
3094            roots.first().copied()
3095        };
3096        let Some(target) = target else {
3097            log::warn!("context_menu_fn on a subtree with no native view — menu dropped");
3098            return;
3099        };
3100        let model = ContextMenuModel::Provider(f.clone());
3101        self.context_menus.insert(node, model.clone());
3102        self.context_menus.insert(target, model);
3103        if let Some(handle) = self.nodes.get(target).and_then(|n| n.handle.clone()) {
3104            self.toolkit
3105                .set_context_menu_fn(&handle, rnode_to_id(target), f);
3106        }
3107    }
3108
3109    fn context_menu_of(&mut self, node: RNode) -> Option<day_spec::ContextMenuFn> {
3110        // The element a script names may sit above or below the node the menu landed on (an id
3111        // on a wrapper, a menu on the label inside it): the node itself, then its ancestors.
3112        let mut cur = node;
3113        loop {
3114            if let Some(model) = self.context_menus.get(&cur) {
3115                return Some(match model {
3116                    ContextMenuModel::Items(items) => {
3117                        let items = items.clone();
3118                        Rc::new(move |_| items.clone())
3119                    }
3120                    ContextMenuModel::Provider(f) => f.clone(),
3121                });
3122            }
3123            // The root (and a list cell's anchor) is its own parent, or has a null one,
3124            // which the lookup misses.
3125            let parent = self.nodes.get(cur)?.parent;
3126            if parent == cur {
3127                return None;
3128            }
3129            cur = parent;
3130        }
3131    }
3132}
3133
3134// ---------------------------------------------------------------------------
3135// Thread-local tree + event pump
3136// ---------------------------------------------------------------------------
3137
3138day_reactive::tls_slots! {
3139    tree;
3140    /// The installed tree. `ManuallyDrop` so the thread-local destructor never drops it: the
3141    /// tree owns the toolkit's native widgets, and dropping those at thread exit runs the
3142    /// toolkit's callbacks (a GtkListBox being emptied calls its header func) against
3143    /// thread-locals that may already be gone — Rust destroys them in an order nobody controls,
3144    /// and a panic inside a C trampoline cannot unwind, so the process aborted on the way out
3145    /// of a clean exit. At process exit the OS reclaims the widgets; nothing in their drop
3146    /// matters. [`uninstall_tree`] (tests, Android's remount) still drops the tree explicitly,
3147    /// while every thread-local is alive.
3148    static TREE: std::mem::ManuallyDrop<RefCell<Option<Box<dyn TreeOps>>>> =
3149        const { std::mem::ManuallyDrop::new(RefCell::new(None)) };
3150    static EVENTS: RefCell<VecDeque<(NodeId, Event)>> = const { RefCell::new(VecDeque::new()) };
3151    static PUMP_PENDING: Cell<bool> = const { Cell::new(false) };
3152    static PUMP_ACTIVE: Cell<bool> = const { Cell::new(false) };
3153    /// Posted main-thread closures that arrived while the tree was borrowed and so could
3154    /// not run (see [`run_posted_or_defer`]). Drained at the next safe point, in order.
3155    static DEFERRED: RefCell<Vec<Box<dyn FnOnce() + Send>>> = const { RefCell::new(Vec::new()) };
3156    /// The event observer installed by [`set_event_observer`] and consulted by [`enqueue_events`].
3157    /// `Rc`, not `Box`, so the handle can be cloned out and invoked with no borrow held (the
3158    /// observer may safely re-enter Day to read the tree or stop recording).
3159    static EVENT_OBSERVER: RefCell<Option<Rc<EventObserver>>> = const { RefCell::new(None) };
3160    /// Monotonic pump counter, bumped once per [`pump_events_inner`]. The recorder ages its
3161    /// coalescing candidate by it: a tap folds into a navigation caused in the same or the next
3162    /// pump (a signal-bound sidebar remount settles one pump late), but never a later, unrelated
3163    /// navigation.
3164    static PUMP_GEN: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
3165
3166    /// The node that last reported gaining focus, cleared when it reports losing it.
3167    static FOCUSED: std::cell::Cell<Option<RNode>> = const { std::cell::Cell::new(None) };
3168}
3169
3170pub fn install_tree(tree: Box<dyn TreeOps>) {
3171    // A fresh mount starts with no route hosts; nav()/tabs() re-register during build.
3172    crate::nav::clear_controllers();
3173    TREE.with(|t| *t.borrow_mut() = Some(tree));
3174    // Read the export answers now, while the tree is free, so a context menu built inside a
3175    // later borrow finds them (`image::canvas_exports`).
3176    let _ = crate::image::canvas_exports();
3177}
3178
3179/// Tear the current mount down so the app can be built again in the same process
3180/// (docs/settings.md "Appearance on Android").
3181///
3182/// Android is the one platform that needs this: an activity recreation (the mechanism of a
3183/// light/dark switch, and the side effect of any configuration change the manifest does not
3184/// claim) destroys the window and calls the app's entry point a second time.
3185/// Everywhere else launch happens exactly once by construction.
3186///
3187/// What this resets is everything an app's `root()` registers, because `root()` is about to run
3188/// again and register it all a second time: the window registry and its content scopes, the
3189/// navigation controllers, the per-window ambient signals, the lifecycle handlers, the menu
3190/// actions. What it leaves alone is state that lives on the root reactive scope
3191/// (`Signal::global`, `Ambient::app()`), which is exactly the state an app expects to outlive a
3192/// window, and which is what makes a recreation cheap rather than a cold start (docs/state.md).
3193///
3194/// One thing it cannot reset is an app's own `std::sync::Once`: a one-time install guarded that
3195/// way will not re-run, so a `Once` in `root()` is the pattern to avoid on Android.
3196pub fn prepare_remount() {
3197    // Drop in-flight reactive work first. Anything already queued was scheduled by the mount
3198    // that is about to be disposed, and running it afterwards reads signals that no longer
3199    // exist ("read of disposed Signal"). This also re-roots the scope stack, which matters more
3200    // than it looks: left pointing at a disposed scope, every `Signal::new` in the rebuild is
3201    // born dead and the new tree panics on its own state.
3202    day_reactive::recover_from_panic();
3203    uninstall_tree();
3204    // Tearing the old tree down can schedule cleanup of its own; go back to idle before rebuilding.
3205    day_reactive::recover_from_panic();
3206    crate::lifecycle::reset_handlers();
3207    crate::menu::reset_menus();
3208}
3209
3210/// Whether a tree is installed on this thread, i.e. whether the app has already launched.
3211///
3212/// The one caller that is not a test is Android's `nativeStart`, which an activity recreation can
3213/// re-enter after the app is already up (docs/settings.md "Appearance on Android"). Everywhere
3214/// else launch happens exactly once by construction.
3215pub fn is_mounted() -> bool {
3216    TREE.with(|t| t.borrow().is_some())
3217}
3218
3219/// Reset the thread-local tree + queues (tests).
3220pub fn uninstall_tree() {
3221    // A decode parked on the tree that is going away would wait forever (docs/images.md).
3222    crate::image::reset();
3223    crate::nav::clear_controllers();
3224    crate::windows::reset_windows();
3225    crate::status::reset_status();
3226    // Per-window signals are keyed by root node, and roots repeat across trees on this thread;
3227    // a stale entry would hand the next tree the previous one's size class.
3228    crate::reset_ambient();
3229    TREE.with(|t| *t.borrow_mut() = None);
3230    EVENTS.with(|e| e.borrow_mut().clear());
3231    PUMP_PENDING.with(|c| c.set(false));
3232    // The event observer (§14.6) is thread-local state too; a test that installed one must not
3233    // leak it into the next tree on this thread.
3234    EVENT_OBSERVER.with(|o| *o.borrow_mut() = None);
3235}
3236
3237/// Access the installed tree. Tree methods never run user code, so nesting cannot occur
3238/// while a borrow is held; if events were queued during the call, they are pumped after
3239/// the borrow is released (the "safe point" of §3.3).
3240pub fn with_tree<R>(f: impl FnOnce(&mut dyn TreeOps) -> R) -> R {
3241    let r = TREE.with(|t| {
3242        let mut opt = t.borrow_mut();
3243        let ops = opt.as_mut().expect("day: no tree installed on this thread");
3244        f(ops.as_mut())
3245    });
3246    if PUMP_PENDING.with(|c| c.replace(false)) {
3247        pump_events();
3248    }
3249    run_deferred();
3250    r
3251}
3252
3253/// Run a posted main-thread closure now, or hold it for the next safe point when the tree is
3254/// borrowed. Tree methods never run user code, but a backend may pump its native message
3255/// loop from inside one (XAML's `open_window` waits for the new root to load; a modal native
3256/// loop can nest anywhere), and a closure that loop dispatches would re-enter [`with_tree`]
3257/// on the borrowed tree and panic ("RefCell already borrowed"). Held closures run at the end
3258/// of the outermost `with_tree`, in arrival order, each contained like any posted task.
3259pub(crate) fn run_posted_or_defer(f: Box<dyn FnOnce() + Send>) {
3260    let borrowed = TREE.with(|t| t.try_borrow_mut().is_err());
3261    if borrowed {
3262        DEFERRED.with(|d| d.borrow_mut().push(f));
3263    } else {
3264        crate::contain_posted_panic(f);
3265    }
3266}
3267
3268/// The safe-point drain for [`run_posted_or_defer`]: called once the borrow is released. A
3269/// drained closure may defer more (it can borrow the tree and nest a pump of its own), so
3270/// this loops until the queue stays empty; each batch is taken out before it runs.
3271fn run_deferred() {
3272    loop {
3273        let batch = DEFERRED.with(|d| std::mem::take(&mut *d.borrow_mut()));
3274        if batch.is_empty() {
3275            return;
3276        }
3277        for f in batch {
3278            crate::contain_posted_panic(f);
3279        }
3280    }
3281}
3282
3283/// Query the active toolkit's support for a capability (docs). Lets app/piece code adapt its
3284/// content to the backend, e.g. a page can skip a title the native nav already shows in a header
3285/// (`Cap::NavHeader`), or pick a presentation from `Cap::NavSplit`.
3286pub fn capability(cap: day_spec::Cap) -> day_spec::Support {
3287    with_tree(|t| t.capability(cap))
3288}
3289
3290/// Open `url` in the platform's default handler (system browser for `http(s)`, mail client for
3291/// `mailto:`, …). The function behind the [`link`](/api/day_pieces/fn.link.html) piece; call it
3292/// directly from a tap handler for a custom affordance. Fire and forget: no result, unopenable
3293/// URLs are ignored by the backend.
3294pub fn open_url(url: &str) {
3295    // While `day test` runs the app's cases, a link is recorded for `assert_opened_url` and
3296    // nothing opens: a test run must not launch the machine's browser (docs/testing.md).
3297    if crate::conformance::intercept_open_url(url) {
3298        return;
3299    }
3300    with_tree(|t| t.open_url(url));
3301}
3302
3303/// Speak `text` through the screen reader without moving its focus (docs/accessibility.md):
3304/// the outcome of an action the user cannot otherwise learn about ("Saved", "3 results"). The
3305/// sentence waits for whatever is being read to finish; [`announce_urgent`] interrupts it.
3306/// Nothing happens when no screen reader is running, and nothing happens on a toolkit that
3307/// answers [`Cap::Announce`](day_spec::Cap::Announce) with `Unsupported`.
3308pub fn announce(text: &str) {
3309    crate::conformance::note_announcement(text);
3310    with_tree(|t| t.announce(text, false));
3311}
3312
3313/// [`announce`], interrupting what the screen reader is saying: an error, or a change that
3314/// cannot wait.
3315pub fn announce_urgent(text: &str) {
3316    crate::conformance::note_announcement(text);
3317    with_tree(|t| t.announce(text, true));
3318}
3319
3320/// Whether the current toolkit can present a native sharing chooser.
3321pub fn share_support() -> Support {
3322    with_tree(|t| t.share_support())
3323}
3324/// Present a native chooser for a URL. Call directly from user activation (required on web).
3325/// Returns false if unavailable or presentation failed; cancellation is not an error and no
3326/// completion is implied. The caller owns any explicitly labeled fallback.
3327pub fn share_url(url: &str, title: &str) -> bool {
3328    with_tree(|t| t.share_url(url, title))
3329}
3330
3331/// Query current system associations on the UI thread. macOS AppKit implements all query
3332/// kinds; other backends return `Unsupported`. No application is launched by discovery.
3333pub fn application_handlers(
3334    query: &day_spec::applications::HandlerQuery,
3335) -> Result<day_spec::applications::ApplicationHandlers, day_spec::applications::ApplicationError> {
3336    with_tree(|t| t.application_handlers(query))
3337}
3338
3339/// The OS-chosen browser for HTTPS links, read afresh rather than cached by Day.
3340pub fn default_browser()
3341-> Result<Option<day_spec::applications::Application>, day_spec::applications::ApplicationError> {
3342    application_handlers(&day_spec::applications::HandlerQuery::Scheme(
3343        "https".into(),
3344    ))
3345    .map(|h| h.default)
3346}
3347
3348/// Open a URL or file URL in a chosen application. Await on the UI executor. Success means
3349/// the OS accepted the open request; tab/window presentation belongs to the receiving app.
3350pub async fn open_url_with_application(
3351    url: &str,
3352    application: &str,
3353) -> Result<(), day_spec::applications::ApplicationError> {
3354    let (send, receive) = day_async::oneshot();
3355    with_tree(|t| {
3356        t.open_url_with_application(url, application, Box::new(move |result| send.send(result)))
3357    });
3358    receive
3359        .await
3360        .unwrap_or(Err(day_spec::applications::ApplicationError::LaunchFailed))
3361}
3362
3363/// Tell layout that a node's intrinsic size may have changed. For tweaks (docs/tweaks.md):
3364/// after a native call that alters a widget's preferred size (fonts, tick marks, bezel styles),
3365/// the measure cache along the node's path must be invalidated, since Day can't see native
3366/// mutations it didn't make. Relayout runs at the next turn boundary as usual. No-op on a
3367/// disposed node.
3368pub fn invalidate_size(node: RNode) {
3369    with_tree(|t| {
3370        t.mark_needs_measure(node);
3371        t.mark_layout_dirty();
3372    });
3373}
3374
3375/// Like `with_tree`, but returns `None` instead of panicking when the tree can't be entered:
3376/// already borrowed, or not installed yet. A snapshot (`TreeOps::snapshot`) holds the borrow
3377/// while the backend draws the window synchronously, and that draw can re-enter Day through a
3378/// native callback, e.g. a lazy list's `viewForRow`/`connect_bind`/`cellForRow` firing during
3379/// `cacheDisplayInRect`. And platform style callbacks can fire before `install_tree`, e.g.
3380/// GTK's StyleManager emits a `dark` notify while `startup` applies a forced `DAY_THEME`
3381/// scheme, before `activate` mounts the tree; a panic there unwinds into a C signal trampoline
3382/// and aborts the process. Such callbacks use this and skip their work; the next real
3383/// layout (or the signal's first post-mount read) catches up.
3384pub fn try_with_tree<R>(f: impl FnOnce(&mut dyn TreeOps) -> R) -> Option<R> {
3385    let r = TREE.with(|t| {
3386        let mut opt = t.try_borrow_mut().ok()?;
3387        let ops = opt.as_mut()?;
3388        Some(f(ops.as_mut()))
3389    });
3390    if r.is_some() {
3391        if PUMP_PENDING.with(|c| c.replace(false)) {
3392            pump_events();
3393        }
3394        run_deferred();
3395    }
3396    r
3397}
3398
3399/// [`try_with_tree`] without the pump on the way out, for work that is itself part of a pump,
3400/// or that raises no events (`release_image`), where a nested pump would be wrong or wasted.
3401pub(crate) fn with_tree_if_free<R>(f: impl FnOnce(&mut dyn TreeOps) -> R) -> Option<R> {
3402    TREE.with(|t| {
3403        let mut opt = t.try_borrow_mut().ok()?;
3404        let ops = opt.as_mut()?;
3405        Some(f(ops.as_mut()))
3406    })
3407}
3408
3409/// Whether no tree is installed, as distinct from one that is merely borrowed right now, which
3410/// this answers `false` for. [`is_mounted`] cannot tell the two apart without panicking.
3411pub(crate) fn tree_absent() -> bool {
3412    // Bitmap handles can outlive the entire core TLS root at process/thread exit.
3413    TREE.try_with(|t| t.try_borrow().map(|o| o.is_none()).unwrap_or(false))
3414        .unwrap_or(true)
3415}
3416
3417/// Ask for a pump at the next safe point (the end of the current `with_tree`), for work queued
3418/// while the tree was borrowed.
3419pub(crate) fn request_pump() {
3420    PUMP_PENDING.with(|c| c.set(true));
3421}
3422
3423pub fn has_tree() -> bool {
3424    // Native resize/layout callbacks can ask while a Toolkit call already borrows the tree.
3425    // A busy tree is installed, not absent; this presence probe must not borrow it again.
3426    !tree_absent()
3427}
3428
3429/// The enqueue-only event sink installed into every backend (§8.3). May be invoked
3430/// re-entrantly from inside any Toolkit method; dispatch happens at the next safe point.
3431pub fn enqueue_event(id: NodeId, ev: Event) {
3432    enqueue_events([(id, ev)]);
3433}
3434
3435/// The toolkit's event sink: [`enqueue_event`], after a sectioned list's row events are
3436/// translated from the host's display rows to the app's items (docs/list.md "Sections").
3437/// Everything else (dayscript, probes, day-core's own reports) already speaks items and
3438/// enqueues directly.
3439pub(crate) fn enqueue_native_event(id: NodeId, ev: Event) {
3440    if let Some(ev) = crate::list::translate_native_event(id, ev) {
3441        enqueue_event(id, ev);
3442    }
3443}
3444
3445/// Enqueue several events into one drain before dispatching. Backends that observe a focus
3446/// move at a single point (Qt's `focusChanged(old, new)`, an AppKit first-responder change)
3447/// deliver the loss+gain pair through this so the pump can dispatch the gain first and a
3448/// shared group signal never passes through `None` (docs/focus.md).
3449pub fn enqueue_events(evs: impl IntoIterator<Item = (NodeId, Event)>) {
3450    // Recording/telemetry hook (§14.6): when an observer is installed, let it see every event in
3451    // the exact order and form the app is about to receive, before it is dispatched,
3452    // so it observes precisely what the app receives. This is the single point every backend
3453    // funnels native events through, so the observer needs no per-toolkit code. The handle is
3454    // cloned out first (a cheap `Rc` bump) so no thread-local borrow is held across the call: the
3455    // observer may re-enter Day (resolve an id via `id_of`, or even remove itself to stop
3456    // recording) without a borrow conflict. A `None` observer costs one `Option` check and takes
3457    // the original zero-copy `extend` path.
3458    let observer = EVENT_OBSERVER.with(|o| o.borrow().clone());
3459    match observer {
3460        Some(obs) => {
3461            let batch: Vec<(NodeId, Event)> = evs.into_iter().collect();
3462            for (id, ev) in &batch {
3463                obs(*id, ev);
3464            }
3465            EVENTS.with(|e| e.borrow_mut().extend(batch));
3466        }
3467        None => EVENTS.with(|e| e.borrow_mut().extend(evs)),
3468    }
3469    let tree_free = TREE.with(|t| t.try_borrow_mut().is_ok());
3470    if tree_free {
3471        pump_events();
3472    } else {
3473        PUMP_PENDING.with(|c| c.set(true));
3474    }
3475}
3476
3477/// The current pump generation (see `PUMP_GEN`). Read by the recorder's nav coalescing.
3478pub fn pump_generation() -> u64 {
3479    PUMP_GEN.with(|g| g.get())
3480}
3481
3482/// Install (or clear, with `None`) an observer that sees every event day-core dispatches, in queue
3483/// order, at the one point every backend funnels native events through ([`enqueue_events`],
3484/// §8.3), before the event reaches the app, so it observes exactly what the app receives. This
3485/// is the recording/telemetry hook behind [`day::record`](../day_script/record/index.html)
3486/// (§14.6): a higher layer captures user actions into a replayable dayscript without touching
3487/// any of the backends. Main-thread only; a `None` observer adds no cost to the event path. The
3488/// boxed closure is adopted into an `Rc` internally so [`enqueue_events`] can call it with no
3489/// borrow held.
3490pub fn set_event_observer(observer: Option<Box<EventObserver>>) {
3491    EVENT_OBSERVER.with(|o| *o.borrow_mut() = observer.map(Rc::from));
3492}
3493
3494/// Resolve a dispatched [`NodeId`] back to the app-authored `.id()` string that named its node
3495/// (`find_by_id`'s inverse, §5.5), or `None` for an id-less node or one no longer in the tree. The
3496/// recorder ([`day::record`](../day_script/record/index.html), §14.6) calls this from inside its
3497/// event observer to label a tap/input with the id an app would target in a dayscript step.
3498/// Borrow-safe: returns `None` rather than panicking if the tree is momentarily borrowed by a
3499/// re-entrant backend call.
3500pub fn id_of(node: NodeId) -> Option<String> {
3501    try_with_tree(|t| t.node_id(id_to_rnode(node))).flatten()
3502}
3503
3504/// A human-readable label for a node, for annotating a recorded script (§14.6): the accessibility
3505/// label if one is set (`.a11y(label = …)`, the localized string the screen reader speaks),
3506/// otherwise the control's own visible text. `None` when the node carries neither.
3507pub fn label_of(node: NodeId) -> Option<String> {
3508    try_with_tree(|t| {
3509        let rnode = id_to_rnode(node);
3510        let a11y = t
3511            .node_a11y(rnode)
3512            .and_then(|a| a.label)
3513            .filter(|s| !s.is_empty());
3514        a11y.or_else(|| {
3515            t.node_probe(rnode)
3516                .map(|p| p.text)
3517                .filter(|s| !s.is_empty())
3518        })
3519    })
3520    .flatten()
3521}
3522
3523/// Dispatch queued native events from the backend queue, containing any panic. Native event
3524/// callbacks reach Day through `extern "C"` signal trampolines (GTK's `value_changed_trampoline`,
3525/// Qt's event filters, …) that abort the process on unwind (`panic_cannot_unwind`). A panic in a
3526/// Day event handler or its reactive drain (e.g. the reactive-cycle assertion firing during a
3527/// slider drag) would therefore `SIGABRT` the whole app instead of surfacing. Catch it at this
3528/// single backend-agnostic boundary, log it (the message carries the offending effect's source
3529/// location), and reset the runtime so the app keeps running (degraded) rather than crashing.
3530pub fn pump_events() {
3531    // Native mutations in a handler can enqueue focus, value, or dismissal
3532    // events. Finish that handler before dispatching them: nested dispatch can
3533    // dispose the signals still used by its remaining binding writes.
3534    if PUMP_ACTIVE.with(|active| active.replace(true)) {
3535        return;
3536    }
3537    struct PumpGuard;
3538    impl Drop for PumpGuard {
3539        fn drop(&mut self) {
3540            PUMP_ACTIVE.with(|active| active.set(false));
3541        }
3542    }
3543    let _guard = PumpGuard;
3544    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3545        loop {
3546            pump_events_inner();
3547            // flush_sync and route notifications may themselves enqueue events.
3548            // Drain these after their current reactions finish, before returning.
3549            if EVENTS.with(|events| events.borrow().is_empty()) {
3550                break;
3551            }
3552        }
3553    }));
3554    if let Err(payload) = result {
3555        let msg = payload
3556            .downcast_ref::<&str>()
3557            .map(|s| (*s).to_string())
3558            .or_else(|| payload.downcast_ref::<String>().cloned())
3559            .unwrap_or_else(|| "unknown panic".to_string());
3560        log::warn!(
3561            "a native event handler panicked and was contained — the app continues, but \
3562             reactive/UI state may be inconsistent until the next interaction. Cause: {msg}"
3563        );
3564        // Drop the in-flight event batch and reset drain state so the runtime isn't wedged.
3565        EVENTS.with(|e| e.borrow_mut().clear());
3566        PUMP_PENDING.with(|c| c.set(false));
3567        day_reactive::recover_from_panic();
3568        crate::notify_contained_panic();
3569    }
3570}
3571
3572fn pump_events_inner() {
3573    // The tree borrow has ended by now: run the image releases that arrived while it was held.
3574    crate::image::flush_deferred_releases();
3575    // A new pump: bump the generation, then record any route change that a previous pump left
3576    // pending (a signal-bound sidebar/stack remount settles into NAV_STACK a tick after the pump
3577    // that triggered it, so the tail check below reads a stale route; this start check catches it
3578    // at the next pump boundary, still fresh enough to fold its triggering select/tap). §14.6.
3579    PUMP_GEN.with(|g| g.set(g.get() + 1));
3580    crate::nav::maybe_notify_route_change();
3581    loop {
3582        let item = EVENTS.with(|e| e.borrow_mut().pop_front());
3583        let Some((id, ev)) = item else { break };
3584        // Presentation answers are keyed by request id, not by tree node (docs/dialogs.md).
3585        if let Event::PresentResult { req, result } = ev {
3586            crate::present::resolve_presentation(req, result);
3587            continue;
3588        }
3589        // Image decodes/encodes are keyed by request id too (docs/images.md), for the same
3590        // reason presentation is: the answer belongs to the caller that asked, not to a node.
3591        if let Event::ImageDecoded { req, result } = ev {
3592            crate::image::resolve_image_decode(req, result);
3593            continue;
3594        }
3595        if let Event::ImageEncoded { req, result } = ev {
3596            crate::image::resolve_image_encode(req, result);
3597            continue;
3598        }
3599        // Canvas exports answer the caller too (docs/canvas.md "Exporting").
3600        if let Event::BitmapRendered { req, result } = ev {
3601            crate::image::resolve_bitmap_render(req, result);
3602            continue;
3603        }
3604        if let Event::PdfRendered { req, result } = ev {
3605            crate::image::resolve_pdf_render(req, result);
3606            continue;
3607        }
3608        // Menu actions are keyed by action id, not by tree node (§ menus).
3609        if let Event::MenuAction(action) = ev {
3610            crate::menu::dispatch_menu_action(action);
3611            continue;
3612        }
3613        // Toolbar values are keyed by action id too (docs/toolbars.md); borrowed, because the
3614        // payload owns a String and the arms below still need `ev`.
3615        if let Event::ToolbarChanged { action, value } = &ev {
3616            crate::toolbar::dispatch_toolbar_value(*action, value);
3617            continue;
3618        }
3619        // Lifecycle phases are app-global, not keyed by tree node (docs/lifecycle.md).
3620        if let Event::Lifecycle(phase) = ev {
3621            crate::lifecycle::dispatch_lifecycle(phase);
3622            continue;
3623        }
3624        // Focus loss/gain pairing (docs/focus.md): when focus moves between two Day controls,
3625        // the loss and gain arrive as separate events. Dispatching the queued gain first lets a
3626        // shared group signal transition `Some(A)` → `Some(B)` without an observable `None`
3627        // (the loss handler only clears the signal if it still names its own control).
3628        if ev == Event::FocusChanged(false) {
3629            let paired = EVENTS.with(|e| {
3630                let mut q = e.borrow_mut();
3631                let gain = q
3632                    .iter()
3633                    .position(|(gid, gev)| *gid != id && *gev == Event::FocusChanged(true));
3634                gain.map(|i| q.remove(i).expect("indexed event"))
3635            });
3636            if let Some((gid, gev)) = paired {
3637                dispatch_focus_probe(gid, &gev);
3638                dispatch_to_node(gid, &gev);
3639            }
3640        }
3641        if let Event::FocusChanged(_) = ev {
3642            dispatch_focus_probe(id, &ev);
3643        }
3644        dispatch_to_node(id, &ev);
3645    }
3646    day_reactive::flush_sync();
3647    // The route has settled now that every queued event is dispatched and reactive writes have
3648    // flushed. A sidebar click, a nav_link, a stack push, and a native back all change the route
3649    // by calling `navigate`/`pop` from an event handler, none of which pass back through
3650    // `enqueue_events`, so the event observer never sees them. Notifying here (deduped against
3651    // the last route) is how the recorder captures navigation regardless of what triggered it
3652    // (docs/navigation.md, §14.6).
3653    crate::nav::maybe_notify_route_change();
3654}
3655
3656/// The node that currently has the keyboard, as the platform last reported it. Keys follow
3657/// focus (docs/menus.md), so this is where a synthetic key has to land for a scripted run to
3658/// exercise the same route a real one does. `None` when nothing focusable holds it.
3659pub fn focused_node() -> Option<RNode> {
3660    FOCUSED.with(|f| f.get())
3661}
3662
3663/// Mirror a focus event into the node's dayscript probe (`assert_focused` reads it).
3664fn dispatch_focus_probe(id: NodeId, ev: &Event) {
3665    if let Event::FocusChanged(f) = ev {
3666        let node = id_to_rnode(id);
3667        with_tree(|t| t.set_probe_focused(node, *f));
3668    }
3669}
3670
3671/// Imperatively scroll a `scroll` piece (docs/scroll.md): `node` is the scroll node for edge
3672/// and offset targets (`ScrollTarget::Id` ignores it and reveals the named element in its own
3673/// nearest scroll). Animated on-screen; dayscript uses the unanimated variant for determinism.
3674/// Call with no tree borrow held.
3675pub fn scroll_to(node: RNode, target: ScrollTarget) {
3676    with_tree(|t| {
3677        t.scroll_to_target(node, &target, true);
3678    });
3679}
3680
3681/// Run one already-routed event through its node's handlers (the tail of the pump loop).
3682fn dispatch_to_node(id: NodeId, ev: &Event) {
3683    let node = if id == day_spec::WINDOW_NODE {
3684        with_tree(|t| t.root_node())
3685    } else {
3686        id_to_rnode(id)
3687    };
3688    // A gesture addressed to a piece's own node (a dayscript step by id, which names that node
3689    // wherever `.id` sits in the chain; a probe's emit) is delivered to the node that enabled
3690    // the gesture: the node itself, or the wrapper above it where `.padding(..).on_tap(..)`
3691    // registered its handler, as a native gesture on the inner view reaches the recognizer on
3692    // its superview. A natively delivered gesture already names that node, so nothing moves.
3693    let gesture = match ev {
3694        Event::Tap(_) => Some(day_spec::GestureKind::Tap),
3695        Event::LongPress(_) => Some(day_spec::GestureKind::LongPress),
3696        Event::Drag { .. } => Some(day_spec::GestureKind::Drag),
3697        Event::Pan { .. } => Some(day_spec::GestureKind::Pan),
3698        Event::Pinch { .. } => Some(day_spec::GestureKind::Pinch),
3699        Event::Hover { .. } => Some(day_spec::GestureKind::Hover),
3700        _ => None,
3701    };
3702    let node = match gesture.and_then(|k| with_tree(|t| t.gesture_target(node, k))) {
3703        Some(target) => target,
3704        None => node,
3705    };
3706    let mut handlers = with_tree(|t| t.handlers_for(node));
3707    // A key or a focus change the toolkit raised on a native view also reaches the layout-only
3708    // wrappers directly above it (the same element): `.frame(..).on_key(..)` listens on the
3709    // wrapper, while the platform delivers the key to the view. Only these two: a tap or a drag
3710    // already reaches the wrapper through `enable_gesture`'s descent, and would arrive twice.
3711    if matches!(ev, Event::Key(_) | Event::FocusChanged(_)) {
3712        with_tree(|t| {
3713            for w in t.wrappers_above(node) {
3714                handlers.extend(t.handlers_for(w));
3715            }
3716        });
3717    }
3718    if handlers.is_empty() {
3719        // A press routed to a node that no longer exists is a staleness bug somewhere
3720        // upstream (an element index, a native view outliving its node); surface it in
3721        // debug builds instead of dropping silently; alive-but-handlerless is normal.
3722        if cfg!(debug_assertions)
3723            && matches!(ev, Event::Pressed | Event::Tap(_))
3724            && !with_tree(|t| t.node_exists(node))
3725        {
3726            log::warn!("{ev:?} dropped — node {id:?} no longer exists (stale reference)");
3727        }
3728        return;
3729    }
3730    day_reactive::batch(|| {
3731        for h in &handlers {
3732            h(ev);
3733        }
3734    });
3735}
3736
3737fn collect_menu_families(
3738    items: &[day_spec::MenuItem],
3739    families: &mut std::collections::BTreeSet<String>,
3740) {
3741    for item in items {
3742        match item {
3743            day_spec::MenuItem::Action { id: Some(id), .. } => {
3744                families.insert(crate::element_id::family_of(id).to_owned());
3745            }
3746            day_spec::MenuItem::Submenu { items, .. } => collect_menu_families(items, families),
3747            _ => {}
3748        }
3749    }
3750}
3751
3752#[cfg(test)]
3753mod thread_exit_tests {
3754    use super::*;
3755    use std::sync::Arc;
3756    use std::sync::atomic::{AtomicBool, Ordering};
3757
3758    /// A toolkit that records being dropped: the tree owns it, so it goes when the tree does.
3759    struct Probe(Arc<AtomicBool>);
3760
3761    impl Drop for Probe {
3762        fn drop(&mut self) {
3763            self.0.store(true, Ordering::SeqCst);
3764        }
3765    }
3766
3767    impl Toolkit for Probe {
3768        type Handle = ();
3769        fn realize(&mut self, _: PieceKind, _: &dyn Any, _: NodeId) -> Self::Handle {}
3770        fn release(&mut self, _: Self::Handle) {}
3771        fn insert(&mut self, _: &Self::Handle, _: &Self::Handle, _: usize) {}
3772        fn remove(&mut self, _: &Self::Handle, _: &Self::Handle) {
3773            panic!("test probe has no attached children to remove");
3774        }
3775        fn move_child(&mut self, _: &Self::Handle, _: &Self::Handle, _: usize) {}
3776        fn measure(&mut self, _: &Self::Handle, _: PieceKind, _: Proposal) -> Size {
3777            Size::new(0.0, 0.0)
3778        }
3779        fn set_frame(&mut self, _: &Self::Handle, _: Rect, _: Option<&AnimSpec>) {}
3780        fn set_input_traits(&mut self, _: &Self::Handle, _: &InputTraits) -> Option<Self::Handle> {
3781            None
3782        }
3783        fn set_event_sink(&mut self, _: EventSink) {}
3784        fn update(&mut self, _: &Self::Handle, _: &'static str, _: &dyn Any, _: Option<&AnimSpec>) {
3785        }
3786    }
3787
3788    fn install_probe(dropped: &Arc<AtomicBool>) {
3789        let tree = Tree::new(Probe(dropped.clone()), (), Size::new(0.0, 0.0));
3790        install_tree(Box::new(tree));
3791    }
3792
3793    /// The tree owns the toolkit's native widgets, and dropping those at thread exit runs the
3794    /// toolkit's callbacks against thread-locals that are already gone (a GtkListBox being
3795    /// emptied calls its header func; a panic there cannot unwind through C and aborts the
3796    /// process on the way out of a clean exit). So a thread's end must leak the tree.
3797    #[test]
3798    fn the_tree_is_not_dropped_when_its_thread_ends() {
3799        let dropped = Arc::new(AtomicBool::new(false));
3800        let d = dropped.clone();
3801        std::thread::spawn(move || install_probe(&d))
3802            .join()
3803            .expect("thread");
3804        assert!(
3805            !dropped.load(Ordering::SeqCst),
3806            "the thread-local destructor dropped the tree, and with it the native widgets"
3807        );
3808    }
3809
3810    /// Explicit teardown is a different matter: tests and Android's remount take the tree down
3811    /// while every thread-local is alive, and that must still drop it.
3812    #[test]
3813    fn uninstall_still_drops_the_tree() {
3814        let dropped = Arc::new(AtomicBool::new(false));
3815        let d = dropped.clone();
3816        std::thread::spawn(move || {
3817            install_probe(&d);
3818            uninstall_tree();
3819            assert!(
3820                d.load(Ordering::SeqCst),
3821                "uninstall_tree must drop the tree"
3822            );
3823        })
3824        .join()
3825        .expect("thread");
3826    }
3827
3828    #[test]
3829    fn removing_an_unattached_node_releases_without_native_detach() {
3830        let dropped = Arc::new(AtomicBool::new(false));
3831        let mut tree = Tree::new(Probe(dropped), (), Size::new(0.0, 0.0));
3832        let scope = Scope::detached();
3833        let node = tree.create_node(
3834            "fixture.detached",
3835            &(),
3836            Rc::new(crate::LeafLayout),
3837            Flex::default(),
3838            true,
3839            false,
3840            scope,
3841        );
3842        tree.remove_subtree(node);
3843        assert!(!tree.node_exists(node));
3844        assert_eq!(
3845            tree.release_queue.len(),
3846            1,
3847            "the unattached native handle must still be released"
3848        );
3849        scope.dispose();
3850    }
3851
3852    /// A posted closure that arrives while the tree is borrowed (a backend pumping its native
3853    /// loop from inside a tree method) must wait for the safe point rather than re-enter
3854    /// `with_tree` on the borrowed tree: the "RefCell already borrowed" the web view demo hit
3855    /// opening Browser Settings on WinUI while a second tab's poll kept posts in flight.
3856    #[test]
3857    fn a_post_during_a_tree_borrow_runs_at_the_safe_point() {
3858        std::thread::spawn(|| {
3859            install_probe(&Arc::new(AtomicBool::new(false)));
3860            let ran = Arc::new(AtomicBool::new(false));
3861            let r = ran.clone();
3862            with_tree(|_| {
3863                run_posted_or_defer(Box::new(move || {
3864                    // The deferred closure borrows the tree itself, as a reactive flush or an
3865                    // eval reply does; running it inside the outer borrow would panic here.
3866                    with_tree(|_| r.store(true, Ordering::SeqCst));
3867                }));
3868                assert!(!ran.load(Ordering::SeqCst), "ran inside the borrow");
3869            });
3870            assert!(
3871                ran.load(Ordering::SeqCst),
3872                "never ran after the borrow was released"
3873            );
3874            // With the tree free, the same door runs the closure immediately.
3875            let now = Arc::new(AtomicBool::new(false));
3876            let n = now.clone();
3877            run_posted_or_defer(Box::new(move || n.store(true, Ordering::SeqCst)));
3878            assert!(
3879                now.load(Ordering::SeqCst),
3880                "a post with the tree free must run at once"
3881            );
3882            uninstall_tree();
3883        })
3884        .join()
3885        .expect("thread");
3886    }
3887}