use std::collections::HashMap;
use std::ops::Range;
use std::rc::Rc;
use frust_core::accesskit::Role;
use frust_core::{
AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
LayoutCtx, PaintCtx, PaintScene, ScrollAxis, ScrollDirection, SemanticsCtx, SliverConstraints,
SliverGeometry, SliverWidget, View, Widget, any,
};
use kurbo::Size;
use super::adapter::{axis_point, finite_extent, non_negative};
use super::keep_alive::{RowScope, row_wants_keep_alive};
use super::reconcile::{self, ItemViews, KeptRow, LiveRows, Measured, Retention};
use super::{NearEdge, cache_offset, paint_offset};
use crate::ChildKey;
const DUPLICATE_KEY_MSG: &str = "sliver_list produced a duplicate key inside one window: row \
identity is ambiguous (release: the first slot claiming a key keeps the live row, later \
duplicates build fresh)";
const DEFAULT_ESTIMATED_ITEM_EXTENT: f64 = 48.0;
const DRIFT_TOLERANCE: f64 = 1e-6;
type ItemBuilder<State> = Rc<dyn Fn(usize) -> AnyView<State>>;
type KeyOf = Rc<dyn Fn(usize) -> ChildKey>;
type OnNearEdge<State> = Rc<dyn Fn(&mut State)>;
pub struct SliverListView<State: 'static> {
item_count: usize,
estimate: f64,
builder: ItemBuilder<State>,
key_of: KeyOf,
on_near_start: Option<OnNearEdge<State>>,
near_start_threshold: f64,
on_near_end: Option<OnNearEdge<State>>,
near_end_threshold: f64,
}
pub fn sliver_list<State: 'static, V: View<State>>(
item_count: usize,
key_of: impl Fn(usize) -> ChildKey + 'static,
builder: impl Fn(usize) -> V + 'static,
) -> SliverListView<State> {
SliverListView {
item_count,
estimate: DEFAULT_ESTIMATED_ITEM_EXTENT,
builder: Rc::new(move |index| any(RowScope::new(builder(index)))),
key_of: Rc::new(key_of),
on_near_start: None,
near_start_threshold: 0.0,
on_near_end: None,
near_end_threshold: 0.0,
}
}
impl<State: 'static> SliverListView<State> {
pub fn estimated_item_extent(mut self, estimate_px: f64) -> Self {
assert!(
estimate_px > 0.0 && estimate_px.is_finite(),
"sliver_list estimated_item_extent must be positive and finite"
);
self.estimate = estimate_px;
self
}
pub fn on_near_start<F: Fn(&mut State) + 'static>(
mut self,
callback: F,
threshold_px: f64,
) -> Self {
self.on_near_start = Some(Rc::new(callback));
self.near_start_threshold = threshold_px;
self
}
pub fn on_near_end<F: Fn(&mut State) + 'static>(
mut self,
callback: F,
threshold_px: f64,
) -> Self {
self.on_near_end = Some(Rc::new(callback));
self.near_end_threshold = threshold_px;
self
}
fn install_edges(&self, widget: &mut SliverListWidget) {
widget.near_start.callback = self
.on_near_start
.as_ref()
.map(crate::authoring::erase_callback);
widget.near_start.threshold = self.near_start_threshold;
widget.near_end.callback = self
.on_near_end
.as_ref()
.map(crate::authoring::erase_callback);
widget.near_end.threshold = self.near_end_threshold;
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct Anchor {
key: Option<ChildKey>,
index: usize,
y: f64,
}
impl Anchor {
const START: Self = Self {
key: None,
index: 0,
y: 0.0,
};
}
struct Plan {
range: Range<usize>,
reference: (usize, f64),
}
pub struct SliverListWidget {
children: Vec<ChildPod>,
indices: Vec<usize>,
slot_keys: Vec<ChildKey>,
key_index: HashMap<ChildKey, usize>,
kept: HashMap<ChildKey, KeptRow>,
key_of: Option<KeyOf>,
measured: HashMap<ChildKey, Measured>,
measured_sum: f64,
estimate: f64,
item_count: usize,
anchor: Anchor,
anchoring: bool,
pending_correction: f64,
corrections: u64,
constraints: Option<SliverConstraints>,
paint_extent: f64,
clip: bool,
observed_at: Option<(f64, f64)>,
near_start: NearEdge,
near_end: NearEdge,
}
impl SliverListWidget {
fn new(item_count: usize, estimate: f64, key_of: KeyOf) -> Self {
Self {
children: Vec::new(),
indices: Vec::new(),
slot_keys: Vec::new(),
key_index: HashMap::new(),
kept: HashMap::new(),
key_of: Some(key_of),
measured: HashMap::new(),
measured_sum: 0.0,
estimate,
item_count,
anchor: Anchor::START,
anchoring: false,
pending_correction: 0.0,
corrections: 0,
constraints: None,
paint_extent: 0.0,
clip: false,
observed_at: None,
near_start: NearEdge::new(),
near_end: NearEdge::new(),
}
}
pub fn window(&self) -> &[usize] {
&self.indices
}
pub fn item_count(&self) -> usize {
self.item_count
}
pub fn measured_count(&self) -> usize {
self.measured.len()
}
pub fn kept_alive_count(&self) -> usize {
self.kept.len()
}
pub fn scroll_corrections(&self) -> u64 {
self.corrections
}
fn extent_at(&self, index: usize) -> f64 {
match self.key_of.as_ref() {
Some(key_of) if index < self.item_count => self
.measured
.get(&key_of(index))
.map_or(self.estimate, |measured| measured.extent),
_ => self.estimate,
}
}
fn estimated_total(&self) -> f64 {
reconcile::estimated_total(
self.measured_sum,
self.measured.len(),
self.item_count,
self.estimate,
)
}
fn plan(&self, shift: f64) -> Plan {
let count = self.item_count;
let empty = |reference| Plan {
range: 0..0,
reference,
};
let Some(sc) = self.constraints.as_ref() else {
return empty((self.anchor.index, self.anchor.y));
};
if count == 0 {
return empty((0, 0.0));
}
let lead = sc.scroll_offset + shift;
let (w, y_w) = reconcile::walk_to_offset(
count,
self.anchor.index,
self.anchor.y,
lead,
self.estimate,
|index| self.extent_at(index),
);
let start_of_cache = lead + sc.cache_origin;
let end_of_cache = start_of_cache + sc.remaining_cache_extent;
let past_the_end = w + 1 == count && y_w + self.extent_at(w) <= start_of_cache;
if sc.remaining_cache_extent <= 0.0 || past_the_end {
return empty((w, y_w));
}
let (mut start, mut y_start) = (w, y_w);
while start > 0 && y_start > start_of_cache {
start -= 1;
y_start -= self.extent_at(start);
}
let (mut end, mut y_end) = (w, y_w);
loop {
y_end += self.extent_at(end);
end += 1;
if end >= count || y_end >= end_of_cache {
break;
}
}
Plan {
range: start..end,
reference: (w, y_w),
}
}
fn covers(&self, range: &Range<usize>) -> bool {
if range.is_empty() {
return true;
}
match (self.indices.first(), self.indices.last()) {
(Some(&first), Some(&last)) => first <= range.start && last + 1 >= range.end,
_ => false,
}
}
fn observe_edges(&mut self, sc: &SliverConstraints, total: f64) {
let at = (sc.scroll_offset, sc.remaining_paint_extent);
let previous = self.observed_at.replace(at);
if previous.is_none_or(|previous| previous == at) || self.item_count == 0 {
return;
}
let to_end = non_negative(total - sc.scroll_offset - sc.remaining_paint_extent);
let start = self.near_start.observe(sc.scroll_offset);
let end = self.near_end.observe(to_end);
if start || end {
frust_core::mark_pending_result_flush();
}
}
fn room_before(&self, index: usize) -> f64 {
let walked = index.min(reconcile::MAX_PREFIX_STEP);
let estimated = (index - walked) as f64 * self.estimate;
estimated
+ (index - walked..index)
.map(|i| self.extent_at(i))
.sum::<f64>()
}
fn place(&self, extents: &[f64]) -> Vec<f64> {
let n = extents.len();
let mut ys = vec![0.0; n];
let Some(&first) = self.indices.first() else {
return ys;
};
let from = match self
.anchor
.index
.checked_sub(first)
.filter(|slot| *slot < n)
{
Some(slot) => {
ys[slot] = self.anchor.y;
slot
}
None => {
let rows = first as f64 - self.anchor.index as f64;
ys[0] = self.anchor.y + rows * self.estimate;
0
}
};
for slot in from + 1..n {
ys[slot] = ys[slot - 1] + extents[slot - 1];
}
for slot in (0..from).rev() {
ys[slot] = ys[slot + 1] - extents[slot];
}
ys
}
}
fn starts_at_or_above_leading_edge(sc: &SliverConstraints) -> bool {
sc.scroll_offset > 0.0 || sc.remaining_paint_extent >= sc.viewport_main_axis_extent
}
impl<State: 'static> View<State> for SliverListView<State> {
type Element = SliverListWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> SliverListWidget {
let mut widget =
SliverListWidget::new(self.item_count, self.estimate, Rc::clone(&self.key_of));
self.install_edges(&mut widget);
widget
}
fn rebuild(
&self,
prev: &Self,
element: &mut SliverListWidget,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
self.install_edges(element);
element.key_of = Some(Rc::clone(&self.key_of));
let key_of: &dyn Fn(usize) -> ChildKey = &*self.key_of;
let mut flags = ChangeFlags::NONE;
let old_count = element.item_count;
let count = self.item_count;
let delta = count as isize - old_count as isize;
if old_count != count {
element.item_count = count;
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
if element.estimate != self.estimate {
element.estimate = self.estimate;
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
let anchor_before = element.anchor;
let mut anchor_missed = false;
let mut shifted_down = false;
if let Some(key) = element.anchor.key {
match reconcile::locate_key(key, element.anchor.index, delta, count, key_of) {
Some(index) if index != element.anchor.index => {
let rows = index as f64 - element.anchor.index as f64;
let shift = rows * element.estimate;
element.anchor.index = index;
element.anchor.y += shift;
if element.anchoring {
element.pending_correction += shift;
shifted_down = shift > 0.0;
}
}
Some(_) => {}
None => {
anchor_missed = true;
element.anchor.key = None;
element.anchor.index = element.anchor.index.min(count.saturating_sub(1));
}
}
}
if delta != 0 {
if !shifted_down {
element.near_start.armed = true;
}
element.near_end.armed = true;
}
if !element.kept.is_empty() {
let mut gone = Vec::new();
for (key, row) in element.kept.iter_mut() {
match reconcile::locate_key(*key, row.index, delta, count, key_of) {
Some(index) => row.index = index,
None => gone.push(*key),
}
}
for key in gone {
if let Some(row) = element.kept.remove(&key) {
row.teardown::<State>(ctx);
}
reconcile::forget_measurement(
&mut element.measured,
&mut element.measured_sum,
&key,
);
}
}
if count < old_count {
reconcile::evict_measurements_past(
&mut element.measured,
&mut element.measured_sum,
count,
);
}
let plan = element.plan(element.pending_correction);
let locate = |key: ChildKey, prev_index: usize| {
reconcile::locate_key(key, prev_index, delta, count, key_of)
};
let outcome = reconcile::reconcile_keyed_retaining(
ItemViews {
prev: &*prev.builder,
next: &*self.builder,
},
LiveRows {
children: &mut element.children,
indices: &mut element.indices,
},
&mut element.key_index,
key_of,
plan.range.clone(),
DUPLICATE_KEY_MSG,
Some(Retention {
kept: &mut element.kept,
wants: &row_wants_keep_alive,
locate: &locate,
}),
ctx,
);
flags |= outcome.flags;
if anchor_missed && !outcome.any_survivor {
element.measured.clear();
element.measured_sum = 0.0;
} else {
for (key, prev_index) in &outcome.departed {
if reconcile::probe_key(*key, *prev_index, delta, count, key_of).is_none() {
reconcile::forget_measurement(
&mut element.measured,
&mut element.measured_sum,
key,
);
}
}
}
element.slot_keys = outcome.slot_keys;
let (index, y) = if plan.range.contains(&element.anchor.index) {
(element.anchor.index, element.anchor.y)
} else {
plan.reference
};
let key = index
.checked_sub(plan.range.start)
.and_then(|slot| element.slot_keys.get(slot).copied())
.filter(|_| plan.range.contains(&index));
element.anchor = Anchor { key, index, y };
if element.anchor != anchor_before || element.pending_correction != 0.0 {
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
flags
}
fn teardown(&self, element: &mut SliverListWidget, ctx: &mut BuildCtx<'_>) {
for (index, pod) in element.indices.iter().zip(element.children.iter_mut()) {
crate::authoring::teardown_child(&(self.builder)(*index), pod, ctx);
}
for (_, row) in element.kept.drain() {
row.teardown::<State>(ctx);
}
}
}
impl Widget for SliverListWidget {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let axis = self.constraints.map_or(ScrollAxis::Vertical, |sc| sc.axis);
let max = bc.max();
let main = non_negative(axis.main_extent(max));
let cross = finite_extent(axis.cross_extent(max));
let sc = SliverConstraints {
axis,
scroll_offset: 0.0,
preceding_scroll_extent: 0.0,
overlap: 0.0,
remaining_paint_extent: main,
cross_axis_extent: cross,
viewport_main_axis_extent: main,
cache_origin: 0.0,
remaining_cache_extent: main,
user_scroll_direction: ScrollDirection::Idle,
};
self.pending_correction = 0.0;
let corrections = self.corrections;
let mut geometry = self.layout_sliver(ctx, &sc);
for _ in 0..2 {
if geometry.scroll_offset_correction.is_none() {
break;
}
geometry = self.layout_sliver(ctx, &sc);
}
self.corrections = corrections;
bc.constrain(axis.size(geometry.paint_extent, cross))
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
if self.clip {
scene.push_clip(ctx.origin(), ctx.size());
}
let axis = self.constraints.map_or(ScrollAxis::Vertical, |sc| sc.axis);
let paint_extent = self.paint_extent;
for pod in &mut self.children {
let origin = pod.origin();
let start = axis.main_extent(Size::new(origin.x, origin.y));
let extent = axis.main_extent(pod.size());
if start + extent > 0.0 && start < paint_extent {
pod.paint_child(ctx, scene);
}
}
if self.clip {
scene.pop_clip();
}
let wanted = self.plan(0.0).range;
if !self.covers(&wanted) || self.near_start.pending || self.near_end.pending {
ctx.request_frame();
}
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
self.near_start.deliver(ctx);
self.near_end.deliver(ctx);
crate::authoring::route_event(&mut self.children, ctx, event)
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
let count = self.item_count;
ctx.push_container(
Role::List,
move |node| node.set_size_of_set(count),
|ctx| {
for pod in &self.children {
pod.semantics_child(ctx);
}
},
);
}
fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
Some(self)
}
crate::authoring::visit_children!(children);
}
impl SliverWidget for SliverListWidget {
fn layout_sliver(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry {
let pending = std::mem::take(&mut self.pending_correction);
if pending.is_finite() && pending.abs() > DRIFT_TOLERANCE {
self.corrections += 1;
return SliverGeometry {
scroll_offset_correction: Some(pending),
..SliverGeometry::ZERO
};
}
let axis = sc.axis;
let row = sc.as_box_constraints();
let mut extents = Vec::with_capacity(self.children.len());
for (slot, pod) in self.children.iter_mut().enumerate() {
let extent = finite_extent(axis.main_extent(pod.layout_child(ctx, &row)));
extents.push(extent);
if let (Some(&key), Some(&index)) = (self.slot_keys.get(slot), self.indices.get(slot)) {
reconcile::record_measurement(
&mut self.measured,
&mut self.measured_sum,
key,
index,
extent,
);
}
}
let mut ys = self.place(&extents);
if let (Some(&first), Some(&y_first)) = (self.indices.first(), ys.first()) {
let wanted = if first == 0 {
0.0
} else if y_first < DRIFT_TOLERANCE {
self.room_before(first)
} else {
y_first
};
let drift = wanted - y_first;
if drift != 0.0 {
for y in &mut ys {
*y += drift;
}
self.anchor.y += drift;
if drift.abs() > DRIFT_TOLERANCE
&& starts_at_or_above_leading_edge(sc)
&& sc.scroll_offset + drift >= 0.0
{
self.corrections += 1;
return SliverGeometry {
scroll_offset_correction: Some(drift),
..SliverGeometry::ZERO
};
}
}
}
let lead = sc.scroll_offset;
for (pod, y) in self.children.iter_mut().zip(&ys) {
pod.set_origin(axis_point(axis, y - lead, 0.0));
}
let count = self.item_count;
let scroll_extent = match (self.indices.last(), ys.last(), extents.last()) {
(Some(&last), Some(&y_last), Some(&extent_last)) => {
let end = y_last + extent_last;
if last + 1 >= count {
end
} else {
self.estimated_total().max(end)
}
}
_ => self.estimated_total(),
};
let scroll_extent = finite_extent(scroll_extent);
let paint_extent = paint_offset(sc, 0.0, scroll_extent);
let geometry = SliverGeometry {
cache_extent: cache_offset(sc, 0.0, scroll_extent).max(paint_extent),
has_visual_overflow: scroll_extent > sc.scroll_offset + sc.remaining_paint_extent
|| sc.scroll_offset > 0.0,
..SliverGeometry::new(scroll_extent, paint_extent, scroll_extent)
};
if !ys.is_empty() {
let slot = (0..ys.len())
.find(|&slot| ys[slot] + extents[slot] > lead)
.unwrap_or(ys.len() - 1);
self.anchor = Anchor {
key: self.slot_keys.get(slot).copied(),
index: self.indices[slot],
y: ys[slot],
};
}
self.anchoring = starts_at_or_above_leading_edge(sc);
self.constraints = Some(*sc);
self.paint_extent = paint_extent;
self.clip = geometry.has_visual_overflow;
self.observe_edges(sc, scroll_extent);
geometry
}
}
#[cfg(test)]
mod tests {
use super::*;
fn widget(count: usize) -> SliverListWidget {
SliverListWidget::new(count, 50.0, Rc::new(|i| ChildKey::new(i as u64)))
}
fn constraints(scroll_offset: f64) -> SliverConstraints {
SliverConstraints {
axis: ScrollAxis::Vertical,
scroll_offset,
preceding_scroll_extent: 0.0,
overlap: 0.0,
remaining_paint_extent: 500.0,
cross_axis_extent: 400.0,
viewport_main_axis_extent: 500.0,
cache_origin: -scroll_offset.min(100.0),
remaining_cache_extent: 600.0 + scroll_offset.min(100.0),
user_scroll_direction: ScrollDirection::Idle,
}
}
#[test]
fn the_plan_walks_from_the_anchor_over_measured_and_estimated_rows() {
let mut w = widget(1000);
assert!(w.plan(0.0).range.is_empty(), "no layout, no window");
w.constraints = Some(constraints(0.0));
assert_eq!(w.plan(0.0).range, 0..12);
for i in 0..4u64 {
reconcile::record_measurement(
&mut w.measured,
&mut w.measured_sum,
ChildKey::new(i),
i as usize,
100.0,
);
}
assert_eq!(w.plan(0.0).range, 0..8);
assert_eq!(w.estimated_total(), 400.0 + 996.0 * 50.0);
w.constraints = Some(constraints(1000.0));
w.anchor = Anchor {
key: None,
index: 4,
y: 400.0,
};
let plan = w.plan(0.0);
assert_eq!(plan.reference, (16, 1000.0));
assert_eq!(plan.range, 14..28);
assert_eq!(w.plan(150.0).reference, (19, 1150.0));
}
#[test]
fn rows_are_placed_both_ways_from_the_anchor() {
let mut w = widget(100);
w.indices = vec![10, 11, 12, 13];
w.anchor = Anchor {
key: None,
index: 12,
y: 600.0,
};
assert_eq!(
w.place(&[30.0, 40.0, 50.0, 60.0]),
vec![530.0, 560.0, 600.0, 650.0]
);
}
}