1use std::collections::HashMap;
31use std::ops::Range;
32use std::rc::Rc;
33
34use frust_core::sliver::PRECISION_ERROR_TOLERANCE;
35use frust_core::{
36 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
37 LayoutCtx, PaintCtx, PaintScene, ScrollAxis, ScrollDirection, SemanticsCtx, SliverConstraints,
38 SliverGeometry, SliverWidget, View, Widget, any,
39};
40use kurbo::Size;
41
42use super::adapter::{axis_point, finite_extent, non_negative};
43use super::reconcile::{self, ItemViews, LiveRows};
44use super::{cache_offset, paint_offset};
45use crate::ChildKey;
46
47const DUPLICATE_KEY_MSG: &str = "sliver_grid_keyed produced a duplicate key inside one window: \
50 cell identity is ambiguous (release: the first slot claiming a key keeps the live cell, \
51 later duplicates build fresh)";
52
53type ItemBuilder<State> = Rc<dyn Fn(usize) -> AnyView<State>>;
55
56type KeyOf = Rc<dyn Fn(usize) -> ChildKey>;
58
59#[derive(Clone, Copy, Debug, PartialEq)]
64pub enum SliverGridDelegate {
65 FixedCrossAxisCount {
67 count: usize,
68 main_spacing: f64,
69 cross_spacing: f64,
70 child_aspect_ratio: f64,
71 },
72 MaxCrossAxisExtent {
76 max_cross_extent: f64,
77 main_spacing: f64,
78 cross_spacing: f64,
79 child_aspect_ratio: f64,
80 },
81}
82
83impl Default for SliverGridDelegate {
84 fn default() -> Self {
86 Self::FixedCrossAxisCount {
87 count: 2,
88 main_spacing: 0.0,
89 cross_spacing: 0.0,
90 child_aspect_ratio: 1.0,
91 }
92 }
93}
94
95#[derive(Clone, Copy, Debug, PartialEq)]
97struct GridLayout {
98 count: usize,
99 cell_main: f64,
100 cell_cross: f64,
101 main_stride: f64,
102 cross_stride: f64,
103 main_spacing: f64,
104}
105
106impl SliverGridDelegate {
107 fn validate(&self) {
109 let (main_spacing, cross_spacing, ratio) = match *self {
110 Self::FixedCrossAxisCount {
111 count,
112 main_spacing,
113 cross_spacing,
114 child_aspect_ratio,
115 } => {
116 assert!(count > 0, "SliverGridDelegate count must be at least 1");
117 (main_spacing, cross_spacing, child_aspect_ratio)
118 }
119 Self::MaxCrossAxisExtent {
120 max_cross_extent,
121 main_spacing,
122 cross_spacing,
123 child_aspect_ratio,
124 } => {
125 assert!(
126 max_cross_extent > 0.0 && max_cross_extent.is_finite(),
127 "SliverGridDelegate max_cross_extent must be positive and finite"
128 );
129 (main_spacing, cross_spacing, child_aspect_ratio)
130 }
131 };
132 assert!(
133 main_spacing >= 0.0 && main_spacing.is_finite(),
134 "SliverGridDelegate main_spacing must be non-negative and finite"
135 );
136 assert!(
137 cross_spacing >= 0.0 && cross_spacing.is_finite(),
138 "SliverGridDelegate cross_spacing must be non-negative and finite"
139 );
140 assert!(
141 ratio > 0.0 && ratio.is_finite(),
142 "SliverGridDelegate child_aspect_ratio must be positive and finite"
143 );
144 }
145
146 fn layout(&self, cross: f64) -> GridLayout {
148 let (count, main_spacing, cross_spacing, ratio) = match *self {
149 Self::FixedCrossAxisCount {
150 count,
151 main_spacing,
152 cross_spacing,
153 child_aspect_ratio,
154 } => (count, main_spacing, cross_spacing, child_aspect_ratio),
155 Self::MaxCrossAxisExtent {
156 max_cross_extent,
157 main_spacing,
158 cross_spacing,
159 child_aspect_ratio,
160 } => {
161 let count = (cross / (max_cross_extent + cross_spacing)).ceil();
162 let count = if count.is_finite() {
163 count.max(1.0)
164 } else {
165 1.0
166 };
167 (
168 count as usize,
169 main_spacing,
170 cross_spacing,
171 child_aspect_ratio,
172 )
173 }
174 };
175 let count = count.max(1);
176 let usable = (cross - cross_spacing * (count - 1) as f64).max(0.0);
177 let cell_cross = usable / count as f64;
178 let cell_main = cell_cross / ratio;
179 GridLayout {
180 count,
181 cell_main,
182 cell_cross,
183 main_stride: cell_main + main_spacing,
184 cross_stride: cell_cross + cross_spacing,
185 main_spacing,
186 }
187 }
188}
189
190impl GridLayout {
191 fn rows(&self, item_count: usize) -> usize {
193 item_count.div_ceil(self.count)
194 }
195
196 fn extent(&self, item_count: usize) -> f64 {
198 let rows = self.rows(item_count);
199 if rows == 0 {
200 0.0
201 } else {
202 (rows as f64 * self.main_stride - self.main_spacing).max(0.0)
203 }
204 }
205
206 fn cache_window(&self, sc: &SliverConstraints, item_count: usize) -> Range<usize> {
210 if item_count == 0 || sc.remaining_cache_extent <= 0.0 || self.main_stride <= 0.0 {
211 return 0..0;
212 }
213 let rows = self.rows(item_count) as f64;
214 let start = (sc.scroll_offset + sc.cache_origin).max(0.0);
215 let end = start + sc.remaining_cache_extent;
216 let first_row = row_at(start, self.main_stride, f64::floor).clamp(0.0, rows);
217 let end_row = if end.is_finite() {
218 row_at(end, self.main_stride, f64::ceil).clamp(0.0, rows)
219 } else {
220 rows
221 };
222 let first = (first_row as usize * self.count).min(item_count);
223 let end = (end_row as usize * self.count).min(item_count);
224 first.min(end)..end
225 }
226}
227
228fn row_at(offset: f64, stride: f64, round: fn(f64) -> f64) -> f64 {
232 let actual = offset / stride;
233 let nearest = actual.round();
234 if (actual * stride - nearest * stride).abs() < PRECISION_ERROR_TOLERANCE {
235 nearest
236 } else {
237 round(actual)
238 }
239}
240
241pub struct SliverGridView<State: 'static> {
243 item_count: usize,
244 delegate: SliverGridDelegate,
245 builder: ItemBuilder<State>,
246 key_of: Option<KeyOf>,
248}
249
250pub fn sliver_grid<State: 'static, V: View<State>>(
272 item_count: usize,
273 builder: impl Fn(usize) -> V + 'static,
274) -> SliverGridView<State> {
275 SliverGridView::new(item_count, None, builder)
276}
277
278pub fn sliver_grid_keyed<State: 'static, V: View<State>>(
293 item_count: usize,
294 key_of: impl Fn(usize) -> ChildKey + 'static,
295 builder: impl Fn(usize) -> V + 'static,
296) -> SliverGridView<State> {
297 SliverGridView::new(item_count, Some(Rc::new(key_of)), builder)
298}
299
300impl<State: 'static> SliverGridView<State> {
301 fn new<V: View<State>>(
302 item_count: usize,
303 key_of: Option<KeyOf>,
304 builder: impl Fn(usize) -> V + 'static,
305 ) -> Self {
306 Self {
307 item_count,
308 delegate: SliverGridDelegate::default(),
309 builder: Rc::new(move |index| any(builder(index))),
310 key_of,
311 }
312 }
313
314 pub fn delegate(mut self, delegate: SliverGridDelegate) -> Self {
321 delegate.validate();
322 self.delegate = delegate;
323 self
324 }
325}
326
327pub struct SliverGridWidget {
330 children: Vec<ChildPod>,
332 indices: Vec<usize>,
334 key_index: HashMap<ChildKey, usize>,
336 item_count: usize,
337 delegate: SliverGridDelegate,
338 constraints: Option<SliverConstraints>,
341 grid: Option<GridLayout>,
343 paint_extent: f64,
345 clip: bool,
347}
348
349impl SliverGridWidget {
350 fn new(item_count: usize, delegate: SliverGridDelegate) -> Self {
351 Self {
352 children: Vec::new(),
353 indices: Vec::new(),
354 key_index: HashMap::new(),
355 item_count,
356 delegate,
357 constraints: None,
358 grid: None,
359 paint_extent: 0.0,
360 clip: false,
361 }
362 }
363
364 pub fn window(&self) -> &[usize] {
366 &self.indices
367 }
368
369 pub fn item_count(&self) -> usize {
371 self.item_count
372 }
373
374 fn desired_window(&self) -> Range<usize> {
377 match &self.constraints {
378 Some(sc) => self
379 .delegate
380 .layout(sc.cross_axis_extent)
381 .cache_window(sc, self.item_count),
382 None => 0..0,
383 }
384 }
385
386 fn covers(&self, range: &Range<usize>) -> bool {
388 if range.is_empty() {
389 return true;
390 }
391 match (self.indices.first(), self.indices.last()) {
392 (Some(&first), Some(&last)) => first <= range.start && last + 1 >= range.end,
393 _ => false,
394 }
395 }
396}
397
398impl<State: 'static> View<State> for SliverGridView<State> {
399 type Element = SliverGridWidget;
400
401 fn build(&self, _ctx: &mut BuildCtx<'_>) -> SliverGridWidget {
404 SliverGridWidget::new(self.item_count, self.delegate)
405 }
406
407 fn rebuild(
408 &self,
409 prev: &Self,
410 element: &mut SliverGridWidget,
411 ctx: &mut BuildCtx<'_>,
412 ) -> ChangeFlags {
413 let mut flags = ChangeFlags::NONE;
414 if element.item_count != self.item_count {
415 element.item_count = self.item_count;
416 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
417 }
418 if element.delegate != self.delegate {
419 element.delegate = self.delegate;
420 flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
421 }
422 let range = element.desired_window();
423 let views = ItemViews {
424 prev: &*prev.builder,
425 next: &*self.builder,
426 };
427 let rows = LiveRows {
428 children: &mut element.children,
429 indices: &mut element.indices,
430 };
431 flags |= match self.key_of.as_ref() {
432 Some(key_of) => {
433 reconcile::reconcile_keyed(
434 views,
435 rows,
436 &mut element.key_index,
437 &**key_of,
438 range,
439 DUPLICATE_KEY_MSG,
440 ctx,
441 )
442 .flags
443 }
444 None => {
445 element.key_index.clear();
447 reconcile::reconcile_positional(views, rows, range, ctx)
448 }
449 };
450 flags
451 }
452
453 fn teardown(&self, element: &mut SliverGridWidget, ctx: &mut BuildCtx<'_>) {
454 for (index, pod) in element.indices.iter().zip(element.children.iter_mut()) {
455 crate::authoring::teardown_child(&(self.builder)(*index), pod, ctx);
456 }
457 }
458}
459
460impl Widget for SliverGridWidget {
461 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
465 let axis = self.constraints.map_or(ScrollAxis::Vertical, |sc| sc.axis);
466 let max = bc.max();
467 let main = non_negative(axis.main_extent(max));
468 let cross = finite_extent(axis.cross_extent(max));
469 let sc = SliverConstraints {
470 axis,
471 scroll_offset: 0.0,
472 preceding_scroll_extent: 0.0,
473 overlap: 0.0,
474 remaining_paint_extent: main,
475 cross_axis_extent: cross,
476 viewport_main_axis_extent: main,
477 cache_origin: 0.0,
478 remaining_cache_extent: main,
479 user_scroll_direction: ScrollDirection::Idle,
480 };
481 let geometry = self.layout_sliver(ctx, &sc);
482 bc.constrain(axis.size(geometry.paint_extent, cross))
483 }
484
485 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
486 if self.clip {
487 scene.push_clip(ctx.origin(), ctx.size());
488 }
489 let axis = self.constraints.map_or(ScrollAxis::Vertical, |sc| sc.axis);
490 let cell = self.grid.map_or(0.0, |grid| grid.cell_main);
491 let paint_extent = self.paint_extent;
492 for pod in &mut self.children {
493 let origin = pod.origin();
495 let start = axis.main_extent(Size::new(origin.x, origin.y));
496 if start + cell > 0.0 && start < paint_extent {
497 pod.paint_child(ctx, scene);
498 }
499 }
500 if self.clip {
501 scene.pop_clip();
502 }
503 if !self.covers(&self.desired_window()) {
505 ctx.request_frame();
506 }
507 }
508
509 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
512 crate::authoring::route_event(&mut self.children, ctx, event)
513 }
514
515 fn semantics(&self, ctx: &mut SemanticsCtx) {
516 for pod in &self.children {
517 pod.semantics_child(ctx);
518 }
519 }
520
521 fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
522 Some(self)
523 }
524
525 crate::authoring::visit_children!(children);
526}
527
528impl SliverWidget for SliverGridWidget {
529 fn layout_sliver(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry {
530 let axis = sc.axis;
531 let grid = self.delegate.layout(sc.cross_axis_extent);
532 let cell = BoxConstraints::tight(axis.size(grid.cell_main, grid.cell_cross));
533 for (index, pod) in self.indices.iter().zip(self.children.iter_mut()) {
534 let (row, column) = (index / grid.count, index % grid.count);
535 pod.layout_child(ctx, &cell);
536 pod.set_origin(axis_point(
537 axis,
538 row as f64 * grid.main_stride - sc.scroll_offset,
539 column as f64 * grid.cross_stride,
540 ));
541 }
542 let total = grid.extent(self.item_count);
543 let paint_extent = paint_offset(sc, 0.0, total);
544 let geometry = SliverGeometry {
545 cache_extent: cache_offset(sc, 0.0, total).max(paint_extent),
546 has_visual_overflow: total > sc.scroll_offset + sc.remaining_paint_extent
547 || sc.scroll_offset > 0.0,
548 ..SliverGeometry::new(total, paint_extent, total)
549 };
550 self.constraints = Some(*sc);
551 self.grid = Some(grid);
552 self.paint_extent = paint_extent;
553 self.clip = geometry.has_visual_overflow;
554 geometry
555 }
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 fn constraints(scroll_offset: f64, cross: f64) -> SliverConstraints {
563 SliverConstraints {
564 axis: ScrollAxis::Vertical,
565 scroll_offset,
566 preceding_scroll_extent: 0.0,
567 overlap: 0.0,
568 remaining_paint_extent: 600.0,
569 cross_axis_extent: cross,
570 viewport_main_axis_extent: 600.0,
571 cache_origin: 0.0,
572 remaining_cache_extent: 600.0,
573 user_scroll_direction: ScrollDirection::Idle,
574 }
575 }
576
577 #[test]
578 fn both_delegates_resolve_the_same_four_column_layout() {
579 let fixed = SliverGridDelegate::FixedCrossAxisCount {
580 count: 4,
581 main_spacing: 10.0,
582 cross_spacing: 8.0,
583 child_aspect_ratio: 2.0,
584 };
585 let max = SliverGridDelegate::MaxCrossAxisExtent {
586 max_cross_extent: 120.0,
587 main_spacing: 10.0,
588 cross_spacing: 8.0,
589 child_aspect_ratio: 2.0,
590 };
591 let expected = GridLayout {
592 count: 4,
593 cell_main: 50.0,
594 cell_cross: 100.0,
595 main_stride: 60.0,
596 cross_stride: 108.0,
597 main_spacing: 10.0,
598 };
599 assert_eq!(fixed.layout(424.0), expected);
600 assert_eq!(max.layout(424.0), expected);
601 assert_eq!(expected.extent(10), 170.0);
603 assert_eq!(expected.extent(0), 0.0);
604 assert_eq!(max.layout(50.0).count, 1);
606 }
607
608 #[test]
609 fn the_window_is_the_whole_rows_the_cache_region_touches() {
610 let grid = SliverGridDelegate::FixedCrossAxisCount {
611 count: 4,
612 main_spacing: 10.0,
613 cross_spacing: 8.0,
614 child_aspect_ratio: 2.0,
615 }
616 .layout(424.0);
617 assert_eq!(grid.cache_window(&constraints(0.0, 424.0), 1000), 0..40);
619 assert_eq!(
621 grid.cache_window(&constraints(6000.0, 424.0), 1000),
622 400..440
623 );
624 assert_eq!(grid.cache_window(&constraints(30.0, 424.0), 1000), 0..44);
626 assert_eq!(grid.cache_window(&constraints(0.0, 424.0), 10), 0..10);
627 assert!(grid.cache_window(&constraints(0.0, 424.0), 0).is_empty());
628 }
629
630 #[test]
631 #[should_panic(expected = "count must be at least 1")]
632 fn a_zero_column_delegate_is_refused() {
633 let _ = sliver_grid::<(), _>(10, |_| crate::text("cell")).delegate(
634 SliverGridDelegate::FixedCrossAxisCount {
635 count: 0,
636 main_spacing: 0.0,
637 cross_spacing: 0.0,
638 child_aspect_ratio: 1.0,
639 },
640 );
641 }
642}