use std::collections::HashMap;
use std::ops::Range;
use std::rc::Rc;
use frust_core::sliver::PRECISION_ERROR_TOLERANCE;
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::reconcile::{self, ItemViews, LiveRows};
use super::{cache_offset, paint_offset};
use crate::ChildKey;
use crate::authoring::ErasedCallback;
const DUPLICATE_KEY_MSG: &str = "sliver_fixed_extent_list_keyed 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)";
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 SliverFixedExtentListView<State: 'static> {
item_count: usize,
item_extent: f64,
builder: ItemBuilder<State>,
key_of: Option<KeyOf>,
on_near_start: Option<OnNearEdge<State>>,
near_start_threshold: f64,
on_near_end: Option<OnNearEdge<State>>,
near_end_threshold: f64,
}
pub fn sliver_fixed_extent_list<State: 'static, V: View<State>>(
item_count: usize,
item_extent: f64,
builder: impl Fn(usize) -> V + 'static,
) -> SliverFixedExtentListView<State> {
SliverFixedExtentListView::new(item_count, item_extent, None, builder)
}
pub fn sliver_fixed_extent_list_keyed<State: 'static, V: View<State>>(
item_count: usize,
item_extent: f64,
key_of: impl Fn(usize) -> ChildKey + 'static,
builder: impl Fn(usize) -> V + 'static,
) -> SliverFixedExtentListView<State> {
SliverFixedExtentListView::new(item_count, item_extent, Some(Rc::new(key_of)), builder)
}
impl<State: 'static> SliverFixedExtentListView<State> {
fn new<V: View<State>>(
item_count: usize,
item_extent: f64,
key_of: Option<KeyOf>,
builder: impl Fn(usize) -> V + 'static,
) -> Self {
assert!(
item_extent > 0.0 && item_extent.is_finite(),
"sliver_fixed_extent_list item_extent must be positive and finite"
);
Self {
item_count,
item_extent,
builder: Rc::new(move |index| any(builder(index))),
key_of,
on_near_start: None,
near_start_threshold: 0.0,
on_near_end: None,
near_end_threshold: 0.0,
}
}
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
}
}
struct NearEdge {
callback: Option<ErasedCallback>,
threshold: f64,
armed: bool,
pending: bool,
}
impl NearEdge {
fn new() -> Self {
Self {
callback: None,
threshold: 0.0,
armed: true,
pending: false,
}
}
fn observe(&mut self, distance: f64) -> bool {
if self.callback.is_none() {
return false;
}
if distance > 2.0 * self.threshold {
self.armed = true;
}
if self.armed && distance <= self.threshold {
self.armed = false;
self.pending = true;
return true;
}
false
}
fn deliver(&mut self, ctx: &mut EventCtx) {
if std::mem::take(&mut self.pending)
&& let Some(callback) = self.callback.as_mut()
{
callback(ctx);
}
}
}
pub struct SliverFixedExtentListWidget {
children: Vec<ChildPod>,
indices: Vec<usize>,
key_index: HashMap<ChildKey, usize>,
item_count: usize,
item_extent: f64,
constraints: Option<SliverConstraints>,
paint_extent: f64,
clip: bool,
observed_at: Option<(f64, f64)>,
near_start: NearEdge,
near_end: NearEdge,
}
fn min_index(offset: f64, extent: f64) -> f64 {
let actual = offset / extent;
let round = actual.round();
if (actual * extent - round * extent).abs() < PRECISION_ERROR_TOLERANCE {
round
} else {
actual.floor()
}
}
fn end_index(offset: f64, extent: f64) -> f64 {
let actual = offset / extent - 1.0;
let round = actual.round();
if (actual * extent - round * extent).abs() < PRECISION_ERROR_TOLERANCE {
round.max(0.0) + 1.0
} else {
actual.ceil().max(0.0) + 1.0
}
}
fn cache_window(sc: &SliverConstraints, item_count: usize, item_extent: f64) -> Range<usize> {
if item_count == 0 || sc.remaining_cache_extent <= 0.0 {
return 0..0;
}
let count = item_count as f64;
let start = (sc.scroll_offset + sc.cache_origin).max(0.0);
let end = start + sc.remaining_cache_extent;
let first = min_index(start, item_extent).clamp(0.0, count);
let last = if end.is_finite() {
end_index(end, item_extent).clamp(0.0, count)
} else {
count
};
let (first, last) = (first as usize, last as usize);
first.min(last)..last
}
impl SliverFixedExtentListWidget {
fn new(item_count: usize, item_extent: f64) -> Self {
Self {
children: Vec::new(),
indices: Vec::new(),
key_index: HashMap::new(),
item_count,
item_extent,
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
}
fn desired_window(&self) -> Range<usize> {
match &self.constraints {
Some(sc) => cache_window(sc, self.item_count, self.item_extent),
None => 0..0,
}
}
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();
}
}
}
impl<State: 'static> SliverFixedExtentListView<State> {
fn install_edges(&self, widget: &mut SliverFixedExtentListWidget) {
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;
}
}
impl<State: 'static> View<State> for SliverFixedExtentListView<State> {
type Element = SliverFixedExtentListWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> SliverFixedExtentListWidget {
let mut widget = SliverFixedExtentListWidget::new(self.item_count, self.item_extent);
self.install_edges(&mut widget);
widget
}
fn rebuild(
&self,
prev: &Self,
element: &mut SliverFixedExtentListWidget,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
self.install_edges(element);
let mut flags = ChangeFlags::NONE;
if element.item_count != self.item_count {
element.item_count = self.item_count;
element.near_start.armed = true;
element.near_end.armed = true;
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
if element.item_extent != self.item_extent {
element.item_extent = self.item_extent;
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
let range = element.desired_window();
let views = ItemViews {
prev: &*prev.builder,
next: &*self.builder,
};
let rows = LiveRows {
children: &mut element.children,
indices: &mut element.indices,
};
flags |= match self.key_of.as_ref() {
Some(key_of) => {
reconcile::reconcile_keyed(
views,
rows,
&mut element.key_index,
&**key_of,
range,
DUPLICATE_KEY_MSG,
ctx,
)
.flags
}
None => {
element.key_index.clear();
reconcile::reconcile_positional(views, rows, range, ctx)
}
};
flags
}
fn teardown(&self, element: &mut SliverFixedExtentListWidget, ctx: &mut BuildCtx<'_>) {
for (index, pod) in element.indices.iter().zip(element.children.iter_mut()) {
crate::authoring::teardown_child(&(self.builder)(*index), pod, ctx);
}
}
}
impl Widget for SliverFixedExtentListWidget {
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,
};
let geometry = self.layout_sliver(ctx, &sc);
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 (extent, paint_extent) = (self.item_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));
if start + extent > 0.0 && start < paint_extent {
pod.paint_child(ctx, scene);
}
}
if self.clip {
scene.pop_clip();
}
let wanted = self.desired_window();
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) {
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 SliverFixedExtentListWidget {
fn layout_sliver(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry {
let axis = sc.axis;
let extent = self.item_extent;
let total = self.item_count as f64 * extent;
let row = BoxConstraints::tight(axis.size(extent, sc.cross_axis_extent));
for (index, pod) in self.indices.iter().zip(self.children.iter_mut()) {
pod.layout_child(ctx, &row);
pod.set_origin(axis_point(
axis,
*index as f64 * extent - sc.scroll_offset,
0.0,
));
}
let paint_extent = paint_offset(sc, 0.0, total);
let geometry = SliverGeometry {
cache_extent: cache_offset(sc, 0.0, total).max(paint_extent),
has_visual_overflow: total > sc.scroll_offset + sc.remaining_paint_extent
|| sc.scroll_offset > 0.0,
..SliverGeometry::new(total, paint_extent, total)
};
self.constraints = Some(*sc);
self.paint_extent = paint_extent;
self.clip = geometry.has_visual_overflow;
self.observe_edges(sc, total);
geometry
}
}
#[cfg(test)]
mod tests {
use super::*;
fn constraints(scroll_offset: f64, cache: 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(cache),
remaining_cache_extent: 500.0 + cache + scroll_offset.min(cache),
user_scroll_direction: ScrollDirection::Idle,
}
}
#[test]
fn the_window_is_exactly_the_rows_the_cache_region_touches() {
assert_eq!(cache_window(&constraints(0.0, 250.0), 10_000, 50.0), 0..15);
assert_eq!(
cache_window(&constraints(1000.0, 250.0), 10_000, 50.0),
15..35
);
let sc = SliverConstraints {
scroll_offset: 1000.0 + 1e-12,
..constraints(1000.0, 250.0)
};
assert_eq!(cache_window(&sc, 10_000, 50.0), 15..35);
assert_eq!(
cache_window(&constraints(1010.0, 250.0), 10_000, 50.0),
15..36
);
assert_eq!(cache_window(&constraints(0.0, 250.0), 4, 50.0), 0..4);
assert_eq!(cache_window(&constraints(5000.0, 250.0), 4, 50.0), 4..4);
let none = SliverConstraints {
remaining_cache_extent: 0.0,
..constraints(0.0, 250.0)
};
assert!(cache_window(&none, 100, 50.0).is_empty());
assert!(cache_window(&constraints(0.0, 250.0), 0, 50.0).is_empty());
let all = SliverConstraints {
remaining_cache_extent: f64::INFINITY,
..constraints(0.0, 250.0)
};
assert_eq!(cache_window(&all, 7, 50.0), 0..7);
}
#[test]
fn a_near_edge_fires_once_per_approach() {
let mut edge = NearEdge::new();
assert!(!edge.observe(0.0), "no callback, no fire");
edge.callback = Some(Box::new(|_: &mut EventCtx| {}));
edge.threshold = 100.0;
assert!(edge.observe(50.0));
assert!(edge.pending);
assert!(!edge.observe(20.0), "disarmed");
assert!(!edge.observe(150.0), "not yet past twice the threshold");
assert!(!edge.observe(80.0));
assert!(!edge.observe(250.0));
assert!(edge.observe(90.0), "rearmed past twice the threshold");
}
}