1use std::{hash::Hash, ops::Range, sync::Arc};
7
8use tessera_ui::{
9 AxisConstraint, CallbackWith, Color, ComputedData, Constraint, Dp, FocusDirection,
10 LayoutResult, MeasurementError, Modifier, ParentConstraint, Px, PxPosition, RenderSlotWith,
11 State, key,
12 layout::{MeasureScope, PlacementResult, PlacementScope, layout},
13 modifier::FocusModifierExt as _,
14 remember, tessera,
15};
16
17use crate::{
18 alignment::{CrossAxisAlignment, MainAxisAlignment},
19 layout_flex::{cross_axis_child_constraint, px_mul},
20 lazy_grid::{GridCells, GridSlots, compute_cell_offset, resolve_grid_slots},
21 lazy_shared::{
22 LazyItemsPlan as LazySlotPlan, LazyItemsSlot as LazySlot, MeasuredMain, ZeroLayout,
23 clamp_reported_main, hash_key,
24 },
25 scrollable::{
26 ScrollBarBehavior, ScrollBarLayout, ScrollableController, scrollable,
27 synchronize_scroll_density,
28 },
29};
30
31const DEFAULT_VIEWPORT_ITEMS: usize = 8;
32
33pub type StaggeredGridCells = GridCells;
35
36pub struct LazyStaggeredGridController {
38 cache: StaggeredGridCache,
39}
40
41impl Default for LazyStaggeredGridController {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl LazyStaggeredGridController {
48 pub fn new() -> Self {
50 Self {
51 cache: StaggeredGridCache::default(),
52 }
53 }
54}
55
56#[derive(Clone, Default, PartialEq)]
58pub struct LazyStaggeredGridContent {
59 slots: Vec<LazySlot>,
60}
61
62impl LazyStaggeredGridContent {
63 fn push(&mut self, slot: LazySlot) {
64 self.slots.push(slot);
65 }
66}
67
68#[tessera]
136pub fn lazy_vertical_staggered_grid(
137 modifier: Option<Modifier>,
138 scroll_smoothing: Option<f32>,
139 scrollbar_behavior: Option<ScrollBarBehavior>,
140 scrollbar_track_color: Option<Color>,
141 scrollbar_thumb_color: Option<Color>,
142 scrollbar_thumb_hover_color: Option<Color>,
143 scrollbar_layout: Option<ScrollBarLayout>,
144 columns: Option<StaggeredGridCells>,
145 main_axis_spacing: Option<Dp>,
146 cross_axis_spacing: Option<Dp>,
147 cross_axis_alignment: Option<MainAxisAlignment>,
148 item_alignment: Option<CrossAxisAlignment>,
149 overscan: Option<usize>,
150 estimated_item_size: Option<Dp>,
151 content_padding: Option<Dp>,
152 max_viewport_main: Option<Px>,
153 controller: Option<State<LazyStaggeredGridController>>,
154 #[prop(skip_setter)] content: Option<LazyStaggeredGridContent>,
155) {
156 let scroll_smoothing = scroll_smoothing.unwrap_or(crate::scrollable::DEFAULT_SCROLL_SMOOTHING);
157 let scrollbar_behavior = scrollbar_behavior.unwrap_or_default();
158 let scrollbar_layout = scrollbar_layout.unwrap_or_default();
159 let columns = columns.unwrap_or_default();
160 let main_axis_spacing = main_axis_spacing.unwrap_or(Dp(0.0));
161 let cross_axis_spacing = cross_axis_spacing.unwrap_or(Dp(0.0));
162 let cross_axis_alignment = cross_axis_alignment.unwrap_or_default();
163 let item_alignment = item_alignment.unwrap_or_default();
164 let overscan = overscan.unwrap_or(0);
165 let estimated_item_size = estimated_item_size.unwrap_or(Dp(0.0));
166 let content_padding = content_padding.unwrap_or(Dp(0.0));
167 let content = content.unwrap_or_default();
168 let controller = match controller {
169 Some(value) => value,
170 None => remember(LazyStaggeredGridController::new),
171 };
172 let scroll_controller = remember(ScrollableController::default);
173 lazy_staggered_grid_slots(
174 LazyStaggeredGridSlotsArgs {
175 modifier: modifier.unwrap_or_default(),
176 scroll_smoothing,
177 scrollbar_behavior,
178 scrollbar_track_color,
179 scrollbar_thumb_color,
180 scrollbar_thumb_hover_color,
181 scrollbar_layout,
182 grid_cells: columns,
183 main_axis_spacing,
184 cross_axis_spacing,
185 cross_axis_alignment,
186 item_alignment,
187 overscan,
188 estimated_item_size,
189 content_padding,
190 max_viewport_main,
191 controller,
192 scroll_controller,
193 slots: content.slots,
194 },
195 StaggeredGridAxis::Vertical,
196 );
197}
198
199#[derive(Clone, PartialEq)]
200struct LazyStaggeredGridSlotsArgs {
201 modifier: Modifier,
202 scroll_smoothing: f32,
203 scrollbar_behavior: ScrollBarBehavior,
204 scrollbar_track_color: Option<Color>,
205 scrollbar_thumb_color: Option<Color>,
206 scrollbar_thumb_hover_color: Option<Color>,
207 scrollbar_layout: ScrollBarLayout,
208 grid_cells: StaggeredGridCells,
209 main_axis_spacing: Dp,
210 cross_axis_spacing: Dp,
211 cross_axis_alignment: MainAxisAlignment,
212 item_alignment: CrossAxisAlignment,
213 overscan: usize,
214 estimated_item_size: Dp,
215 content_padding: Dp,
216 max_viewport_main: Option<Px>,
217 controller: State<LazyStaggeredGridController>,
218 scroll_controller: State<ScrollableController>,
219 slots: Vec<LazySlot>,
220}
221
222#[tessera]
223fn lazy_staggered_grid_slots(args: LazyStaggeredGridSlotsArgs, axis: StaggeredGridAxis) {
224 let scroll_controller = args.scroll_controller;
225 let main_axis_spacing = sanitize_spacing(Px::from(args.main_axis_spacing));
226 let cross_axis_spacing = sanitize_spacing(Px::from(args.cross_axis_spacing));
227 let estimated_item_main = ensure_positive_px(Px::from(args.estimated_item_size));
228 let padding_main = sanitize_spacing(Px::from(args.content_padding));
229 let padding_cross = sanitize_spacing(Px::from(args.content_padding));
230 scrollable()
231 .modifier(args.modifier)
232 .vertical(axis == StaggeredGridAxis::Vertical)
233 .horizontal(axis == StaggeredGridAxis::Horizontal)
234 .apply_child_offset(false)
235 .scroll_smoothing(args.scroll_smoothing)
236 .scrollbar_behavior(args.scrollbar_behavior)
237 .scrollbar_layout(args.scrollbar_layout)
238 .controller(scroll_controller)
239 .scrollbar_track_color_optional(args.scrollbar_track_color)
240 .scrollbar_thumb_color_optional(args.scrollbar_thumb_color)
241 .scrollbar_thumb_hover_color_optional(args.scrollbar_thumb_hover_color)
242 .child(move || {
243 lazy_staggered_grid_view()
244 .axis(axis)
245 .grid_cells(args.grid_cells.clone())
246 .main_axis_spacing(main_axis_spacing)
247 .cross_axis_spacing(cross_axis_spacing)
248 .cross_axis_alignment(args.cross_axis_alignment)
249 .item_alignment(args.item_alignment)
250 .estimated_item_main(estimated_item_main)
251 .overscan(args.overscan)
252 .padding_main(padding_main)
253 .padding_cross(padding_cross)
254 .slots(args.slots.clone())
255 .controller(args.controller)
256 .scroll_controller(scroll_controller)
257 .max_viewport_main_optional(args.max_viewport_main);
258 });
259}
260
261#[tessera]
329pub fn lazy_horizontal_staggered_grid(
330 modifier: Option<Modifier>,
331 scroll_smoothing: Option<f32>,
332 scrollbar_behavior: Option<ScrollBarBehavior>,
333 scrollbar_track_color: Option<Color>,
334 scrollbar_thumb_color: Option<Color>,
335 scrollbar_thumb_hover_color: Option<Color>,
336 scrollbar_layout: Option<ScrollBarLayout>,
337 rows: Option<StaggeredGridCells>,
338 main_axis_spacing: Option<Dp>,
339 cross_axis_spacing: Option<Dp>,
340 cross_axis_alignment: Option<MainAxisAlignment>,
341 item_alignment: Option<CrossAxisAlignment>,
342 overscan: Option<usize>,
343 estimated_item_size: Option<Dp>,
344 content_padding: Option<Dp>,
345 max_viewport_main: Option<Px>,
346 controller: Option<State<LazyStaggeredGridController>>,
347 #[prop(skip_setter)] content: Option<LazyStaggeredGridContent>,
348) {
349 let scroll_smoothing = scroll_smoothing.unwrap_or(crate::scrollable::DEFAULT_SCROLL_SMOOTHING);
350 let scrollbar_behavior = scrollbar_behavior.unwrap_or_default();
351 let scrollbar_layout = scrollbar_layout.unwrap_or_default();
352 let rows = rows.unwrap_or_default();
353 let main_axis_spacing = main_axis_spacing.unwrap_or(Dp(0.0));
354 let cross_axis_spacing = cross_axis_spacing.unwrap_or(Dp(0.0));
355 let cross_axis_alignment = cross_axis_alignment.unwrap_or_default();
356 let item_alignment = item_alignment.unwrap_or_default();
357 let overscan = overscan.unwrap_or(0);
358 let estimated_item_size = estimated_item_size.unwrap_or(Dp(0.0));
359 let content_padding = content_padding.unwrap_or(Dp(0.0));
360 let content = content.unwrap_or_default();
361 let controller = match controller {
362 Some(value) => value,
363 None => remember(LazyStaggeredGridController::new),
364 };
365 let scroll_controller = remember(ScrollableController::default);
366 lazy_staggered_grid_slots(
367 LazyStaggeredGridSlotsArgs {
368 modifier: modifier.unwrap_or_default(),
369 scroll_smoothing,
370 scrollbar_behavior,
371 scrollbar_track_color,
372 scrollbar_thumb_color,
373 scrollbar_thumb_hover_color,
374 scrollbar_layout,
375 grid_cells: rows,
376 main_axis_spacing,
377 cross_axis_spacing,
378 cross_axis_alignment,
379 item_alignment,
380 overscan,
381 estimated_item_size,
382 content_padding,
383 max_viewport_main,
384 controller,
385 scroll_controller,
386 slots: content.slots,
387 },
388 StaggeredGridAxis::Horizontal,
389 );
390}
391
392crate::lazy_shared::impl_lazy_builder!(LazyVerticalStaggeredGridBuilder, LazyStaggeredGridContent);
393crate::lazy_shared::impl_lazy_builder!(
394 LazyHorizontalStaggeredGridBuilder,
395 LazyStaggeredGridContent
396);
397
398#[derive(Clone, Copy, PartialEq, Eq, Default)]
399enum StaggeredGridAxis {
400 #[default]
401 Vertical,
402 Horizontal,
403}
404
405#[derive(Clone, PartialEq, Eq)]
406struct VisibleStaggeredLayoutItem {
407 item_index: usize,
408}
409
410#[derive(Clone)]
411struct LazyStaggeredGridLayout {
412 axis: StaggeredGridAxis,
413 item_alignment: CrossAxisAlignment,
414 estimated_item_main: Px,
415 main_spacing: Px,
416 max_viewport_main: Option<Px>,
417 padding_main: Px,
418 padding_cross: Px,
419 viewport_limit: Px,
420 total_count: usize,
421 slots: GridSlots,
422 visible_items: Vec<VisibleStaggeredLayoutItem>,
423 scroll_offset: Px,
424 controller: State<LazyStaggeredGridController>,
425 scroll_controller: State<ScrollableController>,
426}
427
428impl PartialEq for LazyStaggeredGridLayout {
429 fn eq(&self, other: &Self) -> bool {
430 tessera_ui::LayoutModifierNode::placement_eq(self, other)
431 }
432}
433
434impl tessera_ui::LayoutModifierNode for LazyStaggeredGridLayout {
435 fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
436 let mut result = LayoutResult::default();
437 let children = input.children();
438 if children.len() != self.visible_items.len() {
439 return Err(MeasurementError::MeasureFnFailed(
440 "Lazy staggered grid measured child count mismatch".into(),
441 ));
442 }
443
444 let (placements, total_main) = self.controller.with_mut(|c| {
445 c.cache.prepare(
446 self.total_count,
447 self.slots.len(),
448 self.estimated_item_main,
449 self.main_spacing,
450 );
451
452 let mut placements = Vec::with_capacity(self.visible_items.len());
453 for (visible, child) in self.visible_items.iter().zip(children.iter()) {
454 let index = visible.item_index;
455 let Some(lane) = c.cache.lane_of(index) else {
456 continue;
457 };
458 let lane_cross = self.slots.sizes.get(lane).copied().unwrap_or(Px::ZERO);
459 let child_constraint = self.axis.child_constraint(lane_cross, self.item_alignment);
460 let child_size = child.measure(input, &child_constraint)?;
461 let child_size = child_size.size();
462 let item_main = self.axis.main(&child_size);
463 c.cache.record_measurement(index, item_main);
464 let Some(lane) = c.cache.lane_of(index) else {
465 continue;
466 };
467 let item_start = c.cache.item_start(index);
468
469 let cell_offset = compute_cell_offset(
470 lane_cross,
471 self.axis.cross(&child_size),
472 self.item_alignment,
473 );
474 let cross_offset = self.padding_cross
475 + self.slots.positions.get(lane).copied().unwrap_or(Px::ZERO)
476 + cell_offset;
477 let position = self.axis.position(
478 item_start + self.padding_main - self.scroll_offset,
479 cross_offset,
480 );
481 placements.push((*child, position));
482 }
483
484 let total_main = c.cache.total_main();
485 Ok::<_, MeasurementError>((placements, total_main))
486 })?;
487
488 let total_main_with_padding = total_main + self.padding_main + self.padding_main;
489 let cross_with_padding = self.slots.cross_size + self.padding_cross + self.padding_cross;
490 let size = self
491 .axis
492 .pack_size(total_main_with_padding, cross_with_padding);
493 self.scroll_controller
494 .with_mut(|c| c.override_child_size(size));
495
496 let reported_main = clamp_reported_main(
497 total_main_with_padding,
498 self.viewport_limit,
499 self.axis.constraint_max(input.parent_constraint()),
500 self.max_viewport_main,
501 );
502
503 for (child, position) in placements {
504 result.place_child(child, position);
505 }
506
507 Ok(result.with_size(self.axis.pack_size(reported_main, cross_with_padding)))
508 }
509
510 fn measure_eq(&self, other: &Self) -> bool {
511 self.axis == other.axis
512 && self.item_alignment == other.item_alignment
513 && self.estimated_item_main == other.estimated_item_main
514 && self.main_spacing == other.main_spacing
515 && self.max_viewport_main == other.max_viewport_main
516 && self.padding_main == other.padding_main
517 && self.padding_cross == other.padding_cross
518 && self.viewport_limit == other.viewport_limit
519 && self.total_count == other.total_count
520 && self.slots == other.slots
521 && self.visible_items == other.visible_items
522 }
523
524 fn placement_eq(&self, other: &Self) -> bool {
525 self.measure_eq(other) && self.scroll_offset == other.scroll_offset
526 }
527
528 fn place_children(
529 &self,
530 input: &PlacementScope<'_>,
531 _inner: &PlacementResult,
532 ) -> Option<PlacementResult> {
533 let children = input.children();
534 if children.len() != self.visible_items.len() {
535 return None;
536 }
537
538 let mut result = LayoutResult::default();
539 let child_sizes: Vec<_> = children.iter().map(|child| child.size()).collect();
540 let positions = self.controller.with(|c| {
541 compute_staggered_layout_positions(ComputeStaggeredLayoutPositionsArgs {
542 axis: self.axis,
543 item_alignment: self.item_alignment,
544 cache: &c.cache,
545 slots: &self.slots,
546 padding_main: self.padding_main,
547 padding_cross: self.padding_cross,
548 visible_items: &self.visible_items,
549 child_sizes: &child_sizes,
550 scroll_offset: self.scroll_offset,
551 })
552 });
553 if positions.len() != children.len() {
554 return None;
555 }
556
557 for (child, position) in children.iter().zip(positions) {
558 result.place_child(*child, position);
559 }
560
561 Some(result.into_placement_result())
562 }
563}
564
565#[tessera]
566fn lazy_staggered_grid_view(
567 axis: Option<StaggeredGridAxis>,
568 grid_cells: Option<StaggeredGridCells>,
569 main_axis_spacing: Option<Px>,
570 cross_axis_spacing: Option<Px>,
571 cross_axis_alignment: Option<MainAxisAlignment>,
572 item_alignment: Option<CrossAxisAlignment>,
573 estimated_item_main: Option<Px>,
574 overscan: Option<usize>,
575 max_viewport_main: Option<Px>,
576 padding_main: Option<Px>,
577 padding_cross: Option<Px>,
578 controller: Option<State<LazyStaggeredGridController>>,
579 slots: Option<Vec<LazySlot>>,
580 scroll_controller: Option<State<ScrollableController>>,
581) {
582 let axis = axis.unwrap_or_default();
583 let grid_cells = grid_cells.unwrap_or_default();
584 let main_axis_spacing = main_axis_spacing.unwrap_or(Px::ZERO);
585 let cross_axis_spacing = cross_axis_spacing.unwrap_or(Px::ZERO);
586 let cross_axis_alignment = cross_axis_alignment.unwrap_or_default();
587 let item_alignment = item_alignment.unwrap_or_default();
588 let estimated_item_main = estimated_item_main.unwrap_or(Px::ZERO);
589 let overscan = overscan.unwrap_or(0);
590 let padding_main = padding_main.unwrap_or(Px::ZERO);
591 let padding_cross = padding_cross.unwrap_or(Px::ZERO);
592 let slots = slots.unwrap_or_default();
593 let controller = controller.expect("lazy_staggered_grid_view requires controller");
594 let scroll_controller =
595 scroll_controller.expect("lazy_staggered_grid_view requires scroll_controller");
596 synchronize_scroll_density(scroll_controller);
597 let plan = LazySlotPlan::new(slots.clone());
598 let total_count = plan.total_count();
599
600 let visible_size = scroll_controller.with(|s| s.visible_size());
601 let available_cross = axis.visible_cross(visible_size);
602 let available_cross = (available_cross - padding_cross * 2).max(Px::ZERO);
603 let grid_slots = resolve_grid_slots(
604 available_cross,
605 &grid_cells,
606 cross_axis_spacing,
607 cross_axis_alignment,
608 );
609 let lane_count = grid_slots.len();
610
611 controller.with_mut(|c| {
612 c.cache.prepare(
613 total_count,
614 lane_count,
615 estimated_item_main,
616 main_axis_spacing,
617 );
618 });
619 let total_main = controller.with(|c| c.cache.total_main());
620 let total_main_with_padding = total_main + padding_main + padding_main;
621 let cross_with_padding = grid_slots.cross_size + padding_cross + padding_cross;
622 scroll_controller.with_mut(|c| {
623 c.override_child_size(axis.pack_size(total_main_with_padding, cross_with_padding));
624 });
625
626 let scroll_offset = axis.scroll_offset(scroll_controller.with(|s| s.child_position()));
627 let viewport_span = resolve_viewport_span(
628 axis.visible_span(visible_size),
629 estimated_item_main,
630 main_axis_spacing,
631 );
632 let viewport_span = (viewport_span - (padding_main * 2)).max(Px::ZERO);
633
634 let visible_range = controller.with(|c| {
635 compute_visible_range(
636 &c.cache,
637 total_count,
638 lane_count,
639 scroll_offset,
640 viewport_span,
641 overscan,
642 main_axis_spacing,
643 )
644 });
645 let visible_items = visible_items(&plan, visible_range.clone());
646
647 if visible_items.is_empty() {
648 layout().policy(ZeroLayout);
649 return;
650 }
651
652 let focus_modifier =
653 lazy_staggered_grid_focus_beyond_bounds_modifier(LazyStaggeredGridFocusArgs {
654 axis,
655 controller,
656 scroll_controller,
657 total_count,
658 total_main,
659 viewport_span,
660 visible_range,
661 lane_count,
662 });
663
664 let viewport_limit = viewport_span + padding_main + padding_main;
665 let visible_layout_items = visible_items
666 .iter()
667 .map(|item| VisibleStaggeredLayoutItem {
668 item_index: item.item_index,
669 })
670 .collect();
671
672 layout()
673 .modifier(focus_modifier)
674 .policy(LazyStaggeredGridLayout {
675 axis,
676 item_alignment,
677 estimated_item_main,
678 main_spacing: main_axis_spacing,
679 max_viewport_main,
680 padding_main,
681 padding_cross,
682 viewport_limit,
683 total_count,
684 slots: grid_slots.clone(),
685 visible_items: visible_layout_items,
686 scroll_offset,
687 controller,
688 scroll_controller,
689 })
690 .child(move || {
691 for child in &visible_items {
692 let child = child.clone();
693 key(child.key_hash, || {
694 lazy_staggered_grid_item_host()
695 .builder_handle_shared(child.builder)
696 .local_index(child.local_index);
697 });
698 }
699 });
700}
701
702#[tessera]
703fn lazy_staggered_grid_item_host(
704 builder_handle: Option<RenderSlotWith<usize>>,
705 local_index: Option<usize>,
706) {
707 let local_index = local_index.unwrap_or(0);
708 if let Some(builder_handle) = builder_handle {
709 builder_handle.render(local_index);
710 }
711}
712
713fn resolve_viewport_span(current: Px, estimated: Px, spacing: Px) -> Px {
714 if current > Px::ZERO {
715 current
716 } else {
717 let per_item = estimated + spacing;
718 px_mul(per_item, DEFAULT_VIEWPORT_ITEMS.max(1))
719 }
720}
721
722fn compute_visible_range(
723 cache: &StaggeredGridCache,
724 total_count: usize,
725 lane_count: usize,
726 scroll_offset: Px,
727 viewport_span: Px,
728 overscan: usize,
729 spacing: Px,
730) -> Range<usize> {
731 if total_count == 0 || lane_count == 0 || cache.built_len() != total_count {
732 return 0..0;
733 }
734
735 let viewport_start = scroll_offset;
736 let viewport_end = scroll_offset + viewport_span;
737 let mut first_visible = None;
738 let mut last_visible = None;
739
740 for lane in 0..lane_count {
741 let ends = cache.lane_ends(lane);
742 if ends.is_empty() {
743 continue;
744 }
745 let first = ends.partition_point(|end| *end < viewport_start);
746 let last = if viewport_end < Px::ZERO {
747 0
748 } else {
749 (ends.partition_point(|end| *end + spacing <= viewport_end) + 1).min(ends.len())
750 };
751 if first >= last {
752 continue;
753 }
754 if let Some(start_index) = cache.item_at_lane_slot(lane, first) {
755 first_visible =
756 Some(first_visible.map_or(start_index, |current: usize| current.min(start_index)));
757 }
758 if let Some(end_index) = cache.item_at_lane_slot(lane, last - 1) {
759 last_visible =
760 Some(last_visible.map_or(end_index, |current: usize| current.max(end_index)));
761 }
762 }
763
764 let (mut start, mut end) = match (first_visible, last_visible) {
765 (Some(start), Some(end)) => (start, end + 1),
766 _ => {
767 let end = total_count;
768 let start = total_count.saturating_sub(1);
769 (start, end)
770 }
771 };
772
773 start = start.saturating_sub(overscan);
774 end = end.saturating_add(overscan).min(total_count);
775 if start >= end {
776 end = (start + 1).min(total_count);
777 start = start.saturating_sub(1);
778 }
779
780 start..end
781}
782
783#[derive(Clone, Copy)]
784enum FocusScrollDirection {
785 Forward,
786 Backward,
787}
788
789impl StaggeredGridAxis {
790 fn main(&self, size: &ComputedData) -> Px {
791 match self {
792 Self::Vertical => size.height,
793 Self::Horizontal => size.width,
794 }
795 }
796
797 fn cross(&self, size: &ComputedData) -> Px {
798 match self {
799 Self::Vertical => size.width,
800 Self::Horizontal => size.height,
801 }
802 }
803
804 fn pack_size(&self, main: Px, cross: Px) -> ComputedData {
805 match self {
806 Self::Vertical => ComputedData {
807 width: cross,
808 height: main,
809 },
810 Self::Horizontal => ComputedData {
811 width: main,
812 height: cross,
813 },
814 }
815 }
816
817 fn position(&self, main: Px, cross: Px) -> PxPosition {
818 match self {
819 Self::Vertical => PxPosition::new(cross, main),
820 Self::Horizontal => PxPosition::new(main, cross),
821 }
822 }
823
824 fn visible_span(&self, size: ComputedData) -> Px {
825 match self {
826 Self::Vertical => size.height,
827 Self::Horizontal => size.width,
828 }
829 }
830
831 fn visible_cross(&self, size: ComputedData) -> Px {
832 match self {
833 Self::Vertical => size.width,
834 Self::Horizontal => size.height,
835 }
836 }
837
838 fn scroll_position(&self, offset: Px) -> PxPosition {
839 match self {
840 Self::Vertical => PxPosition::new(Px::ZERO, -offset),
841 Self::Horizontal => PxPosition::new(-offset, Px::ZERO),
842 }
843 }
844
845 fn scroll_offset(&self, position: PxPosition) -> Px {
846 match self {
847 Self::Vertical => Px((-position.y).max(0.0) as i32),
848 Self::Horizontal => Px((-position.x).max(0.0) as i32),
849 }
850 }
851
852 fn focus_scroll_direction(&self, direction: FocusDirection) -> Option<FocusScrollDirection> {
853 match (self, direction) {
854 (_, FocusDirection::Next | FocusDirection::Enter) => {
855 Some(FocusScrollDirection::Forward)
856 }
857 (_, FocusDirection::Previous | FocusDirection::Exit) => {
858 Some(FocusScrollDirection::Backward)
859 }
860 (Self::Vertical, FocusDirection::Down) => Some(FocusScrollDirection::Forward),
861 (Self::Vertical, FocusDirection::Up) => Some(FocusScrollDirection::Backward),
862 (Self::Horizontal, FocusDirection::Right) => Some(FocusScrollDirection::Forward),
863 (Self::Horizontal, FocusDirection::Left) => Some(FocusScrollDirection::Backward),
864 _ => None,
865 }
866 }
867
868 fn child_constraint(&self, cross: Px, alignment: CrossAxisAlignment) -> Constraint {
869 let cross = cross.max(Px::ZERO);
870 let cross_constraint = cross_axis_child_constraint(AxisConstraint::exact(cross), alignment);
874
875 match self {
876 Self::Vertical => Constraint::new(cross_constraint, AxisConstraint::NONE),
877 Self::Horizontal => Constraint::new(AxisConstraint::NONE, cross_constraint),
878 }
879 }
880
881 fn constraint_max(&self, constraint: ParentConstraint<'_>) -> Option<Px> {
882 match self {
883 Self::Vertical => constraint.height().resolve_max(),
884 Self::Horizontal => constraint.width().resolve_max(),
885 }
886 }
887}
888
889fn visible_items(plan: &LazySlotPlan, range: Range<usize>) -> Vec<VisibleStaggeredItem> {
890 let mut result = Vec::new();
891 for index in range {
892 if let Some((slot, local_index)) = plan.resolve(index) {
893 let key_hash = if let Some(provider) = &slot.key_provider {
894 provider.call(local_index)
895 } else {
896 hash_key(index)
897 };
898
899 result.push(VisibleStaggeredItem {
900 item_index: index,
901 local_index,
902 builder: slot.builder,
903 key_hash,
904 });
905 }
906 }
907 result
908}
909
910#[derive(Clone, PartialEq)]
911struct VisibleStaggeredItem {
912 item_index: usize,
913 local_index: usize,
914 builder: RenderSlotWith<usize>,
915 key_hash: u64,
916}
917
918struct LazyStaggeredGridFocusArgs {
919 axis: StaggeredGridAxis,
920 controller: State<LazyStaggeredGridController>,
921 scroll_controller: State<ScrollableController>,
922 total_count: usize,
923 total_main: Px,
924 viewport_span: Px,
925 visible_range: Range<usize>,
926 lane_count: usize,
927}
928
929fn lazy_staggered_grid_focus_beyond_bounds_modifier(args: LazyStaggeredGridFocusArgs) -> Modifier {
930 let current_scroll_offset = args
931 .axis
932 .scroll_offset(args.scroll_controller.with(|s| s.child_position()));
933 let max_scroll = (args.total_main - args.viewport_span).max(Px::ZERO);
934 Modifier::new().focus_beyond_bounds_handler(CallbackWith::new(move |direction| {
935 let Some(scroll_direction) = args.axis.focus_scroll_direction(direction) else {
936 return false;
937 };
938 if args.total_count == 0 || args.lane_count == 0 || args.viewport_span <= Px::ZERO {
939 return false;
940 }
941
942 let target_index = match scroll_direction {
943 FocusScrollDirection::Forward => {
944 if args.visible_range.end >= args.total_count {
945 return false;
946 }
947 args.visible_range.end
948 }
949 FocusScrollDirection::Backward => {
950 let Some(index) = args.visible_range.start.checked_sub(1) else {
951 return false;
952 };
953 index
954 }
955 };
956
957 let Some((target_offset, target_main)) =
958 args.controller.with(|c| c.cache.item_layout(target_index))
959 else {
960 return false;
961 };
962
963 let desired_scroll = match scroll_direction {
964 FocusScrollDirection::Forward => {
965 (target_offset + target_main - args.viewport_span).max(Px::ZERO)
966 }
967 FocusScrollDirection::Backward => target_offset,
968 }
969 .min(max_scroll);
970
971 if desired_scroll == current_scroll_offset {
972 return false;
973 }
974
975 let position = args.axis.scroll_position(desired_scroll);
976 args.scroll_controller
977 .with_mut(|c| c.set_scroll_position(position));
978 true
979 }))
980}
981
982#[derive(PartialEq)]
983struct StaggeredGridCache {
984 item_main: Vec<Option<MeasuredMain>>,
986 density: f64,
987 lane_of: Vec<usize>,
989 lane_ends: Vec<Vec<Px>>,
991 lane_items: Vec<Vec<usize>>,
993 lane_count: usize,
994 spacing: Px,
995 estimated_item_main: Px,
996}
997
998impl Default for StaggeredGridCache {
999 fn default() -> Self {
1000 Self {
1001 item_main: Vec::new(),
1002 density: 0.0,
1003 lane_of: Vec::new(),
1004 lane_ends: Vec::new(),
1005 lane_items: Vec::new(),
1006 lane_count: 0,
1007 spacing: Px::ZERO,
1008 estimated_item_main: Px::ZERO,
1009 }
1010 }
1011}
1012
1013impl StaggeredGridCache {
1014 fn prepare(&mut self, count: usize, lane_count: usize, estimated_item_main: Px, spacing: Px) {
1021 let density = tessera_ui::dp::scale_factor();
1022 if self.density != density {
1023 for measurement in self.item_main.iter_mut().flatten() {
1024 measurement.rebase(density);
1025 }
1026 self.density = density;
1027 self.invalidate_geometry();
1028 }
1029 if self.lane_count != lane_count
1030 || self.spacing != spacing
1031 || self.estimated_item_main != estimated_item_main
1032 {
1033 self.invalidate_geometry();
1034 self.lane_count = lane_count;
1035 self.spacing = spacing;
1036 self.estimated_item_main = estimated_item_main;
1037 }
1038
1039 if self.item_main.len() > count {
1040 self.item_main.truncate(count);
1041 } else if self.item_main.len() < count {
1042 self.item_main.resize(count, None);
1043 }
1044
1045 if self.lane_of.len() > self.item_main.len() {
1046 self.truncate_geometry(self.item_main.len());
1047 }
1048
1049 self.ensure_built(self.item_main.len());
1050 }
1051
1052 fn item_main(&self, index: usize) -> Option<Px> {
1053 self.item_main
1054 .get(index)
1055 .copied()
1056 .flatten()
1057 .map(MeasuredMain::pixels)
1058 }
1059
1060 fn record_measurement(&mut self, index: usize, actual: Px) {
1061 if index >= self.item_main.len() {
1062 return;
1063 }
1064 if self.item_main(index) == Some(actual) {
1065 return;
1066 }
1067 self.item_main[index] = Some(MeasuredMain::new(actual, self.density));
1068 self.truncate_geometry(index);
1069 self.ensure_built(self.item_main.len());
1070 }
1071
1072 fn built_len(&self) -> usize {
1073 self.lane_of.len()
1074 }
1075
1076 fn lane_of(&self, index: usize) -> Option<usize> {
1077 self.lane_of.get(index).copied()
1078 }
1079
1080 fn lane_ends(&self, lane: usize) -> &[Px] {
1081 self.lane_ends.get(lane).map(Vec::as_slice).unwrap_or(&[])
1082 }
1083
1084 fn item_start(&self, index: usize) -> Px {
1085 let Some(lane) = self.lane_of.get(index).copied() else {
1086 return Px::ZERO;
1087 };
1088 let Some(items) = self.lane_items.get(lane) else {
1089 return Px::ZERO;
1090 };
1091 let Some(slot) = items.iter().position(|item| *item == index) else {
1092 return Px::ZERO;
1093 };
1094 if slot == 0 {
1095 Px::ZERO
1096 } else {
1097 self.lane_ends
1098 .get(lane)
1099 .and_then(|ends| ends.get(slot - 1))
1100 .copied()
1101 .unwrap_or(Px::ZERO)
1102 + self.spacing
1103 }
1104 }
1105
1106 fn item_layout(&self, index: usize) -> Option<(Px, Px)> {
1107 if index >= self.lane_of.len() {
1108 return None;
1109 }
1110 Some((self.item_start(index), self.resolved_main(index)))
1111 }
1112
1113 fn item_at_lane_slot(&self, lane: usize, slot: usize) -> Option<usize> {
1114 self.lane_items.get(lane)?.get(slot).copied()
1115 }
1116
1117 fn total_main(&self) -> Px {
1118 if self.lane_count == 0
1119 || self.item_main.is_empty()
1120 || self.lane_of.len() != self.item_main.len()
1121 {
1122 return Px::ZERO;
1123 }
1124 self.lane_ends
1125 .iter()
1126 .filter_map(|ends| ends.last().copied())
1127 .max()
1128 .unwrap_or(Px::ZERO)
1129 }
1130
1131 fn resolved_main(&self, index: usize) -> Px {
1132 self.item_main(index).unwrap_or(self.estimated_item_main)
1133 }
1134
1135 fn invalidate_geometry(&mut self) {
1136 self.lane_of.clear();
1137 self.lane_ends.clear();
1138 self.lane_items.clear();
1139 }
1140
1141 fn truncate_geometry(&mut self, from: usize) {
1143 let keep = from.min(self.lane_of.len());
1144 if keep == self.lane_of.len() && self.lane_ends.len() == self.lane_count {
1145 return;
1146 }
1147 self.lane_of.truncate(keep);
1148 for items in &mut self.lane_items {
1149 items.retain(|index| *index < keep);
1150 }
1151 let spacing = self.spacing;
1152 let mains: Vec<Px> = (0..keep).map(|index| self.resolved_main(index)).collect();
1153 for lane in 0..self.lane_items.len() {
1154 let Some(items) = self.lane_items.get(lane).cloned() else {
1155 continue;
1156 };
1157 let mut cursor = Px::ZERO;
1158 let mut rebuilt = Vec::with_capacity(items.len());
1159 for (slot, index) in items.iter().copied().enumerate() {
1160 let main = mains.get(index).copied().unwrap_or(Px::ZERO);
1161 if slot > 0 {
1162 cursor += spacing;
1163 }
1164 cursor += main;
1165 rebuilt.push(cursor);
1166 }
1167 if let Some(ends) = self.lane_ends.get_mut(lane) {
1168 *ends = rebuilt;
1169 }
1170 }
1171 }
1172
1173 fn ensure_built(&mut self, through: usize) {
1174 let through = through.min(self.item_main.len());
1175 if self.lane_count == 0 {
1176 self.lane_of.clear();
1177 self.lane_ends.clear();
1178 self.lane_items.clear();
1179 return;
1180 }
1181 if self.lane_ends.len() != self.lane_count || self.lane_items.len() != self.lane_count {
1182 self.invalidate_geometry();
1183 self.lane_ends = vec![Vec::new(); self.lane_count];
1184 self.lane_items = vec![Vec::new(); self.lane_count];
1185 }
1186 if self.lane_of.len() > through {
1187 self.truncate_geometry(through);
1188 }
1189 while self.lane_of.len() < through {
1190 let index = self.lane_of.len();
1191 let lane = shortest_lane_index(&self.lane_ends);
1192 let item_main = self.resolved_main(index);
1193 let start = self
1194 .lane_ends
1195 .get(lane)
1196 .and_then(|ends| ends.last().copied())
1197 .map(|end| end + self.spacing)
1198 .unwrap_or(Px::ZERO);
1199 self.lane_of.push(lane);
1200 if let Some(items) = self.lane_items.get_mut(lane) {
1201 items.push(index);
1202 }
1203 if let Some(ends) = self.lane_ends.get_mut(lane) {
1204 ends.push(start + item_main);
1205 }
1206 }
1207 }
1208}
1209
1210fn shortest_lane_index(lane_ends: &[Vec<Px>]) -> usize {
1211 let mut index = 0;
1212 let mut best = lane_ends
1213 .first()
1214 .and_then(|ends| ends.last().copied())
1215 .unwrap_or(Px::ZERO);
1216 for (lane, ends) in lane_ends.iter().enumerate().skip(1) {
1217 let end = ends.last().copied().unwrap_or(Px::ZERO);
1218 if end < best {
1219 best = end;
1220 index = lane;
1221 }
1222 }
1223 index
1224}
1225
1226fn ensure_positive_px(px: Px) -> Px {
1227 if px <= Px::ZERO { Px(1) } else { px }
1228}
1229
1230fn sanitize_spacing(px: Px) -> Px {
1231 if px < Px::ZERO { Px::ZERO } else { px }
1232}
1233
1234struct ComputeStaggeredLayoutPositionsArgs<'a> {
1235 axis: StaggeredGridAxis,
1236 item_alignment: CrossAxisAlignment,
1237 cache: &'a StaggeredGridCache,
1238 slots: &'a GridSlots,
1239 padding_main: Px,
1240 padding_cross: Px,
1241 visible_items: &'a [VisibleStaggeredLayoutItem],
1242 child_sizes: &'a [ComputedData],
1243 scroll_offset: Px,
1244}
1245
1246fn compute_staggered_layout_positions(
1247 args: ComputeStaggeredLayoutPositionsArgs<'_>,
1248) -> Vec<PxPosition> {
1249 let ComputeStaggeredLayoutPositionsArgs {
1250 axis,
1251 item_alignment,
1252 cache,
1253 slots,
1254 padding_main,
1255 padding_cross,
1256 visible_items,
1257 child_sizes,
1258 scroll_offset,
1259 } = args;
1260
1261 let mut positions = Vec::with_capacity(visible_items.len());
1262 if slots.len() == 0 {
1263 return positions;
1264 }
1265
1266 for (visible, child_size) in visible_items.iter().zip(child_sizes.iter()) {
1267 let index = visible.item_index;
1268 let Some(lane) = cache.lane_of(index) else {
1269 continue;
1270 };
1271 let lane_cross = slots.sizes.get(lane).copied().unwrap_or(Px::ZERO);
1272 let item_start = cache.item_start(index);
1273 let cell_offset = compute_cell_offset(lane_cross, axis.cross(child_size), item_alignment);
1274 let cross_offset =
1275 padding_cross + slots.positions.get(lane).copied().unwrap_or(Px::ZERO) + cell_offset;
1276 positions.push(axis.position(item_start + padding_main - scroll_offset, cross_offset));
1277 }
1278
1279 positions
1280}
1281
1282#[cfg(test)]
1283mod tests {
1284 use tessera_ui::{
1285 AxisConstraint, ComputedData, LayoutResult, MeasurementError, Modifier, NoopDrawModifier,
1286 Px, PxPosition, RenderSlotWith,
1287 layout::{MeasureScope, layout},
1288 receive_frame_nanos, remember, tessera,
1289 };
1290
1291 use crate::{
1292 alignment::{CrossAxisAlignment, MainAxisAlignment},
1293 modifier::{ModifierExt as _, SemanticsArgs},
1294 scrollable::{ScrollBarBehavior, ScrollableController, scrollable},
1295 theme::{MaterialTheme, material_theme},
1296 };
1297
1298 use super::{
1299 GridCells, LazyStaggeredGridController, StaggeredGridAxis, lazy_staggered_grid_view,
1300 };
1301
1302 #[derive(Clone, PartialEq)]
1303 struct FixedTestLayout {
1304 width: i32,
1305 height: i32,
1306 }
1307
1308 impl tessera_ui::LayoutModifierNode for FixedTestLayout {
1309 fn measure(&self, _input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
1310 Ok(LayoutResult::new(ComputedData {
1311 width: Px::new(self.width),
1312 height: Px::new(self.height),
1313 }))
1314 }
1315 }
1316
1317 #[tessera]
1318 fn fixed_test_box(tag: Option<String>, width: Option<i32>, height: Option<i32>) {
1319 let tag = tag.unwrap_or_default();
1320 let width = width.unwrap_or(0);
1321 let height = height.unwrap_or(0);
1322
1323 layout()
1324 .policy(FixedTestLayout { width, height })
1325 .draw(NoopDrawModifier)
1326 .modifier(Modifier::new().semantics(SemanticsArgs {
1327 test_tag: Some(tag),
1328 ..Default::default()
1329 }));
1330 }
1331
1332 #[tessera]
1333 fn lazy_vertical_staggered_layout_case() {
1334 let controller = remember(LazyStaggeredGridController::new);
1335 let scroll_controller = remember(ScrollableController::default);
1336 scroll_controller.with_mut(|c| {
1337 c.set_visible_size_for_test(ComputedData {
1338 width: Px::new(60),
1339 height: Px::new(60),
1340 });
1341 });
1342
1343 lazy_staggered_grid_view()
1344 .axis(StaggeredGridAxis::Vertical)
1345 .grid_cells(GridCells::fixed(2))
1346 .main_axis_spacing(Px::new(3))
1347 .cross_axis_spacing(Px::new(3))
1348 .cross_axis_alignment(MainAxisAlignment::Start)
1349 .item_alignment(CrossAxisAlignment::Start)
1350 .estimated_item_main(Px::new(10))
1351 .overscan(0)
1352 .padding_main(Px::new(4))
1353 .padding_cross(Px::new(4))
1354 .slots(vec![super::LazySlot::items(
1355 4,
1356 RenderSlotWith::new(|index| match index {
1357 0 => {
1358 fixed_test_box()
1359 .tag("lazy_staggered_v_first".to_string())
1360 .width(10)
1361 .height(10);
1362 }
1363 1 => {
1364 fixed_test_box()
1365 .tag("lazy_staggered_v_second".to_string())
1366 .width(12)
1367 .height(8);
1368 }
1369 2 => {
1370 fixed_test_box()
1371 .tag("lazy_staggered_v_third".to_string())
1372 .width(11)
1373 .height(12);
1374 }
1375 _ => {
1376 fixed_test_box()
1377 .tag("lazy_staggered_v_fourth".to_string())
1378 .width(9)
1379 .height(9);
1380 }
1381 }),
1382 None,
1383 )])
1384 .controller(controller)
1385 .scroll_controller(scroll_controller);
1386 }
1387
1388 #[tessera]
1389 fn lazy_horizontal_staggered_layout_case() {
1390 let controller = remember(LazyStaggeredGridController::new);
1391 let scroll_controller = remember(ScrollableController::default);
1392 scroll_controller.with_mut(|c| {
1393 c.set_visible_size_for_test(ComputedData {
1394 width: Px::new(70),
1395 height: Px::new(40),
1396 });
1397 });
1398
1399 lazy_staggered_grid_view()
1400 .axis(StaggeredGridAxis::Horizontal)
1401 .grid_cells(GridCells::fixed(2))
1402 .main_axis_spacing(Px::new(3))
1403 .cross_axis_spacing(Px::new(3))
1404 .cross_axis_alignment(MainAxisAlignment::Start)
1405 .item_alignment(CrossAxisAlignment::Start)
1406 .estimated_item_main(Px::new(10))
1407 .overscan(0)
1408 .padding_main(Px::new(4))
1409 .padding_cross(Px::new(4))
1410 .slots(vec![super::LazySlot::items(
1411 4,
1412 RenderSlotWith::new(|index| match index {
1413 0 => {
1414 fixed_test_box()
1415 .tag("lazy_staggered_h_first".to_string())
1416 .width(20)
1417 .height(10);
1418 }
1419 1 => {
1420 fixed_test_box()
1421 .tag("lazy_staggered_h_second".to_string())
1422 .width(15)
1423 .height(12);
1424 }
1425 2 => {
1426 fixed_test_box()
1427 .tag("lazy_staggered_h_third".to_string())
1428 .width(18)
1429 .height(8);
1430 }
1431 _ => {
1432 fixed_test_box()
1433 .tag("lazy_staggered_h_fourth".to_string())
1434 .width(16)
1435 .height(9);
1436 }
1437 }),
1438 None,
1439 )])
1440 .controller(controller)
1441 .scroll_controller(scroll_controller);
1442 }
1443
1444 #[tessera]
1445 fn lazy_vertical_staggered_scrolled_layout_case() {
1446 let controller = remember(LazyStaggeredGridController::new);
1447 let scroll_controller = remember(ScrollableController::default);
1448 let target_position = PxPosition::new(Px::ZERO, Px::new(-5));
1449 scroll_controller.with_mut(|c| {
1450 c.set_visible_size_for_test(ComputedData {
1451 width: Px::new(60),
1452 height: Px::new(20),
1453 });
1454 c.set_scroll_position(target_position);
1455 });
1456
1457 scrollable()
1458 .scrollbar_behavior(ScrollBarBehavior::Hidden)
1459 .modifier(Modifier::new().constrain(
1460 Some(AxisConstraint::exact(Px::new(60))),
1461 Some(AxisConstraint::exact(Px::new(20))),
1462 ))
1463 .vertical(true)
1464 .horizontal(false)
1465 .apply_child_offset(false)
1466 .controller(scroll_controller)
1467 .child(move || {
1468 lazy_staggered_grid_view()
1469 .axis(StaggeredGridAxis::Vertical)
1470 .grid_cells(GridCells::fixed(2))
1471 .main_axis_spacing(Px::new(3))
1472 .cross_axis_spacing(Px::new(3))
1473 .cross_axis_alignment(MainAxisAlignment::Start)
1474 .item_alignment(CrossAxisAlignment::Start)
1475 .estimated_item_main(Px::new(10))
1476 .overscan(0)
1477 .padding_main(Px::new(4))
1478 .padding_cross(Px::new(4))
1479 .slots(vec![super::LazySlot::items(
1480 4,
1481 RenderSlotWith::new(|index| match index {
1482 0 => {
1483 fixed_test_box()
1484 .tag("lazy_staggered_scroll_first".to_string())
1485 .width(10)
1486 .height(10);
1487 }
1488 1 => {
1489 fixed_test_box()
1490 .tag("lazy_staggered_scroll_second".to_string())
1491 .width(12)
1492 .height(8);
1493 }
1494 2 => {
1495 fixed_test_box()
1496 .tag("lazy_staggered_scroll_third".to_string())
1497 .width(11)
1498 .height(12);
1499 }
1500 _ => {
1501 fixed_test_box()
1502 .tag("lazy_staggered_scroll_fourth".to_string())
1503 .width(9)
1504 .height(9);
1505 }
1506 }),
1507 None,
1508 )])
1509 .controller(controller)
1510 .scroll_controller(scroll_controller);
1511 });
1512 }
1513
1514 #[tessera]
1515 fn lazy_vertical_staggered_stable_visible_items_case() {
1516 let controller = remember(LazyStaggeredGridController::new);
1517 let scroll_controller = remember(ScrollableController::default);
1518 scroll_controller.with_mut(|c| {
1519 c.set_visible_size_for_test(ComputedData {
1520 width: Px::new(60),
1521 height: Px::new(50),
1522 });
1523 });
1524
1525 receive_frame_nanos(move |frame_nanos| {
1526 let offset = match frame_nanos {
1527 0 => 0,
1528 16_666_667 => 1,
1529 33_333_334 => 2,
1530 _ => 2,
1531 };
1532 scroll_controller.with_mut(|c| {
1533 c.set_scroll_position(PxPosition::new(Px::ZERO, Px::new(-offset)));
1534 });
1535 tessera_ui::FrameNanosControl::Continue
1536 });
1537
1538 material_theme()
1539 .theme(MaterialTheme::default)
1540 .child(move || {
1541 lazy_staggered_grid_view()
1542 .axis(StaggeredGridAxis::Vertical)
1543 .grid_cells(GridCells::fixed(2))
1544 .main_axis_spacing(Px::new(3))
1545 .cross_axis_spacing(Px::new(3))
1546 .cross_axis_alignment(MainAxisAlignment::Start)
1547 .item_alignment(CrossAxisAlignment::Start)
1548 .estimated_item_main(Px::new(20))
1549 .overscan(0)
1550 .padding_main(Px::new(4))
1551 .padding_cross(Px::new(4))
1552 .slots(vec![super::LazySlot::items(
1553 20,
1554 RenderSlotWith::new(|index| {
1555 fixed_test_box()
1556 .tag(format!("lazy_staggered_stable_visible_{index}"))
1557 .width(20)
1558 .height(20);
1559 }),
1560 None,
1561 )])
1562 .controller(controller)
1563 .scroll_controller(scroll_controller);
1564 });
1565 }
1566
1567 #[test]
1568 fn lazy_vertical_staggered_positions_items_in_shortest_lanes() {
1569 tessera_ui::assert_layout! {
1570 viewport: (80, 80),
1571 content: {
1572 lazy_vertical_staggered_layout_case();
1573 },
1574 expect: {
1575 node("lazy_staggered_v_first").position(4, 4).size(10, 10);
1576 node("lazy_staggered_v_second").position(32, 4).size(12, 8);
1577 node("lazy_staggered_v_third").position(32, 15).size(11, 12);
1578 node("lazy_staggered_v_fourth").position(4, 17).size(9, 9);
1579 }
1580 }
1581 }
1582
1583 #[test]
1584 fn lazy_horizontal_staggered_positions_items_in_shortest_lanes() {
1585 tessera_ui::assert_layout! {
1586 viewport: (90, 50),
1587 content: {
1588 lazy_horizontal_staggered_layout_case();
1589 },
1590 expect: {
1591 node("lazy_staggered_h_first").position(4, 4).size(20, 10);
1592 node("lazy_staggered_h_second").position(4, 22).size(15, 12);
1593 node("lazy_staggered_h_third").position(22, 22).size(18, 8);
1594 node("lazy_staggered_h_fourth").position(27, 4).size(16, 9);
1595 }
1596 }
1597 }
1598
1599 #[test]
1600 fn lazy_vertical_staggered_scroll_repositions_visible_items() {
1601 tessera_ui::assert_layout! {
1602 viewport: (80, 80),
1603 content: {
1604 lazy_vertical_staggered_scrolled_layout_case();
1605 },
1606 expect: {
1607 node("lazy_staggered_scroll_first").position(4, -1).size(10, 10);
1608 node("lazy_staggered_scroll_second").position(32, -1).size(12, 8);
1609 node("lazy_staggered_scroll_third").position(32, 10).size(11, 12);
1610 node("lazy_staggered_scroll_fourth").position(4, 12).size(9, 9);
1611 }
1612 }
1613 }
1614
1615 #[test]
1616 fn lazy_vertical_staggered_does_not_rebuild_visible_items_when_scroll_stays_in_same_window() {
1617 let mut session = tessera_ui::testing::start_layout_test_session(
1618 tessera_ui::testing::layout_test(|| {
1619 lazy_vertical_staggered_stable_visible_items_case();
1620 })
1621 .viewport_px(80, 60),
1622 );
1623
1624 let _ = tessera_ui::testing::pump_layout_test_session(&mut session, 0);
1625 let first_builds = session
1626 .work()
1627 .count("fixed_test_box", tessera_ui::WorkPhase::Build);
1628 assert!(first_builds > 0);
1629
1630 let _ = tessera_ui::testing::pump_layout_test_session(&mut session, 16_666_667);
1631 let second_builds = session
1632 .work()
1633 .count("fixed_test_box", tessera_ui::WorkPhase::Build);
1634 assert_eq!(second_builds, 0);
1635
1636 let _ = tessera_ui::testing::pump_layout_test_session(&mut session, 33_333_334);
1637 let third_builds = session
1638 .work()
1639 .count("fixed_test_box", tessera_ui::WorkPhase::Build);
1640 assert_eq!(third_builds, 0);
1641 }
1642
1643 #[test]
1644 fn density_changes_rebase_lanes_and_keep_the_complete_final_item_extent() {
1645 let runtime = tessera_ui::RuntimeSession::new();
1646 let _scope = runtime.enter();
1647 crate::test_utils::run_isolated_test(
1648 "lazy_staggered_grid::tests::density_changes_rebase_lanes_and_keep_the_complete_final_item_extent",
1649 || {
1650 tessera_ui::dp::set_scale_factor(1.0);
1651 let mut cache = super::StaggeredGridCache::default();
1652 cache.prepare(6, 2, Px(20), Px(4));
1653 for (index, size) in [(0, 33), (1, 33), (4, 55), (5, 55)] {
1654 cache.record_measurement(index, Px(size));
1655 }
1656 for (density, estimated, spacing, last_start, last_main, total) in [
1657 (1.25, 25, 5, 76, 68, 144),
1658 (1.5, 30, 6, 91, 82, 173),
1659 (2.0, 40, 8, 122, 110, 232),
1660 (1.0, 20, 4, 61, 55, 116),
1661 ] {
1662 tessera_ui::dp::set_scale_factor(density);
1663 cache.prepare(6, 2, Px(estimated), Px(spacing));
1664 assert_eq!(cache.item_layout(4), Some((Px(last_start), Px(last_main))));
1665 assert_eq!(cache.item_layout(5), Some((Px(last_start), Px(last_main))));
1666 assert_eq!(cache.total_main(), Px(total));
1667 }
1668 },
1669 );
1670 }
1671
1672 #[test]
1673 fn staggered_cache_keeps_lanes_before_a_remeasured_item() {
1674 let mut cache = super::StaggeredGridCache::default();
1675 let estimated = Px::new(10);
1676 let spacing = Px::new(2);
1677 cache.prepare(6, 2, estimated, spacing);
1678 cache.record_measurement(0, Px::new(30));
1679 cache.record_measurement(1, Px::new(10));
1680 cache.record_measurement(2, Px::new(10));
1681 cache.record_measurement(3, Px::new(40));
1682 cache.record_measurement(4, Px::new(12));
1683 cache.record_measurement(5, Px::new(8));
1684
1685 let lanes_before: Vec<usize> = (0..6)
1686 .map(|index| {
1687 cache
1688 .lane_of(index)
1689 .expect("every measured item must have a lane")
1690 })
1691 .collect();
1692 let starts_before: Vec<Px> = (0..3).map(|index| cache.item_start(index)).collect();
1693
1694 cache.record_measurement(3, Px::new(4));
1695
1696 let lanes_after: Vec<usize> = (0..6)
1697 .map(|index| {
1698 cache
1699 .lane_of(index)
1700 .expect("every measured item must have a lane")
1701 })
1702 .collect();
1703 let starts_after: Vec<Px> = (0..3).map(|index| cache.item_start(index)).collect();
1704 assert_eq!(&lanes_after[..3], &lanes_before[..3]);
1705 assert_eq!(starts_after, starts_before);
1706 assert_ne!(&lanes_after[3..], &lanes_before[3..]);
1707 }
1708}