Skip to main content

ravenrs_widgets/lists/
virtual_list.rs

1//! A list that builds only what shows, with rows of any height.
2
3use std::cell::{Cell, RefCell};
4use std::collections::HashMap;
5use std::fmt;
6use std::ops::Range;
7use std::rc::Rc;
8
9use ravenrs_core::{Border, Color, CursorIcon, Key, Point, Role, Size};
10use ravenrs_layout::{Align, Axis, LayoutStyle};
11use ravenrs_reactive::{Binding, State};
12use ravenrs_style::Semantic;
13
14use crate::context::{BuildCx, NodeId, Paint, QuadPaint, Semantics};
15use crate::view::{AnyView, View};
16
17/// Rows built past each end of the view, so that a fast wheel does not show holes.
18const OVERSCAN: usize = 3;
19/// What an arrow moves a list with nothing to select.
20const LINE_STEP: f32 = 40.0;
21/// Differences smaller than this are rounding, not a scroll.
22const SAME_OFFSET: f32 = 0.25;
23
24/// How a list knows the size of its rows.
25#[derive(Clone)]
26pub(crate) enum Sizing {
27    /// Laid out and then read back; until then, this estimate.
28    Measured(f32),
29    /// All the same, so nothing has to be measured.
30    Fixed(f32),
31    /// Given by whoever uses the list.
32    Known(Rc<dyn Fn(usize) -> f32>),
33}
34
35/// An order given to a list from outside its own build.
36#[derive(Clone, Copy, Debug, PartialEq)]
37pub(crate) enum ScrollCommand {
38    /// Puts this row at the start of the view.
39    ToIndex(usize),
40    /// The least scroll that shows this row whole.
41    Reveal(usize),
42    /// Moves by this many logical pixels.
43    By(f32),
44}
45
46/// What a list found out in its last build, for the view around it.
47#[derive(Clone, Copy, Debug, Default)]
48pub(crate) struct ListProbe {
49    /// How far it is scrolled.
50    pub offset: f32,
51    /// How long the view is, along the list.
52    pub view: f32,
53    /// How long all the rows are.
54    pub total: f32,
55    /// The row in the middle of the view.
56    pub current: usize,
57}
58
59/// Where each row is. It costs a look at every key per build and an addition per row when
60/// something changed, and never a lookup by key unless the keys themselves changed.
61#[derive(Default)]
62struct Track {
63    keys: Vec<u64>,
64    sizes: Vec<f32>,
65    /// `sizes` added up, one entry longer: `tops[n]` is the whole length.
66    tops: Vec<f32>,
67    measured: HashMap<u64, f32>,
68    /// The rows the last build made, which are the ones with something new to read.
69    built: Vec<(u64, usize)>,
70    /// Room before the first row and after the last.
71    padding: f32,
72    /// The row at the start of the view and how far past its start the view is. What stays
73    /// still when rows above it come in or turn out to have another size.
74    anchor: Option<(u64, usize, f32)>,
75    /// The offset the last build left; another one here means somebody scrolled.
76    left_at: Option<f32>,
77    at_end: bool,
78    /// A row to show whole, kept until the rows around it have settled on their size.
79    revealing: Option<usize>,
80    visible: Option<Range<usize>>,
81}
82
83#[cfg_attr(feature = "live", ravenrs_live::expose)]
84impl Track {
85    fn total(&self) -> f32 {
86        self.tops.last().map_or(0.0, |end| end + self.padding)
87    }
88
89    fn max_offset(&self, view: f32) -> f32 {
90        (self.total() - view).max(0.0)
91    }
92
93    fn sum(&mut self) {
94        self.tops.clear();
95        self.tops.reserve(self.sizes.len() + 1);
96        let mut at = self.padding;
97        self.tops.push(at);
98        for size in &self.sizes {
99            at += size;
100            self.tops.push(at);
101        }
102    }
103
104    /// Brings keys and sizes up to date with the data.
105    fn sync(&mut self, list: &VirtualList) {
106        let count = list.count;
107        let anchor_size = self.anchor.and_then(|(_, index, _)| self.sizes.get(index).copied());
108        let mut stale = self.keys.len() != count;
109        if !stale && let Some(key) = &list.key {
110            stale = (0..count).any(|i| self.keys[i] != key(i));
111        }
112        let mut changed = stale || self.padding != list.padding;
113        self.padding = list.padding;
114        if stale {
115            self.keys.clear();
116            self.keys.extend((0..count).map(|i| list.key.as_ref().map_or(i as u64, |key| key(i))));
117            let default = list.default_size();
118            self.sizes.clear();
119            for key in &self.keys {
120                self.sizes.push(self.measured.get(key).copied().unwrap_or(default));
121            }
122        }
123        match &list.sizing {
124            Sizing::Measured(_) => {}
125            Sizing::Fixed(size) => {
126                let size = size + list.spacing;
127                if self.sizes.first().is_some_and(|s| (s - size).abs() > f32::EPSILON) {
128                    self.sizes.fill(size);
129                    changed = true;
130                }
131            }
132            Sizing::Known(size_of) => {
133                for i in 0..count {
134                    let size = size_of(i) + list.spacing;
135                    if (self.sizes[i] - size).abs() > f32::EPSILON {
136                        self.sizes[i] = size;
137                        changed = true;
138                    }
139                }
140                // A zoom changes every row: the view stays at the same place inside its row.
141                if let (Some(before), Some((key, index, delta))) = (anchor_size, self.anchor)
142                    && let Some(&now) = self.sizes.get(index)
143                    && before > 0.0
144                {
145                    self.anchor = Some((key, index, delta * now / before));
146                }
147            }
148        }
149        if changed || self.tops.len() != count + 1 {
150            self.sum();
151        }
152    }
153
154    fn take_measurements(&mut self, cx: &BuildCx<'_>, list: &VirtualList) {
155        if !matches!(list.sizing, Sizing::Measured(_)) {
156            return;
157        }
158        let mut changed = false;
159        for &(key, index) in &self.built {
160            let Some(frame) = cx.keyed_child_frame(key) else { continue };
161            let size = list.along(frame);
162            if self.measured.get(&key).is_none_or(|known| (known - size).abs() > 0.1) {
163                self.measured.insert(key, size);
164                // Rows moved since then found their size when the keys were taken.
165                if self.keys.get(index) == Some(&key) {
166                    self.sizes[index] = size;
167                }
168                changed = true;
169            }
170        }
171        if changed {
172            self.sum();
173        }
174    }
175
176    /// The row holding this offset.
177    fn index_at(&self, offset: f32) -> usize {
178        let last = self.keys.len().saturating_sub(1);
179        self.tops.partition_point(|top| *top <= offset).saturating_sub(1).min(last)
180    }
181
182    fn set_anchor(&mut self, offset: f32) {
183        if self.keys.is_empty() {
184            self.anchor = None;
185            return;
186        }
187        let index = self.index_at(offset);
188        self.anchor = Some((self.keys[index], index, offset - self.tops[index]));
189    }
190
191    /// Where the anchor has gone. A row that left the list leaves the offset alone.
192    fn anchored(&mut self) -> Option<f32> {
193        let (key, index, delta) = self.anchor?;
194        let index = if self.keys.get(index) == Some(&key) {
195            index
196        } else {
197            self.keys.iter().position(|k| *k == key)?
198        };
199        self.anchor = Some((key, index, delta));
200        Some(self.tops[index] + delta)
201    }
202}
203
204/// A list of any length that builds only the rows that show, with rows of any height. They
205/// are laid out and measured as they appear, and what was measured is kept by key, so going
206/// back finds them at the right size. It can jump to a row, stay at the end as a chat does,
207/// and keep what is on screen still when rows come in above it. With nothing to select, the
208/// arrows, Page Up, Page Down, Home and End scroll; with a [`selection`](Self::selection) or
209/// [`on_activate`](Self::on_activate), they move between rows, and Enter or Space activates.
210///
211/// Rows of the same height, told with [`row_height`](Self::row_height), need no measuring.
212///
213/// ```
214/// use ravenrs_reactive::State;
215/// use ravenrs_widgets::{Text, Ui, VirtualList};
216///
217/// let chosen = State::new(None::<usize>);
218/// let list = VirtualList::new(10_000, |index| Text::new(format!("Row {index}")))
219///     .key(|index| index as u64)
220///     .selection(chosen.binding())
221///     .height(320.0);
222/// let mut ui = Ui::new(list, ravenrs_style::Theme::raven_dark());
223/// ui.update();
224/// ```
225#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
226pub struct VirtualList {
227    pub(crate) count: usize,
228    pub(crate) item: Rc<dyn Fn(usize) -> AnyView>,
229    pub(crate) key: Option<Rc<dyn Fn(usize) -> u64>>,
230    pub(crate) sizing: Sizing,
231    pub(crate) spacing: f32,
232    follow_end: bool,
233    scroll_to: Option<Binding<Option<usize>>>,
234    selection: Option<Binding<Option<usize>>>,
235    on_activate: Option<Rc<dyn Fn(usize)>>,
236    on_visible: Option<Rc<dyn Fn(Range<usize>)>>,
237    row_name: Option<Rc<dyn Fn(usize) -> String>>,
238    label: Option<String>,
239    length: Option<f32>,
240    horizontal: bool,
241    /// Rows, focus and semantics are of the view around it.
242    bare: bool,
243    command: Option<State<Option<ScrollCommand>>>,
244    probe: Option<Rc<Cell<ListProbe>>>,
245    /// Scrolls across as well, up to this width of content.
246    cross_content: Option<f32>,
247    row_align: Align,
248    padding: f32,
249}
250
251impl fmt::Debug for VirtualList {
252    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253        f.debug_struct("VirtualList").field("count", &self.count).finish_non_exhaustive()
254    }
255}
256
257#[cfg_attr(feature = "live", ravenrs_live::expose)]
258impl VirtualList {
259    /// A list of `count` rows, with `item` making the view of each one when it shows.
260    pub fn new<V: View + 'static>(count: usize, item: impl Fn(usize) -> V + 'static) -> Self {
261        Self {
262            count,
263            item: Rc::new(move |index| Box::new(item(index))),
264            key: None,
265            sizing: Sizing::Measured(40.0),
266            spacing: 0.0,
267            follow_end: false,
268            scroll_to: None,
269            selection: None,
270            on_activate: None,
271            on_visible: None,
272            row_name: None,
273            label: None,
274            length: None,
275            horizontal: false,
276            bare: false,
277            command: None,
278            probe: None,
279            cross_content: None,
280            row_align: Align::Stretch,
281            padding: 0.0,
282        }
283    }
284
285    /// What identifies a row, which is its position by default. With it, a row keeps its
286    /// measured height and what is on screen stays still when rows come in above. It is
287    /// asked for every row on each build, so it should only read a value.
288    pub fn key(mut self, key: impl Fn(usize) -> u64 + 'static) -> Self {
289        self.key = Some(Rc::new(key));
290        self
291    }
292
293    /// How tall a row probably is until it has been measured (40 by default).
294    pub fn estimated_height(mut self, height: f32) -> Self {
295        self.sizing = Sizing::Measured(height.max(1.0));
296        self
297    }
298
299    /// Every row has this height, and nothing has to be measured.
300    pub fn row_height(mut self, height: f32) -> Self {
301        self.sizing = Sizing::Fixed(height.max(1.0));
302        self
303    }
304
305    /// The height of each row is known, as with the pages of a document. It is asked for every
306    /// row on each build.
307    pub fn row_heights(mut self, height: impl Fn(usize) -> f32 + 'static) -> Self {
308        self.sizing = Sizing::Known(Rc::new(height));
309        self
310    }
311
312    /// Gap between rows, in logical pixels.
313    pub fn spacing(mut self, spacing: f32) -> Self {
314        self.spacing = spacing.max(0.0);
315        self
316    }
317
318    /// Starts at the end and stays there as rows arrive, as a chat does, until the user
319    /// scrolls away; coming back to the end takes it up again.
320    pub fn follow_end(mut self, follow: bool) -> Self {
321        self.follow_end = follow;
322        self
323    }
324
325    /// Set it to a row to bring that row to the start of the view. It goes back to `None`.
326    pub fn scroll_to(mut self, index: Binding<Option<usize>>) -> Self {
327        self.scroll_to = Some(index);
328        self
329    }
330
331    /// Makes the rows selectable, one at a time, with the pointer and the arrows.
332    pub fn selection(mut self, selected: Binding<Option<usize>>) -> Self {
333        self.selection = Some(selected);
334        self
335    }
336
337    /// Called with a row on a double click, or on Enter or Space.
338    pub fn on_activate(mut self, handler: impl Fn(usize) + 'static) -> Self {
339        self.on_activate = Some(Rc::new(handler));
340        self
341    }
342
343    /// Called with the rows that show whenever they change, as for loading what comes next.
344    pub fn on_visible(mut self, handler: impl Fn(Range<usize>) + 'static) -> Self {
345        self.on_visible = Some(Rc::new(handler));
346        self
347    }
348
349    /// What screen readers call each row, for rows whose content does not say it.
350    pub fn row_name(mut self, name: impl Fn(usize) -> String + 'static) -> Self {
351        self.row_name = Some(Rc::new(name));
352        self
353    }
354
355    /// What screen readers call the list, instead of the name in [`Labels`](crate::Labels).
356    pub fn label(mut self, label: impl Into<String>) -> Self {
357        self.label = Some(label.into());
358        self
359    }
360
361    /// A fixed height. It takes the height its parent gives it by default.
362    pub fn height(mut self, height: f32) -> Self {
363        self.length = Some(height);
364        self
365    }
366
367    /// Runs from left to right; the sizes above are then widths, and the height is across.
368    pub fn horizontal(mut self) -> Self {
369        self.horizontal = true;
370        self
371    }
372
373    pub(crate) fn bare(mut self) -> Self {
374        self.bare = true;
375        self
376    }
377
378    pub(crate) fn commands(mut self, command: State<Option<ScrollCommand>>) -> Self {
379        self.command = Some(command);
380        self
381    }
382
383    pub(crate) fn probe(mut self, probe: Rc<Cell<ListProbe>>) -> Self {
384        self.probe = Some(probe);
385        self
386    }
387
388    pub(crate) fn scroll_across(mut self, content: f32) -> Self {
389        self.cross_content = Some(content);
390        self
391    }
392
393    pub(crate) fn row_align(mut self, align: Align) -> Self {
394        self.row_align = align;
395        self
396    }
397
398    /// Room before the first row and after the last, in logical pixels.
399    pub(crate) fn padding(mut self, padding: f32) -> Self {
400        self.padding = padding.max(0.0);
401        self
402    }
403
404    fn default_size(&self) -> f32 {
405        match &self.sizing {
406            Sizing::Measured(estimate) => estimate + self.spacing,
407            Sizing::Fixed(size) => size + self.spacing,
408            Sizing::Known(_) => self.spacing,
409        }
410    }
411
412    fn along(&self, size: Size) -> f32 {
413        if self.horizontal { size.width } else { size.height }
414    }
415
416    fn axis_point(&self, along: f32, across: f32) -> Point {
417        if self.horizontal { Point::new(along, across) } else { Point::new(across, along) }
418    }
419
420    fn interactive(&self) -> bool {
421        self.selection.is_some() || self.on_activate.is_some()
422    }
423
424    fn spacer(&self, cx: &mut BuildCx<'_>, length: f32) -> NodeId {
425        let style = if self.horizontal {
426            LayoutStyle { width: Some(length), ..LayoutStyle::leaf() }
427        } else {
428            LayoutStyle { height: Some(length), ..LayoutStyle::leaf() }
429        };
430        cx.container(style, &[], Paint::None)
431    }
432
433    fn apply(&self, track: &mut Track, command: ScrollCommand, offset: f32, view: f32) -> f32 {
434        let index = |i: usize| i.min(self.count.saturating_sub(1));
435        let target = match command {
436            ScrollCommand::ToIndex(i) => {
437                let i = index(i);
438                let (key, top) = (track.keys.get(i).copied(), track.tops.get(i).copied().unwrap_or(0.0));
439                track.at_end = false;
440                let target = (top - self.padding).clamp(0.0, track.max_offset(view));
441                track.anchor = key.map(|key| (key, i, target - top));
442                return target;
443            }
444            ScrollCommand::Reveal(i) => {
445                let i = index(i);
446                let (Some(top), Some(bottom)) = (track.tops.get(i), track.tops.get(i + 1)) else {
447                    return offset;
448                };
449                let bottom = bottom - self.spacing + self.padding;
450                if *top < offset {
451                    top - self.padding
452                } else if bottom > offset + view {
453                    (bottom - view).min(*top)
454                } else {
455                    offset
456                }
457            }
458            ScrollCommand::By(by) => offset + by,
459        };
460        let target = target.clamp(0.0, track.max_offset(view));
461        track.set_anchor(target);
462        track.at_end = self.follow_end && target >= track.max_offset(view) - 1.0;
463        target
464    }
465}
466
467/// A row of a [`VirtualList`]: what measures it and what makes it a row of a list.
468struct Row<'a> {
469    list: &'a VirtualList,
470    index: usize,
471    size: f32,
472    selected: bool,
473    active: bool,
474    pick: Option<Rc<dyn Fn(usize)>>,
475}
476
477impl View for Row<'_> {
478    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
479        let list = self.list;
480        let theme = *cx.theme();
481        let item = (list.item)(self.index);
482        let child = cx.child(&*item);
483        let fixed = !matches!(list.sizing, Sizing::Measured(_));
484        let gap = ravenrs_core::Insets {
485            bottom: if list.horizontal { 0.0 } else { list.spacing },
486            right: if list.horizontal { list.spacing } else { 0.0 },
487            ..ravenrs_core::Insets::default()
488        };
489        let accent = theme.color(Semantic::Accent);
490        let paint = if self.selected || self.active {
491            let fill = if self.selected { accent.with_alpha(0.14) } else { Color::TRANSPARENT };
492            let border = self.active.then_some(Border { width: 1.5, color: accent });
493            Paint::Quad(QuadPaint { corner_radius: theme.radii.sm, border, ..QuadPaint::new(fill) })
494        } else {
495            Paint::None
496        };
497        let node = cx.container(
498            LayoutStyle {
499                width: (fixed && list.horizontal).then_some(self.size),
500                height: (fixed && !list.horizontal).then_some(self.size),
501                min_width: Some(0.0),
502                padding: gap,
503                overflow_hidden: fixed,
504                align: list.row_align,
505                align_self: Some(Align::Stretch),
506                ..LayoutStyle::stack(Axis::Vertical)
507            },
508            &[child],
509            paint,
510        );
511        if !fixed {
512            cx.report_frame(node);
513        }
514        if list.bare {
515            return node;
516        }
517        let mut semantics = Semantics::new(Role::ListItem).selected(self.selected).active(self.active);
518        if let Some(name) = &list.row_name {
519            semantics = semantics.name(name(self.index));
520        }
521        cx.semantics(node, semantics);
522        if let Some(pick) = &self.pick {
523            let (pick, index) = (pick.clone(), self.index);
524            cx.on_click(node, Rc::new(move || pick(index)));
525            cx.cursor(node, CursorIcon::Pointer);
526        }
527        if let Some(open) = &list.on_activate {
528            let (open, index) = (open.clone(), self.index);
529            cx.on_double_click(node, Rc::new(move || open(index)));
530        }
531        node
532    }
533}
534
535impl View for VirtualList {
536    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
537        let track = cx.use_state(|| Rc::new(RefCell::new(Track::default()))).peek().clone();
538        let local: State<Option<ScrollCommand>> = cx.use_state(|| None);
539        let commands = self.command.clone().unwrap_or_else(|| local.clone());
540        let cursor: State<usize> = cx.use_state(|| 0);
541        let selected = self.selection.as_ref().and_then(Binding::get);
542
543        let raw = cx.scroll_offset();
544        let frame = cx.scroll_viewport();
545        let window = cx.viewport();
546        let view = frame.map_or(self.length.unwrap_or(self.along(window)), |size| self.along(size));
547        let cross = frame.map_or(if self.horizontal { window.height } else { window.width }, |size| {
548            if self.horizontal { size.height } else { size.width }
549        });
550        let mut offset = if self.horizontal { raw.x } else { raw.y };
551        let across = if self.horizontal { raw.y } else { raw.x };
552
553        let mut t = track.borrow_mut();
554        let first_build = t.left_at.is_none();
555        if first_build {
556            t.at_end = self.follow_end;
557        } else if t.left_at.is_some_and(|left| (left - offset).abs() > SAME_OFFSET) {
558            // Somebody scrolled: this is the new place to hold, over the sizes it was seen with.
559            t.set_anchor(offset);
560            t.at_end = self.follow_end && offset >= t.max_offset(view) - 1.0;
561            t.revealing = None;
562        }
563        t.sync(self);
564        t.take_measurements(cx, self);
565
566        let mut desired = t.anchored().unwrap_or(offset);
567        if t.at_end {
568            desired = t.max_offset(view);
569        }
570        desired = desired.clamp(0.0, t.max_offset(view));
571
572        // Each order is read once. Setting them back during a build only asks for another,
573        // which a jump needs anyway.
574        if let Some(order) = commands.get() {
575            commands.set(None);
576            t.revealing = match order {
577                ScrollCommand::Reveal(index) => Some(index),
578                _ => None,
579            };
580            desired = self.apply(&mut t, order, desired, view);
581        }
582        if let Some(asked) = &self.scroll_to
583            && let Some(index) = asked.get()
584        {
585            asked.set(None);
586            t.revealing = None;
587            desired = self.apply(&mut t, ScrollCommand::ToIndex(index), desired, view);
588        }
589        if let Some(index) = t.revealing {
590            desired = self.apply(&mut t, ScrollCommand::Reveal(index), desired, view);
591        }
592        if (desired - offset).abs() > SAME_OFFSET {
593            cx.set_scroll_offset(self.axis_point(desired, across));
594            offset = desired;
595        }
596        t.left_at = Some(offset);
597        if t.anchor.is_none() {
598            t.set_anchor(offset);
599        }
600
601        let count = self.count;
602        let first_seen = t.index_at(offset);
603        let end_seen = t.tops.partition_point(|top| *top < offset + view).max(first_seen + 1).min(count);
604        let current = if offset >= t.max_offset(view) - 1.0 && t.max_offset(view) > 0.0 {
605            count.saturating_sub(1)
606        } else if offset <= 0.0 {
607            0
608        } else {
609            t.index_at(offset + view / 2.0)
610        };
611        if let Some(probe) = &self.probe {
612            probe.set(ListProbe { offset, view, total: t.total(), current });
613        }
614        let seen = first_seen.min(end_seen)..end_seen;
615        if t.visible.as_ref() != Some(&seen) {
616            t.visible = Some(seen.clone());
617            if let Some(handler) = &self.on_visible {
618                handler(seen);
619            }
620        }
621        let first = first_seen.saturating_sub(OVERSCAN);
622        let last = (end_seen + OVERSCAN).min(count);
623        let before = t.tops.get(first).map_or(0.0, |top| top - self.padding);
624        let after = t.tops.last().copied().unwrap_or(0.0) - t.tops.get(last).copied().unwrap_or(0.0);
625        let keys: Vec<u64> = t.keys.get(first..last).unwrap_or(&[]).to_vec();
626        let sizes: Vec<f32> = t.sizes.get(first..last).unwrap_or(&[]).to_vec();
627        t.built.clear();
628        t.built.extend(keys.iter().enumerate().map(|(at, key)| (*key, first + at)));
629        // Nothing below reads the track, and rows may build lists of their own.
630        let unmeasured = matches!(self.sizing, Sizing::Measured(_))
631            && keys.iter().any(|key| !t.measured.contains_key(key));
632        if !unmeasured {
633            t.revealing = None;
634        }
635        drop(t);
636        if unmeasured {
637            // Rows that appear in the last build of a frame are only known in the next one.
638            cx.wake_at(cx.now());
639        }
640
641        let active = cursor.get().min(count.saturating_sub(1));
642        let pick: Option<Rc<dyn Fn(usize)>> = self.selection.clone().map(|selection| {
643            let cursor = cursor.clone();
644            Rc::new(move |index: usize| {
645                cursor.set(index);
646                selection.set(Some(index));
647            }) as Rc<dyn Fn(usize)>
648        });
649        let pick = pick.or_else(|| {
650            self.on_activate.as_ref().map(|_| {
651                let cursor = cursor.clone();
652                Rc::new(move |index: usize| cursor.set(index)) as Rc<dyn Fn(usize)>
653            })
654        });
655
656        let mut nodes = Vec::with_capacity(keys.len() + 2);
657        nodes.push(self.spacer(cx, before));
658        let focused = cx.is_focus_visible();
659        for (at, (key, size)) in keys.iter().zip(&sizes).enumerate() {
660            let index = first + at;
661            let row = Row {
662                list: self,
663                index,
664                size: *size,
665                selected: !self.bare && selected == Some(index),
666                active: !self.bare && self.interactive() && focused && index == active,
667                pick: pick.clone(),
668            };
669            nodes.push(cx.keyed_child(*key, &row));
670        }
671        nodes.push(self.spacer(cx, after));
672        let padding = if self.horizontal {
673            ravenrs_core::Insets::symmetric(0.0, self.padding)
674        } else {
675            ravenrs_core::Insets::symmetric(self.padding, 0.0)
676        };
677        let content = cx.container(
678            LayoutStyle {
679                align: Align::Stretch,
680                padding,
681                // Across a strip, the rows take the whole height it has.
682                grow: if self.horizontal { 1.0 } else { 0.0 },
683                min_width: self.cross_content.map(|content| content.max(cross)),
684                ..LayoutStyle::stack(if self.horizontal { Axis::Horizontal } else { Axis::Vertical })
685            },
686            &nodes,
687            Paint::None,
688        );
689        let body = cx.container(
690            LayoutStyle {
691                height: self.length,
692                grow: if self.length.is_some() { 0.0 } else { 1.0 },
693                min_height: Some(0.0),
694                min_width: Some(0.0),
695                overflow_hidden: true,
696                align: Align::Stretch,
697                align_self: Some(Align::Stretch),
698                ..LayoutStyle::stack(Axis::Vertical)
699            },
700            &[content],
701            Paint::None,
702        );
703        cx.scrollable_virtual(body, self.horizontal || self.cross_content.is_some());
704        if self.horizontal {
705            cx.wheel_scrolls_sideways(body);
706        }
707        if self.bare {
708            return body;
709        }
710
711        let name = self.label.clone().unwrap_or_else(|| cx.labels().list_name.clone());
712        cx.semantics(body, Semantics::new(Role::List).name(name).description(count.to_string()));
713        cx.focusable(body, cx.theme().radii.sm);
714        let (interactive, selection, open) =
715            (self.interactive(), self.selection.clone(), self.on_activate.clone());
716        let horizontal = self.horizontal;
717        let page = (end_seen.saturating_sub(first_seen)).max(2) - 1;
718        cx.on_key(
719            body,
720            Rc::new(move |key, _| {
721                if count == 0 {
722                    return false;
723                }
724                let (back, forward) = if horizontal {
725                    (Key::ArrowLeft, Key::ArrowRight)
726                } else {
727                    (Key::ArrowUp, Key::ArrowDown)
728                };
729                if !interactive {
730                    let order = match key {
731                        k if *k == back => ScrollCommand::By(-LINE_STEP),
732                        k if *k == forward => ScrollCommand::By(LINE_STEP),
733                        Key::PageUp => ScrollCommand::By(-view * 0.9),
734                        Key::PageDown => ScrollCommand::By(view * 0.9),
735                        Key::Home => ScrollCommand::ToIndex(0),
736                        Key::End => ScrollCommand::Reveal(count - 1),
737                        _ => return false,
738                    };
739                    commands.set(Some(order));
740                    return true;
741                }
742                let to = match key {
743                    k if *k == back => active.saturating_sub(1),
744                    k if *k == forward => (active + 1).min(count - 1),
745                    Key::PageUp => active.saturating_sub(page),
746                    Key::PageDown => (active + page).min(count - 1),
747                    Key::Home => 0,
748                    Key::End => count - 1,
749                    Key::Enter | Key::Space => {
750                        if let Some(open) = &open {
751                            open(active);
752                        }
753                        if let Some(selection) = &selection {
754                            selection.set(Some(active));
755                        }
756                        return true;
757                    }
758                    _ => return false,
759                };
760                cursor.set(to);
761                if let Some(selection) = &selection {
762                    selection.set(Some(to));
763                }
764                commands.set(Some(ScrollCommand::Reveal(to)));
765                true
766            }),
767        );
768        body
769    }
770}