1use 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
109const 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
114const DEFAULT_ESTIMATED_ITEM_EXTENT: f64 = 48.0;
118
119const DRIFT_TOLERANCE: f64 = 1e-6;
122
123type ItemBuilder<State> = Rc<dyn Fn(usize) -> AnyView<State>>;
125
126type KeyOf = Rc<dyn Fn(usize) -> ChildKey>;
128
129type OnNearEdge<State> = Rc<dyn Fn(&mut State)>;
131
132pub 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
145pub 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 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 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 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 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#[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
259struct Plan {
262 range: Range<usize>,
263 reference: (usize, f64),
265}
266
267pub struct SliverListWidget {
271 children: Vec<ChildPod>,
273 indices: Vec<usize>,
275 slot_keys: Vec<ChildKey>,
277 key_index: HashMap<ChildKey, usize>,
279 kept: HashMap<ChildKey, KeptRow>,
281 key_of: Option<KeyOf>,
284 measured: HashMap<ChildKey, Measured>,
286 measured_sum: f64,
288 estimate: f64,
289 item_count: usize,
290 anchor: Anchor,
293 anchoring: bool,
296 pending_correction: f64,
299 corrections: u64,
301 constraints: Option<SliverConstraints>,
304 paint_extent: f64,
306 clip: bool,
308 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 pub fn window(&self) -> &[usize] {
343 &self.indices
344 }
345
346 pub fn item_count(&self) -> usize {
348 self.item_count
349 }
350
351 pub fn measured_count(&self) -> usize {
353 self.measured.len()
354 }
355
356 pub fn kept_alive_count(&self) -> usize {
358 self.kept.len()
359 }
360
361 pub fn scroll_corrections(&self) -> u64 {
364 self.corrections
365 }
366
367 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 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 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 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 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 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 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 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
515fn 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 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 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 if !shifted_down {
586 element.near_start.armed = true;
587 }
588 element.near_end.armed = true;
589 }
590
591 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(w.plan(0.0).range, 0..12);
926 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 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 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}