Skip to main content

tilo_app/framework/
each.rs

1//! A keyed list of rows that follows a collection in the resource, with each
2//! row's item actions in its context menu.
3
4use std::cell::Cell;
5use std::collections::HashMap;
6use std::ops::Range;
7use std::rc::Rc;
8use ui_core::widget::{AppModel, ModelPath, Remount, StructureChange};
9
10use taffy::style_helpers::{length, percent};
11use taffy::{NodeId, Style, TaffyTree};
12use tilo_resource::{Id, Resource};
13use ui_core::a11y::{AccessNode, Role};
14use ui_core::presence::PresenceCtx;
15use ui_core::text_render::TextRenderer;
16use ui_core::vello::Scene;
17use ui_core::vello::kurbo::{Affine, Point, Rect};
18use ui_core::widget::{ContextMenu, Element, MenuItem, TaffyTreeExt, Widget, draw_child};
19use ui_core::winit::event::{ElementState, KeyEvent, MouseButton, WindowEvent};
20use ui_core::winit::keyboard::{Key, NamedKey};
21use ui_core::{Theme, taffy};
22
23use super::dispatch::Dispatcher;
24use super::live::Live;
25use super::reactive::{Gate, Updaters};
26
27/// One entry of a row's context menu: an action published on the row's item.
28struct MenuEntry {
29    address: tilo_actions::ActionAddress,
30    /// Whether the action still takes arguments once the item is bound;
31    /// those open in the action palette, which asks for them.
32    prompts: bool,
33}
34
35/// The context menu of a row: the actions offered on its item.
36#[derive(Default)]
37struct RowMenu {
38    menu: ContextMenu,
39    entries: Vec<MenuEntry>,
40    cursor: Point,
41    /// An entry that needs arguments, for the host to open in the palette.
42    palette: Option<tilo_actions::ActionAddress>,
43}
44
45/// One item of the list: its id and where it sits, and its row once built.
46/// Most items of a long list are never shown, and never build a row.
47struct Row {
48    id: Id,
49    /// Height measured the last time the row was laid out at the list's
50    /// current width. Rows not measured yet take the measured average.
51    height: Option<f64>,
52    /// The row's view, from the first time it was mounted for as long as
53    /// its item exists.
54    view: Option<Box<RowView>>,
55}
56
57/// A built row: its widgets and the gate its readers run behind.
58struct RowView {
59    /// Dropping it retires every binding the row registered.
60    _alive: Rc<()>,
61    /// Whether the row's readers run: while it is mounted.
62    gate: Gate,
63    widget: Element,
64    /// Layout node while the row is mounted.
65    node: Option<NodeId>,
66}
67
68impl Row {
69    fn node(&self) -> Option<NodeId> {
70        self.view.as_ref().and_then(|view| view.node)
71    }
72
73    fn widget(&self) -> Option<&dyn Widget> {
74        self.view.as_ref().map(|view| view.widget.as_ref())
75    }
76}
77
78/// Which rows a list builds.
79#[derive(Clone, Copy, PartialEq, Eq)]
80enum Builds {
81    /// No row was built yet, so it is not known whether rows are read from
82    /// outside.
83    Unknown,
84    /// The rows it mounts, each the first time.
85    OnMount,
86    /// Every row: one is read from outside the row (an edit bound into the
87    /// published tree, an effect it answers), so all of them may be.
88    Every,
89}
90
91/// What the tree pushed down to the list last, for rows built later.
92#[derive(Default)]
93struct Pushed {
94    conventions: Option<ui_core::platform::PlatformConventions>,
95    environment: Option<ui_core::interaction::InteractionEnvironment>,
96    motion_reduced: Option<bool>,
97}
98
99impl Pushed {
100    fn apply(&self, widget: &mut dyn Widget) {
101        if let Some(conventions) = self.conventions {
102            widget.set_platform_conventions(conventions);
103        }
104        if let Some(environment) = self.environment {
105            widget.set_interaction_environment(environment);
106        }
107        if let Some(reduced) = self.motion_reduced {
108            widget.set_motion_reduced(reduced);
109        }
110    }
111}
112
113type Ids<R> = Box<dyn Fn(&<R as Resource>::State) -> Vec<Id>>;
114type BuildRow = Box<dyn Fn(&Id, &Rc<()>, &Gate) -> Element>;
115
116/// Height assumed for a row before any row was measured.
117const ESTIMATED_ROW_HEIGHT: f64 = 32.0;
118/// Rows mounted before the first layout says what the list shows.
119const INITIAL_ROWS: usize = 64;
120/// Least distance beyond the shown part of the list that stays mounted.
121const MIN_OVERSCAN: f64 = 160.0;
122/// Remounts the layouts of one frame (from one `update` to the next draw)
123/// may ask for, so rows whose measured heights keep moving the window cannot
124/// keep a frame from settling.
125const MAX_REMOUNTS_PER_FRAME: u8 = 3;
126/// The height a mounted row is laid out in, the same at any list length:
127/// out of flow, a row is otherwise given the list's whole height, which
128/// changes with every item added and lays every row out again.
129const ROW_SPACE: f32 = 1.0e6;
130
131/// Rows for the items of a collection, in collection order, built by
132/// [`ViewCx::each`](super::ViewCx::each).
133///
134/// A row is built the first time it is mounted, and then stays built — its
135/// bindings, readers and local state live as long as its item — so opening
136/// a list of any length builds the rows it shows, and only removed items
137/// drop rows. Inside a scroll view only the rows near what the view shows
138/// are *mounted*: laid out, synced, drawn and given events. The
139/// list places its rows at measured offsets (estimated for rows never
140/// mounted), so it keeps its whole height and a scroll position keeps its
141/// meaning. Adding or removing an item away from the shown rows changes the
142/// list's geometry, not the window's widget tree. The row holding focus
143/// stays mounted with its neighbours wherever the view is scrolled, so Tab
144/// and the arrow keys always find the next row, as does a row holding the
145/// pointer. A row whose local state something else reads (an edit it binds
146/// into the published tree, an effect it answers) is published whether or
147/// not anyone looked at it, so a list whose rows do that builds every row
148/// as soon as one shows it does (the first row is built with the list to
149/// find out).
150///
151/// Agents and assistive technology reach every item regardless: the items
152/// are published from the resource, not from the rows, and the list's
153/// semantics give each mounted row its position in the whole collection.
154pub struct Each<R: Resource> {
155    live: Live<R>,
156    dispatch: Dispatcher<R>,
157    menu: RowMenu,
158    ids: Ids<R>,
159    build: BuildRow,
160    rows: Vec<Row>,
161    builds: Builds,
162    pushed: Pushed,
163    /// Set when the collection's membership may have changed since the rows
164    /// were last reconciled with it.
165    stale: Rc<Cell<bool>>,
166    /// The view's readers, which run a row's missed changes as it mounts.
167    updaters: Updaters<R>,
168    /// Indices into `rows` of the mounted rows, ascending.
169    mounted: Vec<usize>,
170    /// Top of each row in list coordinates, and the list's whole height.
171    tops: Vec<f64>,
172    total: f64,
173    /// The sum and count of the measured heights, and the estimate the
174    /// tops were laid out with.
175    measured: (f64, u32),
176    estimated: f64,
177    gap: f64,
178    /// Width the measured heights hold for.
179    width: f64,
180    node: Option<NodeId>,
181    /// The node the mounted rows are placed in: [`ROW_SPACE`] tall whatever
182    /// the list's height, so a row's layout does not depend on how many
183    /// items there are and stays cached as items come and go.
184    track: Option<NodeId>,
185    rect: Rect,
186    /// The span of the list a scroll view shows, in list coordinates, as of
187    /// the last layout; `None` when nothing clips the list.
188    visible: Option<(f64, f64)>,
189    laid_out: bool,
190    remounts: u8,
191    /// The structure change the list has to report beside a remount: a
192    /// rebuild when rows it showed left it or moved among the others.
193    structure: StructureChange,
194    /// The rows to mount changed while every row stayed alive (a scroll
195    /// brought rows near or took them away, an item was added): a change
196    /// the list carries out in place ([`StructureChange::Children`]),
197    /// reported until it is carried out, in place or by a rebuild.
198    remount: bool,
199    geometry_dirty: bool,
200}
201
202impl<R: Resource> Each<R> {
203    pub(crate) fn new(
204        live: Live<R>,
205        dispatch: Dispatcher<R>,
206        ids: Ids<R>,
207        stale: Rc<Cell<bool>>,
208        updaters: Updaters<R>,
209        build: BuildRow,
210    ) -> Self {
211        let mut each = Self {
212            stale,
213            updaters,
214            live,
215            dispatch,
216            menu: RowMenu::default(),
217            ids,
218            build,
219            rows: Vec::new(),
220            builds: Builds::Unknown,
221            pushed: Pushed::default(),
222            mounted: Vec::new(),
223            tops: Vec::new(),
224            total: 0.0,
225            measured: (0.0, 0),
226            estimated: ESTIMATED_ROW_HEIGHT,
227            gap: 0.0,
228            width: 0.0,
229            node: None,
230            track: None,
231            rect: Rect::ZERO,
232            visible: None,
233            laid_out: false,
234            remounts: 0,
235            structure: StructureChange::None,
236            remount: false,
237            geometry_dirty: false,
238        };
239        each.reconcile();
240        each
241    }
242
243    fn reconcile(&mut self) {
244        let ids = self.live.with(|state| (self.ids)(state));
245        let unchanged = ids.len() == self.rows.len()
246            && ids.iter().zip(&self.rows).all(|(id, row)| *id == row.id);
247        if unchanged {
248            return;
249        }
250        let was_mounted: Vec<Id> = self
251            .mounted
252            .iter()
253            .filter_map(|&index| self.rows.get(index))
254            .map(|row| row.id.clone())
255            .collect();
256        let mut old: HashMap<Id, Row> = self
257            .rows
258            .drain(..)
259            .map(|row| (row.id.clone(), row))
260            .collect();
261        // An added item's row is built when it is first mounted.
262        self.rows = ids
263            .into_iter()
264            .map(|id| {
265                old.remove(&id).unwrap_or(Row {
266                    id,
267                    height: None,
268                    view: None,
269                })
270            })
271            .collect();
272        // A mounted row that left (its widget is gone from under the node
273        // map) or moved among the mounted ones changes the window's tree.
274        // Anything else only moves rows: the next layout mounts whatever
275        // came into view.
276        self.mounted = self
277            .rows
278            .iter()
279            .enumerate()
280            .filter(|(_, row)| row.node().is_some())
281            .map(|(index, _)| index)
282            .collect();
283        let still_mounted = self.mounted().map(|(id, _)| id);
284        if !still_mounted.eq(was_mounted.iter()) {
285            self.structure = StructureChange::Rebuild;
286        }
287        if self.builds == Builds::Every {
288            self.build_all();
289        } else if self.builds == Builds::Unknown && !self.rows.is_empty() {
290            // Whether rows are read from outside, which makes every row's
291            // view part of the published tree.
292            self.ensure_built(0);
293        }
294        self.recount();
295        self.relayout_tops(0);
296        self.geometry_dirty = true;
297    }
298
299    /// Build the row at `index` if it is not built yet, from the state as
300    /// it is now. A row read from outside makes the list build every row.
301    fn ensure_built(&mut self, index: usize) {
302        let Some(row) = self.rows.get_mut(index) else {
303            return;
304        };
305        if row.view.is_some() {
306            return;
307        }
308        let alive = Rc::new(());
309        let gate = Gate::mounted();
310        let mut widget = (self.build)(&row.id, &alive, &gate);
311        self.pushed.apply(widget.as_mut());
312        let reads_unmounted = gate.reads_unmounted();
313        row.view = Some(Box::new(RowView {
314            _alive: alive,
315            gate,
316            widget,
317            node: None,
318        }));
319        if reads_unmounted && self.builds != Builds::Every {
320            self.builds = Builds::Every;
321            self.build_all();
322        } else if self.builds == Builds::Unknown {
323            self.builds = Builds::OnMount;
324        }
325    }
326
327    fn build_all(&mut self) {
328        for index in 0..self.rows.len() {
329            self.ensure_built(index);
330        }
331    }
332
333    /// Make `window` (ascending indices) the rows that follow changes,
334    /// building the rows it mounts for the first time: rows leaving it stop
335    /// following, and rows entering it catch up with the changes they
336    /// missed before they are laid out.
337    fn open_window(&mut self, window: &[usize]) {
338        for &index in window {
339            self.ensure_built(index);
340        }
341        for (index, row) in self.rows.iter().enumerate() {
342            let Some(view) = row.view.as_ref() else {
343                continue;
344            };
345            let shown = window.binary_search(&index).is_ok();
346            view.gate.set_mounted(shown);
347            if shown {
348                self.live
349                    .with(|state| self.updaters.catch_up(&view.gate, state));
350            }
351        }
352    }
353
354    /// Height assumed for rows not measured yet: the measured average.
355    fn estimate(&self) -> f64 {
356        let (sum, count) = self.measured;
357        if count == 0 {
358            ESTIMATED_ROW_HEIGHT
359        } else {
360            sum / f64::from(count)
361        }
362    }
363
364    /// Sum the measured heights again, after rows came or went.
365    fn recount(&mut self) {
366        self.measured = self
367            .rows
368            .iter()
369            .filter_map(|row| row.height)
370            .fold((0.0, 0_u32), |(sum, count), height| {
371                (sum + height, count.saturating_add(1))
372            });
373    }
374
375    /// Place the rows from `from` on; all of them when the estimate the
376    /// others were placed with moved.
377    fn relayout_tops(&mut self, from: usize) {
378        let estimate = self.estimate();
379        let from = if (estimate - self.estimated).abs() > f64::EPSILON
380            || self.tops.len() != self.rows.len()
381        {
382            0
383        } else {
384            from.min(self.rows.len())
385        };
386        self.estimated = estimate;
387        self.tops.truncate(from);
388        let mut y = from
389            .checked_sub(1)
390            .and_then(|last| {
391                let top = self.tops.get(last)?;
392                let row = self.rows.get(last)?;
393                Some(top + row.height.unwrap_or(estimate))
394            })
395            .unwrap_or(0.0);
396        for (index, row) in self.rows.iter().enumerate().skip(from) {
397            if index > 0 {
398                y += self.gap;
399            }
400            self.tops.push(y);
401            y += row.height.unwrap_or(estimate);
402        }
403        self.total = y;
404    }
405
406    /// The rows reaching into `top..bottom` of the list, by index. A row
407    /// reaches down to the next row's top.
408    fn rows_between(&self, top: f64, bottom: f64) -> Range<usize> {
409        let first = self
410            .tops
411            .partition_point(|&row_top| row_top <= top)
412            .saturating_sub(1);
413        let end = self.tops.partition_point(|&row_top| row_top < bottom);
414        first..end.max(first)
415    }
416
417    /// Distance beyond what the view shows that the window keeps mounted.
418    fn overscan(&self) -> f64 {
419        self.visible.map_or(0.0, |(top, bottom)| {
420            ((bottom - top) * 0.5).max(MIN_OVERSCAN)
421        })
422    }
423
424    /// The mounted row holding keyboard focus, by index.
425    fn focused_index(&self) -> Option<usize> {
426        self.mounted.iter().copied().find(|&index| {
427            self.rows
428                .get(index)
429                .and_then(Row::widget)
430                .is_some_and(holds_focus)
431        })
432    }
433
434    /// The focused row and the rows beside it: where Tab and the arrow keys
435    /// go next, whether or not the view follows focus.
436    fn around_focus(&self) -> Range<usize> {
437        self.focused_index().map_or(0..0, |index| {
438            index.saturating_sub(1)..index.saturating_add(2).min(self.rows.len())
439        })
440    }
441
442    /// The rows to mount: all of them when nothing clips the list; else the
443    /// shown ones with an overscan on both sides, the focused row and its
444    /// neighbours, and every mounted row holding the pointer.
445    fn window(&self) -> Vec<usize> {
446        let count = self.rows.len();
447        if !self.laid_out {
448            return (0..count.min(INITIAL_ROWS)).collect();
449        }
450        let Some((top, bottom)) = self.visible else {
451            return (0..count).collect();
452        };
453        let overscan = self.overscan();
454        let mut window: Vec<usize> = self
455            .rows_between(top - overscan, bottom + overscan)
456            .collect();
457        window.extend(self.around_focus());
458        window.extend(self.mounted.iter().copied().filter(|&index| {
459            self.rows
460                .get(index)
461                .and_then(Row::widget)
462                .is_some_and(|widget| {
463                    widget.wants_pointer_capture() || widget.modal_capture().is_some()
464                })
465        }));
466        window.sort_unstable();
467        window.dedup();
468        window
469    }
470
471    /// Whether the mounted rows no longer suit what the view shows: a row
472    /// near the shown ones or beside the focused one is missing, or far more
473    /// are mounted than needed.
474    fn window_stale(&self) -> bool {
475        let Some((top, bottom)) = self.visible else {
476            return self.mounted.len() != self.rows.len();
477        };
478        let mounts = |rows: Range<usize>| {
479            rows.is_empty()
480                || self.mounted.binary_search(&rows.start).is_ok_and(|at| {
481                    let last = at.saturating_add(rows.len()).saturating_sub(1);
482                    self.mounted.get(last) == Some(&rows.end.saturating_sub(1))
483                })
484        };
485        let overscan = self.overscan();
486        let needed = self.rows_between(top - overscan / 4.0, bottom + overscan / 4.0);
487        let wanted = self.rows_between(top - overscan, bottom + overscan).len();
488        !mounts(needed)
489            || !mounts(self.around_focus())
490            || self.mounted.len() > wanted.saturating_mul(2).saturating_add(8)
491    }
492
493    /// Ids of the items, in order.
494    pub fn row_ids(&self) -> impl Iterator<Item = &Id> {
495        self.rows.iter().map(|row| &row.id)
496    }
497
498    /// How many rows are mounted: laid out, synced, drawn and given events.
499    #[must_use]
500    pub fn mounted_rows(&self) -> usize {
501        self.mounted.len()
502    }
503
504    /// The mounted rows, each with its item's id. A mounted row is built.
505    fn mounted(&self) -> impl Iterator<Item = (&Id, &RowView)> {
506        self.mounted.iter().filter_map(|&index| {
507            let row = self.rows.get(index)?;
508            Some((&row.id, row.view.as_deref()?))
509        })
510    }
511
512    fn for_each_mounted(&mut self, mut visit: impl FnMut(&mut RowView)) {
513        for &index in &self.mounted {
514            if let Some(view) = self.rows.get_mut(index).and_then(|row| row.view.as_mut()) {
515                visit(view);
516            }
517        }
518    }
519
520    /// Bring the list's own height and the mounted rows' offsets in `taffy`
521    /// up to date. Returns whether a style changed.
522    fn push_geometry(&mut self, taffy: &mut TaffyTree<()>) -> bool {
523        let mut changed = false;
524        if let Some(node) = self.node
525            && let Ok(style) = taffy.style(node)
526            && style.size.height != length(px(self.total))
527        {
528            let mut style = style.clone();
529            style.size.height = length(px(self.total));
530            changed |= taffy.set_style(node, style).is_ok();
531        }
532        for &index in &self.mounted {
533            let Some(child) = self.rows.get(index).and_then(Row::node) else {
534                continue;
535            };
536            let top = length(px(self.tops.get(index).copied().unwrap_or_default()));
537            if let Ok(style) = taffy.style(child)
538                && style.inset.top != top
539            {
540                let mut style = style.clone();
541                style.inset.top = top;
542                changed |= taffy.set_style(child, style).is_ok();
543            }
544        }
545        self.geometry_dirty = false;
546        changed
547    }
548
549    /// Open the context menu of the row for `item` at `at`: every action the
550    /// resource offers on that item. Returns whether there was any.
551    fn open_menu(&mut self, item: &Id, at: Point) -> bool {
552        let children = self.live.with(|state| R::children(state));
553        let Some(resource) = children.iter().find(|child| {
554            tilo_resource::items::item_of_resource::<R>(child.id.as_str())
555                .is_some_and(|(_, id)| &id == item)
556        }) else {
557            return false;
558        };
559        let read_only = self.dispatch.read_only();
560        let mut menu = ContextMenu::new();
561        let mut entries = Vec::new();
562        for spec in &resource.actions {
563            let address = tilo_actions::ActionAddress::new(resource.id.as_str(), spec.id.as_str());
564            let entry = if spec.contract.risk == tilo_actions::RiskLevel::Destructive {
565                MenuItem::destructive_selection(spec.label.clone())
566            } else {
567                MenuItem::selection(spec.label.clone())
568            };
569            menu.push(entry.for_action(address.clone()).with_enabled(!read_only));
570            entries.push(MenuEntry {
571                address,
572                prompts: !spec.params().is_empty(),
573            });
574        }
575        // The row's item, in its resource, is what the menu acts on and a
576        // question typed into it is about, unless the right-click landed
577        // inside what is selected in the list: then the selection is, as in
578        // every context menu (`shared_selection::pointed_selection`).
579        let selected = ui_core::shared_selection::pointed_selection(
580            ui_core::shared_selection::subtree_selection(&*self),
581            Some(at),
582        );
583        let mut about = ui_core::shared_selection::SharedSelection::of_text(&resource.label)
584            .at(tilo_actions::SelectionPlace::item(item.as_str()))
585            .in_resource(tilo_actions::ActionResource {
586                id: resource.id.0.clone(),
587                kind: resource.kind.clone(),
588                label: resource.label.clone(),
589            });
590        if let Some((_, view)) = self.mounted().find(|(id, _)| *id == item) {
591            about = about.within(view.widget.get_rect());
592        }
593        menu.set_about(Some(selected.unwrap_or(about)));
594        #[allow(
595            clippy::cast_possible_truncation,
596            reason = "a menu anchor is a logical pixel position on screen"
597        )]
598        menu.open_at([at.x as f32, at.y as f32]);
599        if menu.items().is_empty() {
600            return false;
601        }
602        self.menu.menu = menu;
603        self.menu.entries = entries;
604        true
605    }
606
607    /// Act on the menu entry the user chose: invoke it, or hand it to the
608    /// palette when it still needs arguments.
609    fn choose(&mut self, index: usize) {
610        let Some(MenuEntry { address, prompts }) = self.menu.entries.get(index) else {
611            return;
612        };
613        if *prompts {
614            self.menu.palette = Some(address.clone());
615        } else {
616            self.dispatch
617                .invoke_at(address.clone(), serde_json::Map::new());
618        }
619    }
620
621    /// The row holding keyboard focus.
622    fn focused_row(&self) -> Option<(&Id, &dyn Widget)> {
623        let row = self.rows.get(self.focused_index()?)?;
624        Some((&row.id, row.widget()?))
625    }
626
627    /// Route an event to the row menu. Returns whether it was consumed.
628    fn menu_event(&mut self, event: &WindowEvent, ctx: &mut ui_core::widget::EventCtx<'_>) -> bool {
629        if let WindowEvent::CursorMoved { position, .. } = event {
630            self.menu.cursor = ctx.logical(position);
631        }
632        if self.menu.menu.is_open() {
633            self.menu.menu.handle_event(event, ctx);
634            if let Some(index) = self.menu.menu.take_selection() {
635                self.choose(index);
636            }
637            return *ctx.handled;
638        }
639        let target = match event {
640            WindowEvent::MouseInput {
641                state: ElementState::Pressed,
642                button: MouseButton::Right,
643                ..
644            } => self
645                .mounted()
646                .find(|(_, view)| view.widget.get_rect().contains(self.menu.cursor))
647                .map(|(id, _)| (id.clone(), self.menu.cursor)),
648            WindowEvent::KeyboardInput {
649                event:
650                    KeyEvent {
651                        logical_key,
652                        state: ElementState::Pressed,
653                        ..
654                    },
655                ..
656            } if matches!(logical_key, Key::Named(NamedKey::ContextMenu))
657                || (matches!(logical_key, Key::Named(NamedKey::F10))
658                    && ctx.modifiers.state().shift_key()) =>
659            {
660                self.focused_row().map(|(id, widget)| {
661                    let rect = widget.get_rect();
662                    (id.clone(), Point::new(rect.x0, rect.y1))
663                })
664            }
665            _ => None,
666        };
667        let Some((item, at)) = target else {
668            return false;
669        };
670        if self.open_menu(&item, at) {
671            *ctx.handled = true;
672            return true;
673        }
674        false
675    }
676}
677
678/// Place a row's layout `node` in the list: out of flow, as wide as the
679/// list, `top` below its top.
680fn place_row(taffy: &mut TaffyTree<()>, node: NodeId, top: f32) {
681    let mut style = taffy.style(node).cloned().unwrap_or_default();
682    style.position = taffy::Position::Absolute;
683    style.inset.left = length(0.0_f32);
684    style.inset.right = length(0.0_f32);
685    style.inset.top = length(top);
686    style.size.width = taffy::Dimension::auto();
687    let _ = taffy.set_style(node, style);
688}
689
690/// Whether `widget` or a descendant holds keyboard focus.
691fn holds_focus(widget: &dyn Widget) -> bool {
692    let mut found = widget.is_focused();
693    widget.for_each_child(&mut |child| found |= holds_focus(child));
694    found
695}
696
697fn px(value: f64) -> f32 {
698    #[allow(
699        clippy::cast_possible_truncation,
700        reason = "list offsets are logical pixels, far below f32::MAX"
701    )]
702    let value = value as f32;
703    value
704}
705
706impl<R: Resource> Widget for Each<R> {
707    fn sync(&mut self, state: &AppModel, path: &ModelPath) {
708        if self.stale.replace(false) {
709            self.reconcile();
710        }
711        // A row synced again when it is next mounted: that rebuild syncs
712        // the whole tree.
713        self.for_each_mounted(|row| row.widget.sync(state, path));
714    }
715
716    // The per-frame update pass drives retained models; every mounted row
717    // sees it. It also opens the frame's remount budget.
718    fn update(&mut self, state: &AppModel, path: &ModelPath) -> bool {
719        self.remounts = 0;
720        let mut changed = false;
721        self.for_each_mounted(|row| changed |= row.widget.update(state, path));
722        changed
723    }
724
725    // Inputs pushed down the tree reach every built row, mounted or not,
726    // and rows built later, so a row mounted later shows the same as one
727    // that always was.
728    fn set_platform_conventions(&mut self, conventions: ui_core::platform::PlatformConventions) {
729        self.pushed.conventions = Some(conventions);
730        for view in self.rows.iter_mut().filter_map(|row| row.view.as_mut()) {
731            view.widget.set_platform_conventions(conventions);
732        }
733    }
734
735    fn set_interaction_environment(
736        &mut self,
737        environment: ui_core::interaction::InteractionEnvironment,
738    ) {
739        self.pushed.environment = Some(environment);
740        for view in self.rows.iter_mut().filter_map(|row| row.view.as_mut()) {
741            view.widget.set_interaction_environment(environment);
742        }
743    }
744
745    fn set_motion_reduced(&mut self, reduced: bool) {
746        self.pushed.motion_reduced = Some(reduced);
747        for view in self.rows.iter_mut().filter_map(|row| row.view.as_mut()) {
748            view.widget.set_motion_reduced(reduced);
749        }
750    }
751
752    fn build_layout(
753        &mut self,
754        taffy: &mut TaffyTree<()>,
755        theme: &Theme,
756        node_map: &mut HashMap<NodeId, *mut dyn Widget>,
757    ) -> NodeId {
758        let gap = f64::from(theme.gap_standard);
759        if (gap - self.gap).abs() > f64::EPSILON {
760            self.gap = gap;
761            self.relayout_tops(0);
762        }
763        let window = self.window();
764        // Only mounted rows hold a node.
765        self.for_each_mounted(|view| view.node = None);
766        self.open_window(&window);
767        let mut nodes = Vec::with_capacity(window.len());
768        for &index in &window {
769            let top = px(self.tops.get(index).copied().unwrap_or_default());
770            let Some(row) = self.rows.get_mut(index).and_then(|row| row.view.as_mut()) else {
771                continue;
772            };
773            // A row out of the tree kept no structure of its own to report:
774            // it is built fresh here.
775            let _ = row.widget.take_structure_dirty();
776            let child = row.widget.build_layout(taffy, theme, node_map);
777            place_row(taffy, child, top);
778            row.node = Some(child);
779            nodes.push(child);
780        }
781        self.mounted = window;
782        let track = taffy.parent_node(
783            Style {
784                position: taffy::Position::Absolute,
785                inset: taffy::Rect {
786                    left: length(0.0_f32),
787                    right: length(0.0_f32),
788                    top: length(0.0_f32),
789                    bottom: taffy::LengthPercentageAuto::auto(),
790                },
791                size: taffy::Size {
792                    width: taffy::Dimension::auto(),
793                    height: length(ROW_SPACE),
794                },
795                ..Default::default()
796            },
797            &nodes,
798        );
799        // The track is taller than the list: it stands in a box as tall as
800        // the list that clips what it adds to the list's content, so a
801        // scroll view around the list scrolls the list's own height.
802        let clip = taffy.parent_node(
803            Style {
804                position: taffy::Position::Absolute,
805                inset: taffy::Rect {
806                    left: length(0.0_f32),
807                    right: length(0.0_f32),
808                    top: length(0.0_f32),
809                    bottom: length(0.0_f32),
810                },
811                overflow: taffy::Point {
812                    x: taffy::Overflow::Visible,
813                    y: taffy::Overflow::Clip,
814                },
815                ..Default::default()
816            },
817            &[track],
818        );
819        let style = Style {
820            size: taffy::Size {
821                width: percent(1.0_f32),
822                height: length(px(self.total)),
823            },
824            flex_shrink: 0.0,
825            ..Default::default()
826        };
827        let node = taffy.parent_node(style, &[clip]);
828        node_map.insert(node, std::ptr::from_mut::<dyn Widget>(self));
829        self.node = Some(node);
830        self.track = Some(track);
831        self.structure = StructureChange::None;
832        self.remount = false;
833        self.geometry_dirty = false;
834        node
835    }
836
837    // The rows to mount changed and every row is still alive: let go of the
838    // nodes of rows leaving the window, mount the rows entering it, and keep
839    // the others' nodes, with their cached layout, where they are.
840    fn remount_children(&mut self, remount: &mut Remount<'_>) -> bool {
841        let mut applied = true;
842        if !self.remount {
843            self.for_each_mounted(|view| {
844                applied = applied && view.widget.remount_children(remount);
845            });
846            return applied;
847        }
848        let (Some(node), Some(track)) = (self.node, self.track) else {
849            return false;
850        };
851        let window = self.window();
852        for &index in &self.mounted {
853            if window.binary_search(&index).is_err()
854                && let Some(child) = self
855                    .rows
856                    .get_mut(index)
857                    .and_then(|row| row.view.as_mut())
858                    .and_then(|view| view.node.take())
859            {
860                remount.unmount(child);
861            }
862        }
863        self.open_window(&window);
864        let mut nodes = Vec::with_capacity(window.len());
865        for &index in &window {
866            let top = px(self.tops.get(index).copied().unwrap_or_default());
867            let Some(row) = self.rows.get_mut(index).and_then(|row| row.view.as_mut()) else {
868                continue;
869            };
870            let child = if let Some(child) = row.node {
871                applied = applied && row.widget.remount_children(remount);
872                child
873            } else {
874                let _ = row.widget.take_structure_dirty();
875                let child = remount.mount(node, row.widget.as_mut());
876                place_row(remount.taffy(), child, top);
877                row.node = Some(child);
878                child
879            };
880            nodes.push(child);
881        }
882        self.mounted = window;
883        self.remount = false;
884        // The retained rows' offsets and the list's height, as the measured
885        // heights put them.
886        self.push_geometry(remount.taffy());
887        applied && remount.set_children(track, &nodes).is_ok()
888    }
889
890    // Measured rows move the rows after them: read what Taffy measured and
891    // place the mounted rows (and the list's height) to match.
892    fn adjust_layout_style(
893        &mut self,
894        taffy: &mut TaffyTree<()>,
895        node: NodeId,
896        _text: &mut TextRenderer,
897    ) -> bool {
898        if self.node != Some(node) {
899            return false;
900        }
901        let width = taffy
902            .layout(node)
903            .map_or(0.0, |layout| f64::from(layout.size.width));
904        let mut measured = (width - self.width).abs() > 0.5;
905        if measured {
906            // Heights measured at another width no longer hold.
907            self.width = width;
908            for row in &mut self.rows {
909                if row.node().is_none() {
910                    row.height = None;
911                }
912            }
913            self.recount();
914        }
915        // The first row whose height moved: the rows before it stay put.
916        let mut moved = None;
917        for &index in &self.mounted {
918            let Some(row) = self.rows.get_mut(index) else {
919                continue;
920            };
921            let Some(height) = row
922                .node()
923                .and_then(|child| taffy.layout(child).ok())
924                .map(|layout| f64::from(layout.size.height))
925            else {
926                continue;
927            };
928            if row.height.is_none_or(|known| (known - height).abs() > 0.01) {
929                let (sum, count) = self.measured;
930                self.measured = match row.height.replace(height) {
931                    Some(known) => (sum - known + height, count),
932                    None => (sum + height, count.saturating_add(1)),
933                };
934                moved = Some(moved.map_or(index, |first: usize| first.min(index)));
935            }
936        }
937        if measured {
938            self.relayout_tops(0);
939        } else if let Some(first) = moved {
940            measured = true;
941            self.relayout_tops(first);
942        }
943        if measured || self.geometry_dirty {
944            self.push_geometry(taffy)
945        } else {
946            false
947        }
948    }
949
950    fn apply_layout(&mut self, taffy: &TaffyTree<()>, node: NodeId, origin: Point) {
951        self.rect = ui_core::widget::layout_rect(taffy, node, origin);
952        let inner = self.rect.origin();
953        self.for_each_mounted(|view| {
954            if let Some(child) = view.node {
955                view.widget.apply_layout(taffy, child, inner);
956            }
957        });
958        self.visible = ui_core::widget::layout_clip()
959            .map(|clip| (clip.y0 - self.rect.y0, clip.y1 - self.rect.y0));
960        self.laid_out = true;
961        if self.window_stale() && self.remounts < MAX_REMOUNTS_PER_FRAME {
962            self.remounts = self.remounts.saturating_add(1);
963            self.remount = true;
964        }
965    }
966
967    fn draw(
968        &mut self,
969        scene: &mut Scene,
970        transform: Affine,
971        text: &mut TextRenderer,
972        theme: &Theme,
973        presence: &PresenceCtx,
974    ) {
975        self.remounts = 0;
976        self.for_each_mounted(|view| {
977            draw_child(
978                view.widget.as_mut(),
979                scene,
980                transform,
981                text,
982                theme,
983                presence,
984            );
985        });
986    }
987
988    fn handle_event(&mut self, event: &WindowEvent, ctx: &mut ui_core::widget::EventCtx<'_>) {
989        if self.menu_event(event, ctx) {
990            return;
991        }
992        let capturing = self.mounted.iter().copied().find(|&index| {
993            self.rows
994                .get(index)
995                .and_then(Row::widget)
996                .is_some_and(ui_core::widget::holds_pointer)
997        });
998        if let Some(index) = capturing {
999            if let Some(view) = self.rows.get_mut(index).and_then(|row| row.view.as_mut())
1000                && let Some(child) = view.node
1001            {
1002                view.widget
1003                    .handle_event(event, &mut ctx.reborrow(ctx.path, child));
1004            }
1005            return;
1006        }
1007        for &index in &self.mounted {
1008            let Some(view) = self.rows.get_mut(index).and_then(|row| row.view.as_mut()) else {
1009                continue;
1010            };
1011            let Some(child) = view.node else {
1012                continue;
1013            };
1014            view.widget
1015                .handle_event(event, &mut ctx.reborrow(ctx.path, child));
1016            if *ctx.handled {
1017                break;
1018            }
1019        }
1020    }
1021
1022    fn wants_pointer_capture(&self) -> bool {
1023        self.menu.menu.is_open()
1024            || self
1025                .mounted()
1026                .any(|(_, view)| view.widget.wants_pointer_capture())
1027    }
1028
1029    fn modal_capture(&self) -> Option<Rect> {
1030        let mut capture = self.menu.menu.modal_capture();
1031        for (_, view) in self.mounted() {
1032            if capture.is_none() {
1033                capture = view.widget.modal_capture();
1034            }
1035        }
1036        capture
1037    }
1038
1039    fn draw_overlay(
1040        &mut self,
1041        scene: &mut Scene,
1042        transform: Affine,
1043        text: &mut TextRenderer,
1044        theme: &Theme,
1045        presence: &PresenceCtx,
1046    ) {
1047        self.for_each_mounted(|row| {
1048            row.widget
1049                .draw_overlay(scene, transform, text, theme, presence);
1050        });
1051        // Drawn while it is open and while it leaves (`motion::Appear`);
1052        // a closed menu draws nothing.
1053        self.menu
1054            .menu
1055            .draw_overlay(scene, transform, text, theme, presence);
1056    }
1057
1058    fn take_action_palette_request(&mut self) -> Option<tilo_actions::ActionAddress> {
1059        if let Some(address) = self.menu.palette.take() {
1060            return Some(address);
1061        }
1062        let mut request = None;
1063        self.for_each_mounted(|row| {
1064            if request.is_none() {
1065                request = row.widget.take_action_palette_request();
1066            }
1067        });
1068        request
1069    }
1070
1071    fn take_search_request(&mut self) -> bool {
1072        let mut requested = false;
1073        self.for_each_mounted(|row| requested |= row.widget.take_search_request());
1074        requested
1075    }
1076
1077    fn take_structure_dirty(&mut self) -> bool {
1078        let mut dirty = std::mem::take(&mut self.structure).rebuilds() || self.remount;
1079        self.for_each_mounted(|row| dirty |= row.widget.take_structure_dirty());
1080        dirty
1081    }
1082
1083    /// Mounting the rows a scroll or an added item brought near, and letting
1084    /// go of the ones it took away, is carried out in place: every row stays
1085    /// alive, they came and went outside what the view shows or where the
1086    /// scroll exposed it, and a mounted row draws itself whole. Rows that
1087    /// left the list or moved among the others rebuild the tree.
1088    fn take_structure_change(&mut self) -> StructureChange {
1089        let mut change = std::mem::take(&mut self.structure);
1090        if self.remount {
1091            change = change.join(StructureChange::Children);
1092        }
1093        self.for_each_mounted(|view| change = change.join(view.widget.take_structure_change()));
1094        change
1095    }
1096
1097    fn is_layout_dirty(&self) -> bool {
1098        self.geometry_dirty
1099            || self
1100                .mounted()
1101                .any(|(_, view)| view.widget.is_layout_dirty())
1102    }
1103
1104    fn layout_needs_measure(&self) -> bool {
1105        self.geometry_dirty
1106            || self
1107                .mounted()
1108                .any(|(_, view)| view.widget.layout_needs_measure())
1109    }
1110
1111    // The mounted rows, and an open row menu: it is part of the tree, so
1112    // assistive technology and inspection see it; it lays itself out where
1113    // it opened.
1114    /// The list draws only its mounted rows (and an open menu, in the top
1115    /// layer): its damage is theirs. Rows that came, went or moved are the
1116    /// host's to repaint, which compares every widget's box.
1117    fn collect_damage(&self, regions: &mut Vec<Rect>) {
1118        self.for_each_child(&mut |child| child.collect_damage(regions));
1119    }
1120
1121    fn children(&self) -> Vec<&dyn Widget> {
1122        let open = self
1123            .menu
1124            .menu
1125            .is_open()
1126            .then_some(&self.menu.menu as &dyn Widget);
1127        self.mounted()
1128            .map(|(_, view)| view.widget.as_ref())
1129            .chain(open)
1130            .collect()
1131    }
1132
1133    fn children_mut(&mut self) -> Vec<&mut (dyn Widget + '_)> {
1134        let open = self.menu.menu.is_open();
1135        let mut children: Vec<&mut (dyn Widget + '_)> =
1136            Vec::with_capacity(self.mounted.len().saturating_add(1));
1137        // `mounted` is ascending: split each mounted row off the rest.
1138        let mut rest: &mut [Row] = &mut self.rows;
1139        let mut start = 0_usize;
1140        for &index in &self.mounted {
1141            let Some((row, tail)) = index.checked_sub(start).and_then(|offset| {
1142                std::mem::take(&mut rest)
1143                    .get_mut(offset..)
1144                    .and_then(<[Row]>::split_first_mut)
1145            }) else {
1146                break;
1147            };
1148            if let Some(view) = row.view.as_mut() {
1149                children.push(view.widget.as_mut());
1150            }
1151            rest = tail;
1152            start = index.saturating_add(1);
1153        }
1154        if open {
1155            children.push(&mut self.menu.menu);
1156        }
1157        children
1158    }
1159
1160    // A list of every item: each mounted row sits in an item that says
1161    // where it stands in the whole collection, so a screen reader announces
1162    // "4 of 1,000" whichever rows are mounted.
1163    fn accessibility(&self) -> Option<AccessNode> {
1164        let count = self.rows.len();
1165        let items = self
1166            .mounted
1167            .iter()
1168            .filter_map(|&index| {
1169                let widget = self.rows.get(index)?.widget()?;
1170                Some(
1171                    AccessNode::new(Role::ListItem)
1172                        .with_position(index.saturating_add(1), count)
1173                        .with_bounds(widget.get_rect()),
1174                )
1175            })
1176            .collect();
1177        Some(AccessNode::new(Role::List).with_children(items))
1178    }
1179
1180    fn accessibility_child_parent(&self, child_index: usize) -> Option<usize> {
1181        (child_index < self.mounted.len()).then_some(child_index)
1182    }
1183
1184    fn name(&self) -> &'static str {
1185        "Each"
1186    }
1187
1188    fn get_rect(&self) -> Rect {
1189        self.rect
1190    }
1191}