1use std::collections::HashMap;
70use std::ops::Range;
71use std::rc::Rc;
72
73use frust_core::sliver::PRECISION_ERROR_TOLERANCE;
74use frust_core::{
75 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
76 LayoutCtx, PaintCtx, PaintScene, ScrollAxis, ScrollDirection, SemanticsCtx, SliverConstraints,
77 SliverGeometry, SliverWidget, View, Widget, any,
78};
79use kurbo::Size;
80
81use super::adapter::{axis_point, finite_extent, non_negative};
82use super::reconcile::{self, ItemViews, LiveRows};
83use super::{cache_offset, paint_offset};
84use crate::ChildKey;
85use crate::authoring::ErasedCallback;
86
87const DUPLICATE_KEY_MSG: &str = "sliver_fixed_extent_list_keyed produced a duplicate key inside \
91 one window: row identity is ambiguous (release: the first slot claiming a key keeps the live \
92 row, later duplicates build fresh)";
93
94type ItemBuilder<State> = Rc<dyn Fn(usize) -> AnyView<State>>;
96
97type KeyOf = Rc<dyn Fn(usize) -> ChildKey>;
99
100type OnNearEdge<State> = Rc<dyn Fn(&mut State)>;
102
103pub struct SliverFixedExtentListView<State: 'static> {
105 item_count: usize,
106 item_extent: f64,
107 builder: ItemBuilder<State>,
108 key_of: Option<KeyOf>,
110 on_near_start: Option<OnNearEdge<State>>,
111 near_start_threshold: f64,
112 on_near_end: Option<OnNearEdge<State>>,
113 near_end_threshold: f64,
114}
115
116pub fn sliver_fixed_extent_list<State: 'static, V: View<State>>(
135 item_count: usize,
136 item_extent: f64,
137 builder: impl Fn(usize) -> V + 'static,
138) -> SliverFixedExtentListView<State> {
139 SliverFixedExtentListView::new(item_count, item_extent, None, builder)
140}
141
142pub fn sliver_fixed_extent_list_keyed<State: 'static, V: View<State>>(
167 item_count: usize,
168 item_extent: f64,
169 key_of: impl Fn(usize) -> ChildKey + 'static,
170 builder: impl Fn(usize) -> V + 'static,
171) -> SliverFixedExtentListView<State> {
172 SliverFixedExtentListView::new(item_count, item_extent, Some(Rc::new(key_of)), builder)
173}
174
175impl<State: 'static> SliverFixedExtentListView<State> {
176 fn new<V: View<State>>(
177 item_count: usize,
178 item_extent: f64,
179 key_of: Option<KeyOf>,
180 builder: impl Fn(usize) -> V + 'static,
181 ) -> Self {
182 assert!(
183 item_extent > 0.0 && item_extent.is_finite(),
184 "sliver_fixed_extent_list item_extent must be positive and finite"
185 );
186 Self {
187 item_count,
188 item_extent,
189 builder: Rc::new(move |index| any(builder(index))),
190 key_of,
191 on_near_start: None,
192 near_start_threshold: 0.0,
193 on_near_end: None,
194 near_end_threshold: 0.0,
195 }
196 }
197
198 pub fn on_near_start<F: Fn(&mut State) + 'static>(
203 mut self,
204 callback: F,
205 threshold_px: f64,
206 ) -> Self {
207 self.on_near_start = Some(Rc::new(callback));
208 self.near_start_threshold = threshold_px;
209 self
210 }
211
212 pub fn on_near_end<F: Fn(&mut State) + 'static>(
217 mut self,
218 callback: F,
219 threshold_px: f64,
220 ) -> Self {
221 self.on_near_end = Some(Rc::new(callback));
222 self.near_end_threshold = threshold_px;
223 self
224 }
225}
226
227struct NearEdge {
229 callback: Option<ErasedCallback>,
230 threshold: f64,
231 armed: bool,
234 pending: bool,
236}
237
238impl NearEdge {
239 fn new() -> Self {
240 Self {
241 callback: None,
242 threshold: 0.0,
243 armed: true,
244 pending: false,
245 }
246 }
247
248 fn observe(&mut self, distance: f64) -> bool {
251 if self.callback.is_none() {
252 return false;
253 }
254 if distance > 2.0 * self.threshold {
255 self.armed = true;
256 }
257 if self.armed && distance <= self.threshold {
258 self.armed = false;
259 self.pending = true;
260 return true;
261 }
262 false
263 }
264
265 fn deliver(&mut self, ctx: &mut EventCtx) {
267 if std::mem::take(&mut self.pending)
268 && let Some(callback) = self.callback.as_mut()
269 {
270 callback(ctx);
271 }
272 }
273}
274
275pub struct SliverFixedExtentListWidget {
278 children: Vec<ChildPod>,
280 indices: Vec<usize>,
282 key_index: HashMap<ChildKey, usize>,
285 item_count: usize,
286 item_extent: f64,
287 constraints: Option<SliverConstraints>,
290 paint_extent: f64,
292 clip: bool,
294 observed_at: Option<(f64, f64)>,
297 near_start: NearEdge,
298 near_end: NearEdge,
299}
300
301fn min_index(offset: f64, extent: f64) -> f64 {
304 let actual = offset / extent;
305 let round = actual.round();
306 if (actual * extent - round * extent).abs() < PRECISION_ERROR_TOLERANCE {
307 round
308 } else {
309 actual.floor()
310 }
311}
312
313fn end_index(offset: f64, extent: f64) -> f64 {
316 let actual = offset / extent - 1.0;
317 let round = actual.round();
318 if (actual * extent - round * extent).abs() < PRECISION_ERROR_TOLERANCE {
319 round.max(0.0) + 1.0
320 } else {
321 actual.ceil().max(0.0) + 1.0
322 }
323}
324
325fn cache_window(sc: &SliverConstraints, item_count: usize, item_extent: f64) -> Range<usize> {
328 if item_count == 0 || sc.remaining_cache_extent <= 0.0 {
329 return 0..0;
330 }
331 let count = item_count as f64;
332 let start = (sc.scroll_offset + sc.cache_origin).max(0.0);
333 let end = start + sc.remaining_cache_extent;
334 let first = min_index(start, item_extent).clamp(0.0, count);
335 let last = if end.is_finite() {
336 end_index(end, item_extent).clamp(0.0, count)
337 } else {
338 count
339 };
340 let (first, last) = (first as usize, last as usize);
341 first.min(last)..last
342}
343
344impl SliverFixedExtentListWidget {
345 fn new(item_count: usize, item_extent: f64) -> Self {
346 Self {
347 children: Vec::new(),
348 indices: Vec::new(),
349 key_index: HashMap::new(),
350 item_count,
351 item_extent,
352 constraints: None,
353 paint_extent: 0.0,
354 clip: false,
355 observed_at: None,
356 near_start: NearEdge::new(),
357 near_end: NearEdge::new(),
358 }
359 }
360
361 pub fn window(&self) -> &[usize] {
363 &self.indices
364 }
365
366 pub fn item_count(&self) -> usize {
368 self.item_count
369 }
370
371 fn desired_window(&self) -> Range<usize> {
374 match &self.constraints {
375 Some(sc) => cache_window(sc, self.item_count, self.item_extent),
376 None => 0..0,
377 }
378 }
379
380 fn covers(&self, range: &Range<usize>) -> bool {
382 if range.is_empty() {
383 return true;
384 }
385 match (self.indices.first(), self.indices.last()) {
386 (Some(&first), Some(&last)) => first <= range.start && last + 1 >= range.end,
387 _ => false,
388 }
389 }
390
391 fn observe_edges(&mut self, sc: &SliverConstraints, total: f64) {
395 let at = (sc.scroll_offset, sc.remaining_paint_extent);
396 let previous = self.observed_at.replace(at);
397 if previous.is_none_or(|previous| previous == at) || self.item_count == 0 {
398 return;
399 }
400 let to_end = non_negative(total - sc.scroll_offset - sc.remaining_paint_extent);
401 let start = self.near_start.observe(sc.scroll_offset);
402 let end = self.near_end.observe(to_end);
403 if start || end {
404 frust_core::mark_pending_result_flush();
405 }
406 }
407}
408
409impl<State: 'static> SliverFixedExtentListView<State> {
410 fn install_edges(&self, widget: &mut SliverFixedExtentListWidget) {
413 widget.near_start.callback = self
414 .on_near_start
415 .as_ref()
416 .map(crate::authoring::erase_callback);
417 widget.near_start.threshold = self.near_start_threshold;
418 widget.near_end.callback = self
419 .on_near_end
420 .as_ref()
421 .map(crate::authoring::erase_callback);
422 widget.near_end.threshold = self.near_end_threshold;
423 }
424}
425
426impl<State: 'static> View<State> for SliverFixedExtentListView<State> {
427 type Element = SliverFixedExtentListWidget;
428
429 fn build(&self, _ctx: &mut BuildCtx<'_>) -> SliverFixedExtentListWidget {
432 let mut widget = SliverFixedExtentListWidget::new(self.item_count, self.item_extent);
433 self.install_edges(&mut widget);
434 widget
435 }
436
437 fn rebuild(
438 &self,
439 prev: &Self,
440 element: &mut SliverFixedExtentListWidget,
441 ctx: &mut BuildCtx<'_>,
442 ) -> ChangeFlags {
443 self.install_edges(element);
444 let mut flags = ChangeFlags::NONE;
445 if element.item_count != self.item_count {
446 element.item_count = self.item_count;
447 element.near_start.armed = true;
449 element.near_end.armed = true;
450 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
451 }
452 if element.item_extent != self.item_extent {
453 element.item_extent = self.item_extent;
454 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
455 }
456 let range = element.desired_window();
457 let views = ItemViews {
458 prev: &*prev.builder,
459 next: &*self.builder,
460 };
461 let rows = LiveRows {
462 children: &mut element.children,
463 indices: &mut element.indices,
464 };
465 flags |= match self.key_of.as_ref() {
466 Some(key_of) => {
467 reconcile::reconcile_keyed(
468 views,
469 rows,
470 &mut element.key_index,
471 &**key_of,
472 range,
473 DUPLICATE_KEY_MSG,
474 ctx,
475 )
476 .flags
477 }
478 None => {
479 element.key_index.clear();
483 reconcile::reconcile_positional(views, rows, range, ctx)
484 }
485 };
486 flags
487 }
488
489 fn teardown(&self, element: &mut SliverFixedExtentListWidget, ctx: &mut BuildCtx<'_>) {
490 for (index, pod) in element.indices.iter().zip(element.children.iter_mut()) {
491 crate::authoring::teardown_child(&(self.builder)(*index), pod, ctx);
492 }
493 }
494}
495
496impl Widget for SliverFixedExtentListWidget {
497 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
502 let axis = self.constraints.map_or(ScrollAxis::Vertical, |sc| sc.axis);
503 let max = bc.max();
504 let main = non_negative(axis.main_extent(max));
505 let cross = finite_extent(axis.cross_extent(max));
506 let sc = SliverConstraints {
507 axis,
508 scroll_offset: 0.0,
509 preceding_scroll_extent: 0.0,
510 overlap: 0.0,
511 remaining_paint_extent: main,
512 cross_axis_extent: cross,
513 viewport_main_axis_extent: main,
514 cache_origin: 0.0,
515 remaining_cache_extent: main,
516 user_scroll_direction: ScrollDirection::Idle,
517 };
518 let geometry = self.layout_sliver(ctx, &sc);
519 bc.constrain(axis.size(geometry.paint_extent, cross))
520 }
521
522 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
523 if self.clip {
524 scene.push_clip(ctx.origin(), ctx.size());
525 }
526 let axis = self.constraints.map_or(ScrollAxis::Vertical, |sc| sc.axis);
527 let (extent, paint_extent) = (self.item_extent, self.paint_extent);
528 for pod in &mut self.children {
529 let origin = pod.origin();
531 let start = axis.main_extent(Size::new(origin.x, origin.y));
532 if start + extent > 0.0 && start < paint_extent {
533 pod.paint_child(ctx, scene);
534 }
535 }
536 if self.clip {
537 scene.pop_clip();
538 }
539 let wanted = self.desired_window();
542 if !self.covers(&wanted) || self.near_start.pending || self.near_end.pending {
543 ctx.request_frame();
544 }
545 }
546
547 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
551 self.near_start.deliver(ctx);
552 self.near_end.deliver(ctx);
553 crate::authoring::route_event(&mut self.children, ctx, event)
554 }
555
556 fn semantics(&self, ctx: &mut SemanticsCtx) {
557 for pod in &self.children {
558 pod.semantics_child(ctx);
559 }
560 }
561
562 fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
563 Some(self)
564 }
565
566 crate::authoring::visit_children!(children);
567}
568
569impl SliverWidget for SliverFixedExtentListWidget {
570 fn layout_sliver(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry {
571 let axis = sc.axis;
572 let extent = self.item_extent;
573 let total = self.item_count as f64 * extent;
574 let row = BoxConstraints::tight(axis.size(extent, sc.cross_axis_extent));
575 for (index, pod) in self.indices.iter().zip(self.children.iter_mut()) {
576 pod.layout_child(ctx, &row);
577 pod.set_origin(axis_point(
578 axis,
579 *index as f64 * extent - sc.scroll_offset,
580 0.0,
581 ));
582 }
583 let paint_extent = paint_offset(sc, 0.0, total);
584 let geometry = SliverGeometry {
585 cache_extent: cache_offset(sc, 0.0, total).max(paint_extent),
586 has_visual_overflow: total > sc.scroll_offset + sc.remaining_paint_extent
587 || sc.scroll_offset > 0.0,
588 ..SliverGeometry::new(total, paint_extent, total)
589 };
590 self.constraints = Some(*sc);
591 self.paint_extent = paint_extent;
592 self.clip = geometry.has_visual_overflow;
593 self.observe_edges(sc, total);
594 geometry
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601
602 fn constraints(scroll_offset: f64, cache: f64) -> SliverConstraints {
603 SliverConstraints {
604 axis: ScrollAxis::Vertical,
605 scroll_offset,
606 preceding_scroll_extent: 0.0,
607 overlap: 0.0,
608 remaining_paint_extent: 500.0,
609 cross_axis_extent: 400.0,
610 viewport_main_axis_extent: 500.0,
611 cache_origin: -scroll_offset.min(cache),
612 remaining_cache_extent: 500.0 + cache + scroll_offset.min(cache),
613 user_scroll_direction: ScrollDirection::Idle,
614 }
615 }
616
617 #[test]
618 fn the_window_is_exactly_the_rows_the_cache_region_touches() {
619 assert_eq!(cache_window(&constraints(0.0, 250.0), 10_000, 50.0), 0..15);
621 assert_eq!(
623 cache_window(&constraints(1000.0, 250.0), 10_000, 50.0),
624 15..35
625 );
626 let sc = SliverConstraints {
628 scroll_offset: 1000.0 + 1e-12,
629 ..constraints(1000.0, 250.0)
630 };
631 assert_eq!(cache_window(&sc, 10_000, 50.0), 15..35);
632 assert_eq!(
634 cache_window(&constraints(1010.0, 250.0), 10_000, 50.0),
635 15..36
636 );
637 assert_eq!(cache_window(&constraints(0.0, 250.0), 4, 50.0), 0..4);
639 assert_eq!(cache_window(&constraints(5000.0, 250.0), 4, 50.0), 4..4);
640 let none = SliverConstraints {
641 remaining_cache_extent: 0.0,
642 ..constraints(0.0, 250.0)
643 };
644 assert!(cache_window(&none, 100, 50.0).is_empty());
645 assert!(cache_window(&constraints(0.0, 250.0), 0, 50.0).is_empty());
646 let all = SliverConstraints {
648 remaining_cache_extent: f64::INFINITY,
649 ..constraints(0.0, 250.0)
650 };
651 assert_eq!(cache_window(&all, 7, 50.0), 0..7);
652 }
653
654 #[test]
655 fn a_near_edge_fires_once_per_approach() {
656 let mut edge = NearEdge::new();
657 assert!(!edge.observe(0.0), "no callback, no fire");
658 edge.callback = Some(Box::new(|_: &mut EventCtx| {}));
659 edge.threshold = 100.0;
660 assert!(edge.observe(50.0));
661 assert!(edge.pending);
662 assert!(!edge.observe(20.0), "disarmed");
663 assert!(!edge.observe(150.0), "not yet past twice the threshold");
664 assert!(!edge.observe(80.0));
665 assert!(!edge.observe(250.0));
666 assert!(edge.observe(90.0), "rearmed past twice the threshold");
667 }
668}