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