Skip to main content

frust_widgets/slivers/
list.rs

1//! `sliver_list`: a lazy, keyed list of rows that size themselves — Flutter's
2//! `SliverList` (`rendering/sliver_list.dart`'s `RenderSliverList`, over
3//! `sliver_multi_box_adaptor.dart`), materialised the way
4//! [`sliver_fixed_extent_list`](crate::sliver_fixed_extent_list) materialises
5//! its rows.
6//!
7//! # Rows, identity and extents
8//!
9//! Every row is keyed: `key_of(index)` is its identity, with
10//! [`ListView::builder_keyed`](crate::ListView::builder_keyed)'s contract (a
11//! duplicate key in one window trips a `debug_assert!`; release keeps the
12//! first claim). A row is laid out tight across the axis and unbounded along
13//! it, and its extent along the axis is cached under its key — `ListView`'s
14//! *Variable extents* bookkeeping, shared through `slivers/reconcile.rs`: a row
15//! never laid out is assumed [`SliverListView::estimated_item_extent`] long
16//! (48 px unless set), so the list's scroll extent is
17//! `Σ measured + estimate × unmeasured` and converges on the true total as rows
18//! are visited (never less than the materialised rows' own end). Once the
19//! last row is in the window the scroll extent is that row's laid-out end
20//! exactly. A row that leaves the data drops its
21//! measurement (the same departing-row and shrink rules `ListView` runs).
22//!
23//! # Materialised at rebuild time, positioned from an anchor
24//!
25//! Rows are built only in [`View::rebuild`], over the cache region the last
26//! sliver layout asked for, with the fixed-extent list's one-frame catch-up
27//! (paint requests a frame whenever the rows do not cover it). The window is
28//! planned from an *anchor*: one row and the content offset it starts at.
29//! Layout records it as the row the paint region's leading edge sits in;
30//! rebuild walks from it to the leading edge over measured-or-estimated
31//! extents (a bounded walk; a jump resolves in closed form) and widens to the
32//! cache region. Layout places that row at its recorded offset and every other
33//! row from it, both ways, by the rows' own laid-out extents — so a row
34//! measuring differently than it was assumed moves the rows on its far side of
35//! the anchor, never the row the user is looking at.
36//!
37//! # Corrections through the viewport
38//!
39//! What the anchor cannot absorb is reported as a
40//! [`scroll_offset_correction`](SliverGeometry::scroll_offset_correction),
41//! which the [`CustomScrollView`](crate::CustomScrollView) applies to its
42//! scroll position before laying every sliver out again — the content moves
43//! by the correction and so does the position, so nothing on screen moves:
44//!
45//! * **Keyed anchoring.** A rebuild whose data moved the anchor row — rows
46//!   prepended or removed above it — finds the anchor's key again, moves it to
47//!   its new index and shifts its offset by the rows it moved times the
48//!   estimate (a prepended row is by definition unmeasured), and the next
49//!   layout reports that shift. The key is looked up at its unchanged index,
50//!   at its index shifted by the frame's net item-count change, and then by a
51//!   bounded scan around them (`reconcile::locate_key`), so a mutation on both
52//!   sides of the anchor in one frame (a prepend above plus an append below)
53//!   is anchored as exactly as a one-sided one. An anchor row that left the
54//!   data, or moved beyond the scan, anchors nothing: the content after the
55//!   mutation simply moves. Anchoring applies while the list's start is at or
56//!   above the viewport's leading edge (scrolled into, or flush with the top);
57//!   a list starting further down the viewport lets a prepend push it down.
58//! * **The leading edge.** Positions derived from an anchor can disagree with
59//!   the list's start once rows above it are measured: row 0 must start at
60//!   offset 0, and rows above the window need room. A layout finding row 0
61//!   elsewhere, or a later first row at or before offset 0, shifts every row
62//!   to fix it (row 0 to 0; any other first row to the room the extent model
63//!   gives the rows before it) and reports the shift — while the list starts
64//!   at or above the leading edge and the shifted position stays at or past
65//!   the list's start. Otherwise there is nothing on screen positioned from
66//!   the list's inside to hold still, and the shift is silent: a gap above
67//!   row 0 wider than the scroll position (rows above measured shorter than
68//!   estimated) closes with a visible shift, as Flutter's does.
69//!
70//! [`SliverListWidget::scroll_corrections`] counts the corrections reported.
71//!
72//! # Keep-alive
73//!
74//! Each row is built inside a keep-alive scope ([`keep_alive`](crate::keep_alive)'s
75//! module docs): a row asking to be kept that leaves the window while its key
76//! is still in the data is parked instead of torn down — not laid out,
77//! painted, routed to or visited — and re-adopted when its key re-enters the
78//! window. Parked rows are checked against the data every rebuild (released
79//! when their key is gone) and torn down with the list.
80//!
81//! # Near edges and semantics
82//!
83//! [`SliverListView::on_near_start`] / [`SliverListView::on_near_end`] are the
84//! fixed-extent list's edge-triggered pair, observed at layout. An item-count
85//! change rearms both — except that a change the keyed anchoring absorbed by
86//! shifting the position down (rows loaded above the anchor, typically in
87//! answer to `on_near_start` itself) leaves the start edge as it was, so the
88//! answer cannot immediately refire it. Semantics is a [`Role::List`] node
89//! carrying the item count as its set size, over the materialised rows.
90
91use std::collections::HashMap;
92use std::ops::Range;
93use std::rc::Rc;
94
95use frust_core::accesskit::Role;
96use frust_core::{
97    AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
98    LayoutCtx, PaintCtx, PaintScene, ScrollAxis, ScrollDirection, SemanticsCtx, SliverConstraints,
99    SliverGeometry, SliverWidget, View, Widget, any,
100};
101use kurbo::Size;
102
103use super::adapter::{axis_point, finite_extent, non_negative};
104use super::keep_alive::{RowScope, row_wants_keep_alive};
105use super::reconcile::{self, ItemViews, KeptRow, LiveRows, Measured, Retention};
106use super::{NearEdge, cache_offset, paint_offset};
107use crate::ChildKey;
108
109/// The debug-only tripwire message for two materialised rows claiming one key.
110const DUPLICATE_KEY_MSG: &str = "sliver_list produced a duplicate key inside one window: row \
111     identity is ambiguous (release: the first slot claiming a key keeps the live row, later \
112     duplicates build fresh)";
113
114/// The extent a row is assumed to have before it is first laid out, unless
115/// [`SliverListView::estimated_item_extent`] sets one: the 48 px minimum
116/// touch-target height, the smallest a tappable row is laid out at.
117const DEFAULT_ESTIMATED_ITEM_EXTENT: f64 = 48.0;
118
119/// A leading-edge mismatch at or below this many px is float noise from
120/// summing extents both ways from the anchor: fixed silently, never reported.
121const DRIFT_TOLERANCE: f64 = 1e-6;
122
123/// A view-held item builder, erased per row (inside its keep-alive scope).
124type ItemBuilder<State> = Rc<dyn Fn(usize) -> AnyView<State>>;
125
126/// A view-held stable-key function (`item index -> row identity`).
127type KeyOf = Rc<dyn Fn(usize) -> ChildKey>;
128
129/// A view-held near-edge callback (erased on build).
130type OnNearEdge<State> = Rc<dyn Fn(&mut State)>;
131
132/// A lazy, keyed list of rows that size themselves. See the
133/// [module docs](self).
134pub struct SliverListView<State: 'static> {
135    item_count: usize,
136    estimate: f64,
137    builder: ItemBuilder<State>,
138    key_of: KeyOf,
139    on_near_start: Option<OnNearEdge<State>>,
140    near_start_threshold: f64,
141    on_near_end: Option<OnNearEdge<State>>,
142    near_end_threshold: f64,
143}
144
145/// A lazy list of `item_count` rows, the row at `index` being `builder(index)`
146/// keyed by `key_of(index)`, each as long along the scroll axis as it lays
147/// itself out. See the [module docs](self).
148///
149/// ```
150/// use frust_widgets::{ChildKey, CustomScrollView, custom_scroll_view, sliver_list, text};
151/// let ids: Vec<u64> = (0..5_000).collect();
152/// let keys = ids.clone();
153/// let view: CustomScrollView<()> = custom_scroll_view(
154///     sliver_list(
155///         ids.len(),
156///         move |i| ChildKey::new(keys[i]),
157///         move |i| text(format!("message {}", ids[i])),
158///     )
159///     .estimated_item_extent(64.0)
160///     .on_near_start(|_: &mut ()| {}, 400.0),
161/// );
162/// # let _ = view;
163/// ```
164pub fn sliver_list<State: 'static, V: View<State>>(
165    item_count: usize,
166    key_of: impl Fn(usize) -> ChildKey + 'static,
167    builder: impl Fn(usize) -> V + 'static,
168) -> SliverListView<State> {
169    SliverListView {
170        item_count,
171        estimate: DEFAULT_ESTIMATED_ITEM_EXTENT,
172        builder: Rc::new(move |index| any(RowScope::new(builder(index)))),
173        key_of: Rc::new(key_of),
174        on_near_start: None,
175        near_start_threshold: 0.0,
176        on_near_end: None,
177        near_end_threshold: 0.0,
178    }
179}
180
181impl<State: 'static> SliverListView<State> {
182    /// Assume a row never laid out is `estimate_px` long along the scroll
183    /// axis (48 px unless set) — what the scroll extent counts for every
184    /// unmeasured row and what keyed anchoring shifts by per prepended row.
185    ///
186    /// # Panics
187    ///
188    /// If `estimate_px` is not positive and finite.
189    pub fn estimated_item_extent(mut self, estimate_px: f64) -> Self {
190        assert!(
191            estimate_px > 0.0 && estimate_px.is_finite(),
192            "sliver_list estimated_item_extent must be positive and finite"
193        );
194        self.estimate = estimate_px;
195        self
196    }
197
198    /// Fire `callback` when the list's start comes within `threshold_px` of
199    /// the viewport's leading edge — the "load older" edge. See the
200    /// [module docs](self)' *Near edges and semantics*.
201    pub fn on_near_start<F: Fn(&mut State) + 'static>(
202        mut self,
203        callback: F,
204        threshold_px: f64,
205    ) -> Self {
206        self.on_near_start = Some(Rc::new(callback));
207        self.near_start_threshold = threshold_px;
208        self
209    }
210
211    /// Fire `callback` when the list's end comes within `threshold_px` of the
212    /// viewport's trailing edge — the "load newer" edge, mirroring
213    /// [`SliverListView::on_near_start`].
214    pub fn on_near_end<F: Fn(&mut State) + 'static>(
215        mut self,
216        callback: F,
217        threshold_px: f64,
218    ) -> Self {
219        self.on_near_end = Some(Rc::new(callback));
220        self.near_end_threshold = threshold_px;
221        self
222    }
223
224    /// Reinstall the near-edge callbacks (closures are not comparable, so
225    /// every build and rebuild does).
226    fn install_edges(&self, widget: &mut SliverListWidget) {
227        widget.near_start.callback = self
228            .on_near_start
229            .as_ref()
230            .map(crate::authoring::erase_callback);
231        widget.near_start.threshold = self.near_start_threshold;
232        widget.near_end.callback = self
233            .on_near_end
234            .as_ref()
235            .map(crate::authoring::erase_callback);
236        widget.near_end.threshold = self.near_end_threshold;
237    }
238}
239
240/// A content position rows are placed from: item `index` starts at content
241/// offset `y`, in the list's own scroll terms. `key` is the row's identity
242/// when the position is a materialised row's, `None` when it was only walked
243/// to over estimates.
244#[derive(Clone, Copy, Debug, PartialEq)]
245struct Anchor {
246    key: Option<ChildKey>,
247    index: usize,
248    y: f64,
249}
250
251impl Anchor {
252    const START: Self = Self {
253        key: None,
254        index: 0,
255        y: 0.0,
256    };
257}
258
259/// The rows one rebuild should materialise and the position they are placed
260/// from.
261struct Plan {
262    range: Range<usize>,
263    /// The row the leading edge falls in, and its content offset.
264    reference: (usize, f64),
265}
266
267/// The retained widget for a [`SliverListView`]: the materialised rows and
268/// their identities, the parked keep-alive rows, the measured extents, the
269/// anchor, and the near-edge state.
270pub struct SliverListWidget {
271    /// The materialised rows, parallel to [`Self::indices`].
272    children: Vec<ChildPod>,
273    /// The item index each row renders — a contiguous ascending run.
274    indices: Vec<usize>,
275    /// Each row's key, parallel to [`Self::indices`].
276    slot_keys: Vec<ChildKey>,
277    /// `key -> the item index that row occupied` as of the last rebuild.
278    key_index: HashMap<ChildKey, usize>,
279    /// The rows kept alive outside the window, by key.
280    kept: HashMap<ChildKey, KeptRow>,
281    /// The view's key function, retained so layout and paint can look a
282    /// row's measurement up without the view in scope.
283    key_of: Option<KeyOf>,
284    /// Every row laid out so far, by key.
285    measured: HashMap<ChildKey, Measured>,
286    /// The running Σ of [`Self::measured`]'s extents.
287    measured_sum: f64,
288    estimate: f64,
289    item_count: usize,
290    /// The position rows are placed from (the module docs' *Materialised at
291    /// rebuild time, positioned from an anchor*).
292    anchor: Anchor,
293    /// Whether the last layout found the list's start at or above the
294    /// viewport's leading edge — where keyed anchoring applies.
295    anchoring: bool,
296    /// A keyed-anchoring shift rebuild owes the scroll position, reported by
297    /// the next layout.
298    pending_correction: f64,
299    /// How many scroll-offset corrections layout has reported.
300    corrections: u64,
301    /// The constraints the last sliver layout ran with — what rebuild windows
302    /// against. `None` until the first layout.
303    constraints: Option<SliverConstraints>,
304    /// The paint extent of the last sliver layout.
305    paint_extent: f64,
306    /// Whether the last layout cut a row at the paint extent (so paint clips).
307    clip: bool,
308    /// Where the near edges were last observed — `(scroll_offset,
309    /// remaining_paint_extent)`; `None` before the first layout.
310    observed_at: Option<(f64, f64)>,
311    near_start: NearEdge,
312    near_end: NearEdge,
313}
314
315impl SliverListWidget {
316    fn new(item_count: usize, estimate: f64, key_of: KeyOf) -> Self {
317        Self {
318            children: Vec::new(),
319            indices: Vec::new(),
320            slot_keys: Vec::new(),
321            key_index: HashMap::new(),
322            kept: HashMap::new(),
323            key_of: Some(key_of),
324            measured: HashMap::new(),
325            measured_sum: 0.0,
326            estimate,
327            item_count,
328            anchor: Anchor::START,
329            anchoring: false,
330            pending_correction: 0.0,
331            corrections: 0,
332            constraints: None,
333            paint_extent: 0.0,
334            clip: false,
335            observed_at: None,
336            near_start: NearEdge::new(),
337            near_end: NearEdge::new(),
338        }
339    }
340
341    /// The item indices currently materialised, ascending.
342    pub fn window(&self) -> &[usize] {
343        &self.indices
344    }
345
346    /// How many items the list holds.
347    pub fn item_count(&self) -> usize {
348        self.item_count
349    }
350
351    /// How many rows have a cached measured extent.
352    pub fn measured_count(&self) -> usize {
353        self.measured.len()
354    }
355
356    /// How many rows are parked, kept alive outside the window.
357    pub fn kept_alive_count(&self) -> usize {
358        self.kept.len()
359    }
360
361    /// How many scroll-offset corrections this list has reported to its
362    /// viewport (the module docs' *Corrections through the viewport*).
363    pub fn scroll_corrections(&self) -> u64 {
364        self.corrections
365    }
366
367    /// The extent item `index` contributes: its cached measurement, else the
368    /// estimate. One `key_of` call.
369    fn extent_at(&self, index: usize) -> f64 {
370        match self.key_of.as_ref() {
371            Some(key_of) if index < self.item_count => self
372                .measured
373                .get(&key_of(index))
374                .map_or(self.estimate, |measured| measured.extent),
375            _ => self.estimate,
376        }
377    }
378
379    /// The scroll extent the measurements alone imply.
380    fn estimated_total(&self) -> f64 {
381        reconcile::estimated_total(
382            self.measured_sum,
383            self.measured.len(),
384            self.item_count,
385            self.estimate,
386        )
387    }
388
389    /// The rows the last layout's cache region asks for, its leading edge
390    /// moved by `shift` (a correction not yet reported) — empty before the
391    /// first layout.
392    fn plan(&self, shift: f64) -> Plan {
393        let count = self.item_count;
394        let empty = |reference| Plan {
395            range: 0..0,
396            reference,
397        };
398        let Some(sc) = self.constraints.as_ref() else {
399            return empty((self.anchor.index, self.anchor.y));
400        };
401        if count == 0 {
402            return empty((0, 0.0));
403        }
404        let lead = sc.scroll_offset + shift;
405        let (w, y_w) = reconcile::walk_to_offset(
406            count,
407            self.anchor.index,
408            self.anchor.y,
409            lead,
410            self.estimate,
411            |index| self.extent_at(index),
412        );
413        let start_of_cache = lead + sc.cache_origin;
414        let end_of_cache = start_of_cache + sc.remaining_cache_extent;
415        let past_the_end = w + 1 == count && y_w + self.extent_at(w) <= start_of_cache;
416        if sc.remaining_cache_extent <= 0.0 || past_the_end {
417            return empty((w, y_w));
418        }
419        let (mut start, mut y_start) = (w, y_w);
420        while start > 0 && y_start > start_of_cache {
421            start -= 1;
422            y_start -= self.extent_at(start);
423        }
424        let (mut end, mut y_end) = (w, y_w);
425        loop {
426            y_end += self.extent_at(end);
427            end += 1;
428            if end >= count || y_end >= end_of_cache {
429                break;
430            }
431        }
432        Plan {
433            range: start..end,
434            reference: (w, y_w),
435        }
436    }
437
438    /// Whether the materialised rows cover `range`.
439    fn covers(&self, range: &Range<usize>) -> bool {
440        if range.is_empty() {
441            return true;
442        }
443        match (self.indices.first(), self.indices.last()) {
444            (Some(&first), Some(&last)) => first <= range.start && last + 1 >= range.end,
445            _ => false,
446        }
447    }
448
449    /// Observe both near edges at `sc` — only once the list has moved against
450    /// the viewport since the last observation. Owes the flush that delivers
451    /// a fire.
452    fn observe_edges(&mut self, sc: &SliverConstraints, total: f64) {
453        let at = (sc.scroll_offset, sc.remaining_paint_extent);
454        let previous = self.observed_at.replace(at);
455        if previous.is_none_or(|previous| previous == at) || self.item_count == 0 {
456            return;
457        }
458        let to_end = non_negative(total - sc.scroll_offset - sc.remaining_paint_extent);
459        let start = self.near_start.observe(sc.scroll_offset);
460        let end = self.near_end.observe(to_end);
461        if start || end {
462            frust_core::mark_pending_result_flush();
463        }
464    }
465
466    /// The content offset item `index` starts at by the extent model alone:
467    /// Σ measured-or-estimated extents of the items before it, walking at most
468    /// [`reconcile::MAX_PREFIX_STEP`] items and counting any further ones at
469    /// the estimate.
470    fn room_before(&self, index: usize) -> f64 {
471        let walked = index.min(reconcile::MAX_PREFIX_STEP);
472        let estimated = (index - walked) as f64 * self.estimate;
473        estimated
474            + (index - walked..index)
475                .map(|i| self.extent_at(i))
476                .sum::<f64>()
477    }
478
479    /// Each row's content offset, placed from the anchor both ways by the
480    /// rows' laid-out `extents`.
481    fn place(&self, extents: &[f64]) -> Vec<f64> {
482        let n = extents.len();
483        let mut ys = vec![0.0; n];
484        let Some(&first) = self.indices.first() else {
485            return ys;
486        };
487        let from = match self
488            .anchor
489            .index
490            .checked_sub(first)
491            .filter(|slot| *slot < n)
492        {
493            Some(slot) => {
494                ys[slot] = self.anchor.y;
495                slot
496            }
497            None => {
498                // An anchor outside the window (never left by rebuild): the
499                // first row goes where the estimate puts it relative to it.
500                let rows = first as f64 - self.anchor.index as f64;
501                ys[0] = self.anchor.y + rows * self.estimate;
502                0
503            }
504        };
505        for slot in from + 1..n {
506            ys[slot] = ys[slot - 1] + extents[slot - 1];
507        }
508        for slot in (0..from).rev() {
509            ys[slot] = ys[slot + 1] - extents[slot];
510        }
511        ys
512    }
513}
514
515/// Whether a sliver laid out under `sc` starts at or above the viewport's
516/// leading edge — scrolled into, or flush with it (nothing before it painted)
517/// — so the rows on screen are positioned from inside it.
518fn starts_at_or_above_leading_edge(sc: &SliverConstraints) -> bool {
519    sc.scroll_offset > 0.0 || sc.remaining_paint_extent >= sc.viewport_main_axis_extent
520}
521
522impl<State: 'static> View<State> for SliverListView<State> {
523    type Element = SliverListWidget;
524
525    /// No layout has run yet, so there is no cache region to window against:
526    /// nothing is materialised until the first rebuild after it.
527    fn build(&self, _ctx: &mut BuildCtx<'_>) -> SliverListWidget {
528        let mut widget =
529            SliverListWidget::new(self.item_count, self.estimate, Rc::clone(&self.key_of));
530        self.install_edges(&mut widget);
531        widget
532    }
533
534    fn rebuild(
535        &self,
536        prev: &Self,
537        element: &mut SliverListWidget,
538        ctx: &mut BuildCtx<'_>,
539    ) -> ChangeFlags {
540        self.install_edges(element);
541        element.key_of = Some(Rc::clone(&self.key_of));
542        let key_of: &dyn Fn(usize) -> ChildKey = &*self.key_of;
543        let mut flags = ChangeFlags::NONE;
544        let old_count = element.item_count;
545        let count = self.item_count;
546        let delta = count as isize - old_count as isize;
547        if old_count != count {
548            element.item_count = count;
549            flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
550        }
551        if element.estimate != self.estimate {
552            element.estimate = self.estimate;
553            flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
554        }
555
556        // Keyed anchoring: follow the anchor row to its index in this frame's
557        // data before the window is planned around it.
558        let anchor_before = element.anchor;
559        let mut anchor_missed = false;
560        let mut shifted_down = false;
561        if let Some(key) = element.anchor.key {
562            match reconcile::locate_key(key, element.anchor.index, delta, count, key_of) {
563                Some(index) if index != element.anchor.index => {
564                    let rows = index as f64 - element.anchor.index as f64;
565                    let shift = rows * element.estimate;
566                    element.anchor.index = index;
567                    element.anchor.y += shift;
568                    if element.anchoring {
569                        element.pending_correction += shift;
570                        shifted_down = shift > 0.0;
571                    }
572                }
573                Some(_) => {}
574                None => {
575                    anchor_missed = true;
576                    element.anchor.key = None;
577                    element.anchor.index = element.anchor.index.min(count.saturating_sub(1));
578                }
579            }
580        }
581
582        if delta != 0 {
583            // Content length changed: both edges may fire again — except the
584            // start edge when the change was absorbed above the anchor.
585            if !shifted_down {
586                element.near_start.armed = true;
587            }
588            element.near_end.armed = true;
589        }
590
591        // A parked row whose key left the data is released.
592        if !element.kept.is_empty() {
593            let mut gone = Vec::new();
594            for (key, row) in element.kept.iter_mut() {
595                match reconcile::locate_key(*key, row.index, delta, count, key_of) {
596                    Some(index) => row.index = index,
597                    None => gone.push(*key),
598                }
599            }
600            for key in gone {
601                if let Some(row) = element.kept.remove(&key) {
602                    row.teardown::<State>(ctx);
603                }
604                reconcile::forget_measurement(
605                    &mut element.measured,
606                    &mut element.measured_sum,
607                    &key,
608                );
609            }
610        }
611
612        if count < old_count {
613            reconcile::evict_measurements_past(
614                &mut element.measured,
615                &mut element.measured_sum,
616                count,
617            );
618        }
619
620        let plan = element.plan(element.pending_correction);
621        let locate = |key: ChildKey, prev_index: usize| {
622            reconcile::locate_key(key, prev_index, delta, count, key_of)
623        };
624        let outcome = reconcile::reconcile_keyed_retaining(
625            ItemViews {
626                prev: &*prev.builder,
627                next: &*self.builder,
628            },
629            LiveRows {
630                children: &mut element.children,
631                indices: &mut element.indices,
632            },
633            &mut element.key_index,
634            key_of,
635            plan.range.clone(),
636            DUPLICATE_KEY_MSG,
637            Some(Retention {
638                kept: &mut element.kept,
639                wants: &row_wants_keep_alive,
640                locate: &locate,
641            }),
642            ctx,
643        );
644        flags |= outcome.flags;
645
646        // Measured-cache hygiene, `ListView`'s rules: a frame where neither
647        // the anchor nor any previous row survived is a full replace;
648        // otherwise a departing row whose key is gone from the data drops its
649        // measurement.
650        if anchor_missed && !outcome.any_survivor {
651            element.measured.clear();
652            element.measured_sum = 0.0;
653        } else {
654            for (key, prev_index) in &outcome.departed {
655                if reconcile::probe_key(*key, *prev_index, delta, count, key_of).is_none() {
656                    reconcile::forget_measurement(
657                        &mut element.measured,
658                        &mut element.measured_sum,
659                        key,
660                    );
661                }
662            }
663        }
664        element.slot_keys = outcome.slot_keys;
665
666        // Rows are placed from the anchor while it is still materialised — it
667        // was laid out where it sits, so nothing positioned from it moves —
668        // else from the row the walk found the leading edge in.
669        let (index, y) = if plan.range.contains(&element.anchor.index) {
670            (element.anchor.index, element.anchor.y)
671        } else {
672            plan.reference
673        };
674        let key = index
675            .checked_sub(plan.range.start)
676            .and_then(|slot| element.slot_keys.get(slot).copied())
677            .filter(|_| plan.range.contains(&index));
678        element.anchor = Anchor { key, index, y };
679        if element.anchor != anchor_before || element.pending_correction != 0.0 {
680            flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
681        }
682        flags
683    }
684
685    fn teardown(&self, element: &mut SliverListWidget, ctx: &mut BuildCtx<'_>) {
686        for (index, pod) in element.indices.iter().zip(element.children.iter_mut()) {
687            crate::authoring::teardown_child(&(self.builder)(*index), pod, ctx);
688        }
689        for (_, row) in element.kept.drain() {
690            row.teardown::<State>(ctx);
691        }
692    }
693}
694
695impl Widget for SliverListWidget {
696    /// Outside a viewport the list lays out as if it were the only sliver of a
697    /// viewport `bc.max()` long, scrolled to its start (the fixed-extent
698    /// list's rule).
699    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
700        let axis = self.constraints.map_or(ScrollAxis::Vertical, |sc| sc.axis);
701        let max = bc.max();
702        let main = non_negative(axis.main_extent(max));
703        let cross = finite_extent(axis.cross_extent(max));
704        let sc = SliverConstraints {
705            axis,
706            scroll_offset: 0.0,
707            preceding_scroll_extent: 0.0,
708            overlap: 0.0,
709            remaining_paint_extent: main,
710            cross_axis_extent: cross,
711            viewport_main_axis_extent: main,
712            cache_origin: 0.0,
713            remaining_cache_extent: main,
714            user_scroll_direction: ScrollDirection::Idle,
715        };
716        // No viewport applies a correction here: the position stays put, and
717        // a leading-edge shift (already applied to the rows) just lays out
718        // again.
719        self.pending_correction = 0.0;
720        let corrections = self.corrections;
721        let mut geometry = self.layout_sliver(ctx, &sc);
722        for _ in 0..2 {
723            if geometry.scroll_offset_correction.is_none() {
724                break;
725            }
726            geometry = self.layout_sliver(ctx, &sc);
727        }
728        self.corrections = corrections;
729        bc.constrain(axis.size(geometry.paint_extent, cross))
730    }
731
732    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
733        if self.clip {
734            scene.push_clip(ctx.origin(), ctx.size());
735        }
736        let axis = self.constraints.map_or(ScrollAxis::Vertical, |sc| sc.axis);
737        let paint_extent = self.paint_extent;
738        for pod in &mut self.children {
739            // A row only cached is laid out but never painted.
740            let origin = pod.origin();
741            let start = axis.main_extent(Size::new(origin.x, origin.y));
742            let extent = axis.main_extent(pod.size());
743            if start + extent > 0.0 && start < paint_extent {
744                pod.paint_child(ctx, scene);
745            }
746        }
747        if self.clip {
748            scene.pop_clip();
749        }
750        // The next rebuild re-windows to what this frame's layout asked for;
751        // make sure it runs. A fire observed at layout owes the same frame.
752        let wanted = self.plan(0.0).range;
753        if !self.covers(&wanted) || self.near_start.pending || self.near_end.pending {
754            ctx.request_frame();
755        }
756    }
757
758    /// Delivers a near-edge fire observed at layout, then routes the event to
759    /// the materialised rows (parked rows receive nothing).
760    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
761        self.near_start.deliver(ctx);
762        self.near_end.deliver(ctx);
763        crate::authoring::route_event(&mut self.children, ctx, event)
764    }
765
766    fn semantics(&self, ctx: &mut SemanticsCtx) {
767        let count = self.item_count;
768        ctx.push_container(
769            Role::List,
770            move |node| node.set_size_of_set(count),
771            |ctx| {
772                for pod in &self.children {
773                    pod.semantics_child(ctx);
774                }
775            },
776        );
777    }
778
779    fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
780        Some(self)
781    }
782
783    crate::authoring::visit_children!(children);
784}
785
786impl SliverWidget for SliverListWidget {
787    fn layout_sliver(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry {
788        // A keyed-anchoring shift rebuild recorded: the viewport moves the
789        // position by it and lays out again.
790        let pending = std::mem::take(&mut self.pending_correction);
791        if pending.is_finite() && pending.abs() > DRIFT_TOLERANCE {
792            self.corrections += 1;
793            return SliverGeometry {
794                scroll_offset_correction: Some(pending),
795                ..SliverGeometry::ZERO
796            };
797        }
798
799        let axis = sc.axis;
800        let row = sc.as_box_constraints();
801        let mut extents = Vec::with_capacity(self.children.len());
802        for (slot, pod) in self.children.iter_mut().enumerate() {
803            let extent = finite_extent(axis.main_extent(pod.layout_child(ctx, &row)));
804            extents.push(extent);
805            if let (Some(&key), Some(&index)) = (self.slot_keys.get(slot), self.indices.get(slot)) {
806                reconcile::record_measurement(
807                    &mut self.measured,
808                    &mut self.measured_sum,
809                    key,
810                    index,
811                    extent,
812                );
813            }
814        }
815        let mut ys = self.place(&extents);
816
817        // The leading edge (the module docs' *Corrections through the
818        // viewport*): row 0 starts at 0, and a window starting at a later row
819        // leaves room before it.
820        if let (Some(&first), Some(&y_first)) = (self.indices.first(), ys.first()) {
821            let wanted = if first == 0 {
822                0.0
823            } else if y_first < DRIFT_TOLERANCE {
824                // Rows above the window with no room left before the list's
825                // start get their room back, as the measurements and the
826                // estimate size it.
827                self.room_before(first)
828            } else {
829                y_first
830            };
831            let drift = wanted - y_first;
832            if drift != 0.0 {
833                for y in &mut ys {
834                    *y += drift;
835                }
836                self.anchor.y += drift;
837                if drift.abs() > DRIFT_TOLERANCE
838                    && starts_at_or_above_leading_edge(sc)
839                    && sc.scroll_offset + drift >= 0.0
840                {
841                    self.corrections += 1;
842                    return SliverGeometry {
843                        scroll_offset_correction: Some(drift),
844                        ..SliverGeometry::ZERO
845                    };
846                }
847            }
848        }
849
850        let lead = sc.scroll_offset;
851        for (pod, y) in self.children.iter_mut().zip(&ys) {
852            pod.set_origin(axis_point(axis, y - lead, 0.0));
853        }
854
855        let count = self.item_count;
856        let scroll_extent = match (self.indices.last(), ys.last(), extents.last()) {
857            (Some(&last), Some(&y_last), Some(&extent_last)) => {
858                let end = y_last + extent_last;
859                if last + 1 >= count {
860                    end
861                } else {
862                    self.estimated_total().max(end)
863                }
864            }
865            _ => self.estimated_total(),
866        };
867        let scroll_extent = finite_extent(scroll_extent);
868        let paint_extent = paint_offset(sc, 0.0, scroll_extent);
869        let geometry = SliverGeometry {
870            cache_extent: cache_offset(sc, 0.0, scroll_extent).max(paint_extent),
871            has_visual_overflow: scroll_extent > sc.scroll_offset + sc.remaining_paint_extent
872                || sc.scroll_offset > 0.0,
873            ..SliverGeometry::new(scroll_extent, paint_extent, scroll_extent)
874        };
875
876        // The row the leading edge sits in anchors the next rebuild.
877        if !ys.is_empty() {
878            let slot = (0..ys.len())
879                .find(|&slot| ys[slot] + extents[slot] > lead)
880                .unwrap_or(ys.len() - 1);
881            self.anchor = Anchor {
882                key: self.slot_keys.get(slot).copied(),
883                index: self.indices[slot],
884                y: ys[slot],
885            };
886        }
887        self.anchoring = starts_at_or_above_leading_edge(sc);
888        self.constraints = Some(*sc);
889        self.paint_extent = paint_extent;
890        self.clip = geometry.has_visual_overflow;
891        self.observe_edges(sc, scroll_extent);
892        geometry
893    }
894}
895
896#[cfg(test)]
897mod tests {
898    use super::*;
899
900    fn widget(count: usize) -> SliverListWidget {
901        SliverListWidget::new(count, 50.0, Rc::new(|i| ChildKey::new(i as u64)))
902    }
903
904    fn constraints(scroll_offset: f64) -> SliverConstraints {
905        SliverConstraints {
906            axis: ScrollAxis::Vertical,
907            scroll_offset,
908            preceding_scroll_extent: 0.0,
909            overlap: 0.0,
910            remaining_paint_extent: 500.0,
911            cross_axis_extent: 400.0,
912            viewport_main_axis_extent: 500.0,
913            cache_origin: -scroll_offset.min(100.0),
914            remaining_cache_extent: 600.0 + scroll_offset.min(100.0),
915            user_scroll_direction: ScrollDirection::Idle,
916        }
917    }
918
919    #[test]
920    fn the_plan_walks_from_the_anchor_over_measured_and_estimated_rows() {
921        let mut w = widget(1000);
922        assert!(w.plan(0.0).range.is_empty(), "no layout, no window");
923        w.constraints = Some(constraints(0.0));
924        // [0, 600) over 50 px estimates: rows 0..12.
925        assert_eq!(w.plan(0.0).range, 0..12);
926        // Rows 0..4 measured at 100: [0, 600) now holds rows 0..8.
927        for i in 0..4u64 {
928            reconcile::record_measurement(
929                &mut w.measured,
930                &mut w.measured_sum,
931                ChildKey::new(i),
932                i as usize,
933                100.0,
934            );
935        }
936        assert_eq!(w.plan(0.0).range, 0..8);
937        assert_eq!(w.estimated_total(), 400.0 + 996.0 * 50.0);
938        // Scrolled to 1000 from an anchor at row 4 (offset 400): the leading
939        // edge falls in row 16 (400 + 12 × 50), the cache region is [900,
940        // 1600).
941        w.constraints = Some(constraints(1000.0));
942        w.anchor = Anchor {
943            key: None,
944            index: 4,
945            y: 400.0,
946        };
947        let plan = w.plan(0.0);
948        assert_eq!(plan.reference, (16, 1000.0));
949        assert_eq!(plan.range, 14..28);
950        // A shift owed to the position moves the region with it.
951        assert_eq!(w.plan(150.0).reference, (19, 1150.0));
952    }
953
954    #[test]
955    fn rows_are_placed_both_ways_from_the_anchor() {
956        let mut w = widget(100);
957        w.indices = vec![10, 11, 12, 13];
958        w.anchor = Anchor {
959            key: None,
960            index: 12,
961            y: 600.0,
962        };
963        assert_eq!(
964            w.place(&[30.0, 40.0, 50.0, 60.0]),
965            vec![530.0, 560.0, 600.0, 650.0]
966        );
967    }
968}