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