1use std::{ops::Range, sync::Arc};
7
8use derive_builder::Builder;
9use parking_lot::RwLock;
10use tessera_ui::{
11 ComputedData, Constraint, DimensionValue, Dp, MeasurementError, NodeId, Px, PxPosition, tessera,
12};
13
14use crate::{
15 alignment::CrossAxisAlignment,
16 scrollable::{ScrollableArgs, ScrollableState, scrollable},
17};
18
19const DEFAULT_VIEWPORT_ITEMS: usize = 8;
20
21#[derive(Default, Clone)]
23pub struct LazyListState {
24 scrollable_state: ScrollableState,
25 cache: Arc<RwLock<LazyListCache>>,
26}
27
28impl LazyListState {
29 pub fn new() -> Self {
31 Self::default()
32 }
33
34 fn scrollable_state(&self) -> ScrollableState {
35 self.scrollable_state.clone()
36 }
37
38 fn cache(&self) -> Arc<RwLock<LazyListCache>> {
39 self.cache.clone()
40 }
41
42 fn override_scroll_extent(&self, axis: LazyListAxis, main: Px, cross: Px) {
43 let size = axis.pack_size(main, cross);
44 self.scrollable_state.override_child_size(size);
45 }
46}
47
48#[derive(Builder, Clone)]
50#[builder(pattern = "owned")]
51pub struct LazyColumnArgs {
52 #[builder(default = "ScrollableArgs::default()")]
54 pub scrollable: ScrollableArgs,
55 #[builder(default = "CrossAxisAlignment::Start")]
57 pub cross_axis_alignment: CrossAxisAlignment,
58 #[builder(default = "Dp(0.0)")]
60 pub item_spacing: Dp,
61 #[builder(default = "2")]
63 pub overscan: usize,
64 #[builder(default = "Dp(48.0)")]
66 pub estimated_item_size: Dp,
67 #[builder(default = "Dp(0.0)")]
69 pub content_padding: Dp,
70 #[builder(default = "Some(Px(8192))")]
73 pub max_viewport_main: Option<Px>,
74}
75
76impl Default for LazyColumnArgs {
77 fn default() -> Self {
78 LazyColumnArgsBuilder::default()
79 .build()
80 .expect("builder construction failed")
81 }
82}
83
84#[derive(Builder, Clone)]
86#[builder(pattern = "owned")]
87pub struct LazyRowArgs {
88 #[builder(default = "ScrollableArgs::default()")]
90 pub scrollable: ScrollableArgs,
91 #[builder(default = "CrossAxisAlignment::Start")]
93 pub cross_axis_alignment: CrossAxisAlignment,
94 #[builder(default = "Dp(0.0)")]
96 pub item_spacing: Dp,
97 #[builder(default = "2")]
99 pub overscan: usize,
100 #[builder(default = "Dp(48.0)")]
102 pub estimated_item_size: Dp,
103 #[builder(default = "Dp(0.0)")]
105 pub content_padding: Dp,
106 #[builder(default = "Some(Px(8192))")]
108 pub max_viewport_main: Option<Px>,
109}
110
111impl Default for LazyRowArgs {
112 fn default() -> Self {
113 LazyRowArgsBuilder::default()
114 .build()
115 .expect("builder construction failed")
116 }
117}
118
119pub struct LazyListScope<'a> {
121 slots: &'a mut Vec<LazySlot>,
122}
123
124impl<'a> LazyListScope<'a> {
125 pub fn items<F>(&mut self, count: usize, builder: F)
127 where
128 F: Fn(usize) + Send + Sync + 'static,
129 {
130 self.slots.push(LazySlot::items(count, builder));
131 }
132
133 pub fn item<F>(&mut self, builder: F)
135 where
136 F: Fn() + Send + Sync + 'static,
137 {
138 self.items(1, move |_| {
139 builder();
140 });
141 }
142
143 pub fn items_from_iter<I, T, F>(&mut self, iter: I, builder: F)
147 where
148 I: IntoIterator<Item = T>,
149 T: Send + Sync + 'static,
150 F: Fn(usize, &T) + Send + Sync + 'static,
151 {
152 let items: Arc<Vec<T>> = Arc::new(iter.into_iter().collect());
153 if items.is_empty() {
154 return;
155 }
156 let builder = Arc::new(builder);
157 let count = items.len();
158 self.slots.push(LazySlot::items(count, {
159 let items = items.clone();
160 let builder = builder.clone();
161 move |idx| {
162 if let Some(item) = items.get(idx) {
163 builder(idx, item);
164 }
165 }
166 }));
167 }
168
169 pub fn items_from_iter_values<I, T, F>(&mut self, iter: I, builder: F)
171 where
172 I: IntoIterator<Item = T>,
173 T: Send + Sync + 'static,
174 F: Fn(&T) + Send + Sync + 'static,
175 {
176 self.items_from_iter(iter, move |_, item| builder(item));
177 }
178}
179
180pub type LazyColumnScope<'a> = LazyListScope<'a>;
182pub type LazyRowScope<'a> = LazyListScope<'a>;
184
185#[tessera]
218pub fn lazy_column<F>(args: LazyColumnArgs, state: LazyListState, configure: F)
219where
220 F: FnOnce(&mut LazyColumnScope),
221{
222 let mut slots = Vec::new();
223 {
224 let mut scope = LazyColumnScope { slots: &mut slots };
225 configure(&mut scope);
226 }
227
228 let mut scrollable_args = args.scrollable.clone();
229 scrollable_args.vertical = true;
230 scrollable_args.horizontal = false;
231
232 let view_args = LazyListViewArgs {
233 axis: LazyListAxis::Vertical,
234 cross_axis_alignment: args.cross_axis_alignment,
235 item_spacing: sanitize_spacing(Px::from(args.item_spacing)),
236 estimated_item_main: ensure_positive_px(Px::from(args.estimated_item_size)),
237 overscan: args.overscan,
238 max_viewport_main: args.max_viewport_main,
239 padding_main: sanitize_spacing(Px::from(args.content_padding)),
240 padding_cross: sanitize_spacing(Px::from(args.content_padding)),
241 };
242
243 let state_for_child = state.clone();
244 scrollable(scrollable_args, state.scrollable_state(), move || {
245 lazy_list_view(view_args, state_for_child.clone(), slots.clone());
246 });
247}
248
249#[tessera]
281pub fn lazy_row<F>(args: LazyRowArgs, state: LazyListState, configure: F)
282where
283 F: FnOnce(&mut LazyRowScope),
284{
285 let mut slots = Vec::new();
286 {
287 let mut scope = LazyRowScope { slots: &mut slots };
288 configure(&mut scope);
289 }
290
291 let mut scrollable_args = args.scrollable.clone();
292 scrollable_args.vertical = false;
293 scrollable_args.horizontal = true;
294
295 let view_args = LazyListViewArgs {
296 axis: LazyListAxis::Horizontal,
297 cross_axis_alignment: args.cross_axis_alignment,
298 item_spacing: sanitize_spacing(Px::from(args.item_spacing)),
299 estimated_item_main: ensure_positive_px(Px::from(args.estimated_item_size)),
300 overscan: args.overscan,
301 max_viewport_main: args.max_viewport_main,
302 padding_main: sanitize_spacing(Px::from(args.content_padding)),
303 padding_cross: sanitize_spacing(Px::from(args.content_padding)),
304 };
305
306 let state_for_child = state.clone();
307 scrollable(scrollable_args, state.scrollable_state(), move || {
308 lazy_list_view(view_args, state_for_child.clone(), slots.clone());
309 });
310}
311
312#[derive(Clone)]
313struct LazyListViewArgs {
314 axis: LazyListAxis,
315 cross_axis_alignment: CrossAxisAlignment,
316 item_spacing: Px,
317 estimated_item_main: Px,
318 overscan: usize,
319 max_viewport_main: Option<Px>,
320 padding_main: Px,
321 padding_cross: Px,
322}
323
324#[tessera]
325fn lazy_list_view(view_args: LazyListViewArgs, state: LazyListState, slots: Vec<LazySlot>) {
326 let plan = LazySlotPlan::new(slots);
327 let total_count = plan.total_count();
328
329 let cache = state.cache();
330 {
331 let mut guard = cache.write();
332 guard.set_item_count(total_count);
333 }
334
335 let scroll_offset = view_args
336 .axis
337 .scroll_offset(state.scrollable_state().child_position());
338 let padding_main = view_args.padding_main;
339 let viewport_span = resolve_viewport_span(
340 view_args
341 .axis
342 .visible_span(state.scrollable_state().visible_size()),
343 view_args.estimated_item_main,
344 view_args.item_spacing,
345 );
346 let viewport_span = (viewport_span - (padding_main * 2)).max(Px::ZERO);
347
348 let visible_children = {
349 let cache_guard = cache.read();
350 compute_visible_children(
351 &plan,
352 &cache_guard,
353 total_count,
354 scroll_offset,
355 viewport_span,
356 view_args.overscan,
357 view_args.estimated_item_main,
358 view_args.item_spacing,
359 )
360 };
361
362 if visible_children.is_empty() {
363 measure(Box::new(move |_| Ok(ComputedData::ZERO)));
364 return;
365 }
366
367 let cache_for_measure = cache.clone();
368 let viewport_limit = viewport_span + padding_main + padding_main;
369 let state_for_measure = state.clone();
370 let child_constraint_axis = view_args.axis;
371 let estimated_item_main = view_args.estimated_item_main;
372 let spacing = view_args.item_spacing;
373 let cross_alignment = view_args.cross_axis_alignment;
374 let padding_cross = view_args.padding_cross;
375 let visible_plan = visible_children.clone();
376
377 measure(Box::new(
378 move |input| -> Result<ComputedData, MeasurementError> {
379 if input.children_ids.len() != visible_plan.len() {
380 return Err(MeasurementError::MeasureFnFailed(
381 "Lazy list measured child count mismatch".into(),
382 ));
383 }
384
385 let mut child_constraint =
386 child_constraint_axis.child_constraint(input.parent_constraint);
387 apply_cross_padding(&mut child_constraint, child_constraint_axis, padding_cross);
388 let mut placements = Vec::with_capacity(visible_plan.len());
389 let mut max_cross = Px::ZERO;
390 {
391 let mut cache_guard = cache_for_measure.write();
392
393 for (visible, child_id) in visible_plan.iter().zip(input.children_ids.iter()) {
394 let item_offset =
395 cache_guard.offset_for(visible.item_index, estimated_item_main, spacing);
396 let child_size = input.measure_child(*child_id, &child_constraint)?;
397
398 cache_guard.record_measurement(
399 visible.item_index,
400 child_constraint_axis.main(&child_size),
401 estimated_item_main,
402 );
403
404 max_cross = max_cross.max(child_constraint_axis.cross(&child_size));
405 placements.push(Placement {
406 child_id: *child_id,
407 offset_main: item_offset,
408 size: child_size,
409 });
410 }
411 }
412
413 let total_main = cache_for_measure
414 .read()
415 .total_main_size(estimated_item_main, spacing);
416
417 let inner_cross = max_cross;
418 let total_main_with_padding = total_main + padding_main + padding_main;
419 let cross_with_padding = inner_cross + padding_cross + padding_cross;
420 state_for_measure.override_scroll_extent(
421 child_constraint_axis,
422 total_main_with_padding,
423 cross_with_padding,
424 );
425
426 let reported_main = clamp_reported_main(
427 child_constraint_axis,
428 input.parent_constraint,
429 total_main_with_padding,
430 viewport_limit,
431 view_args.max_viewport_main,
432 );
433
434 for placement in &placements {
435 let cross_offset = compute_cross_offset(
436 inner_cross,
437 child_constraint_axis.cross(&placement.size),
438 cross_alignment,
439 );
440 let position = child_constraint_axis.position(
441 placement.offset_main + padding_main,
442 padding_cross + cross_offset,
443 );
444 input.place_child(placement.child_id, position);
445 }
446
447 Ok(child_constraint_axis.pack_size(reported_main, cross_with_padding))
448 },
449 ));
450
451 for child in build_child_closures(&visible_children) {
452 child();
453 }
454}
455
456fn resolve_viewport_span(current: Px, estimated: Px, spacing: Px) -> Px {
457 if current > Px::ZERO {
458 current
459 } else {
460 let per_item = estimated + spacing;
461 px_mul(per_item, DEFAULT_VIEWPORT_ITEMS.max(1))
462 }
463}
464
465#[allow(clippy::too_many_arguments)]
466fn compute_visible_children(
467 plan: &LazySlotPlan,
468 cache: &LazyListCache,
469 total_count: usize,
470 scroll_offset: Px,
471 viewport_span: Px,
472 overscan: usize,
473 estimated_main: Px,
474 spacing: Px,
475) -> Vec<VisibleChild> {
476 if total_count == 0 {
477 return Vec::new();
478 }
479
480 let mut start_index = cache.index_for_offset(scroll_offset, estimated_main, spacing);
481 let end_target = scroll_offset + viewport_span;
482 let mut end_index = cache.index_for_offset(end_target, estimated_main, spacing) + 1;
483
484 start_index = start_index.saturating_sub(overscan);
485 end_index = (end_index + overscan).min(total_count);
486 if start_index >= end_index {
487 end_index = (start_index + 1).min(total_count);
488 start_index = start_index.saturating_sub(1);
489 }
490
491 plan.visible_children(start_index..end_index)
492}
493
494fn clamp_reported_main(
495 axis: LazyListAxis,
496 parent_constraint: &Constraint,
497 total_main: Px,
498 viewport_span: Px,
499 fallback_limit: Option<Px>,
500) -> Px {
501 let viewport = viewport_span.max(Px::ZERO);
502 let mut report = total_main.min(viewport);
503 if let Some(max_value) = axis.constraint_max(parent_constraint).or(fallback_limit) {
504 report = report.min(max_value.max(Px::ZERO));
505 }
506 report
507}
508
509fn compute_cross_offset(final_cross: Px, child_cross: Px, alignment: CrossAxisAlignment) -> Px {
510 match alignment {
511 CrossAxisAlignment::Start | CrossAxisAlignment::Stretch => Px::ZERO,
512 CrossAxisAlignment::Center => (final_cross - child_cross).max(Px::ZERO) / 2,
513 CrossAxisAlignment::End => (final_cross - child_cross).max(Px::ZERO),
514 }
515}
516
517#[derive(Clone, Copy)]
518enum LazyListAxis {
519 Vertical,
520 Horizontal,
521}
522
523impl LazyListAxis {
524 fn main(&self, size: &ComputedData) -> Px {
525 match self {
526 Self::Vertical => size.height,
527 Self::Horizontal => size.width,
528 }
529 }
530
531 fn cross(&self, size: &ComputedData) -> Px {
532 match self {
533 Self::Vertical => size.width,
534 Self::Horizontal => size.height,
535 }
536 }
537
538 fn pack_size(&self, main: Px, cross: Px) -> ComputedData {
539 match self {
540 Self::Vertical => ComputedData {
541 width: cross,
542 height: main,
543 },
544 Self::Horizontal => ComputedData {
545 width: main,
546 height: cross,
547 },
548 }
549 }
550
551 fn position(&self, main: Px, cross: Px) -> PxPosition {
552 match self {
553 Self::Vertical => PxPosition { x: cross, y: main },
554 Self::Horizontal => PxPosition { x: main, y: cross },
555 }
556 }
557
558 fn visible_span(&self, size: ComputedData) -> Px {
559 match self {
560 Self::Vertical => size.height,
561 Self::Horizontal => size.width,
562 }
563 }
564
565 fn scroll_offset(&self, position: PxPosition) -> Px {
566 match self {
567 Self::Vertical => (-position.y).max(Px::ZERO),
568 Self::Horizontal => (-position.x).max(Px::ZERO),
569 }
570 }
571
572 fn child_constraint(&self, parent: &Constraint) -> Constraint {
573 match self {
574 Self::Vertical => Constraint::new(
575 parent.width,
576 DimensionValue::Wrap {
577 min: None,
578 max: None,
579 },
580 ),
581 Self::Horizontal => Constraint::new(
582 DimensionValue::Wrap {
583 min: None,
584 max: None,
585 },
586 parent.height,
587 ),
588 }
589 }
590
591 fn constraint_max(&self, constraint: &Constraint) -> Option<Px> {
592 match self {
593 Self::Vertical => constraint.height.get_max(),
594 Self::Horizontal => constraint.width.get_max(),
595 }
596 }
597}
598
599#[derive(Clone)]
600struct Placement {
601 child_id: NodeId,
602 offset_main: Px,
603 size: ComputedData,
604}
605
606#[derive(Clone)]
607enum LazySlot {
608 Items(LazyItemsSlot),
609}
610
611impl LazySlot {
612 fn items<F>(count: usize, builder: F) -> Self
613 where
614 F: Fn(usize) + Send + Sync + 'static,
615 {
616 Self::Items(LazyItemsSlot {
617 count,
618 builder: Arc::new(builder),
619 })
620 }
621
622 fn len(&self) -> usize {
623 match self {
624 Self::Items(slot) => slot.count,
625 }
626 }
627}
628
629#[derive(Clone)]
630struct LazyItemsSlot {
631 count: usize,
632 builder: Arc<dyn Fn(usize) + Send + Sync>,
633}
634
635#[derive(Clone)]
636struct LazySlotPlan {
637 entries: Vec<LazySlotEntry>,
638 total_count: usize,
639}
640
641impl LazySlotPlan {
642 fn new(slots: Vec<LazySlot>) -> Self {
643 let mut entries = Vec::with_capacity(slots.len());
644 let mut cursor = 0;
645 for slot in slots {
646 let len = slot.len();
647 entries.push(LazySlotEntry {
648 start: cursor,
649 len,
650 slot,
651 });
652 cursor += len;
653 }
654 Self {
655 entries,
656 total_count: cursor,
657 }
658 }
659
660 fn total_count(&self) -> usize {
661 self.total_count
662 }
663
664 fn visible_children(&self, range: Range<usize>) -> Vec<VisibleChild> {
665 let mut result = Vec::new();
666 for index in range {
667 if let Some((slot, local_index)) = self.resolve(index) {
668 result.push(VisibleChild {
669 item_index: index,
670 local_index,
671 builder: slot.builder.clone(),
672 });
673 }
674 }
675 result
676 }
677
678 fn resolve(&self, index: usize) -> Option<(&LazyItemsSlot, usize)> {
679 self.entries.iter().find_map(|entry| {
680 if index >= entry.start && index < entry.start + entry.len {
681 let local_index = index - entry.start;
682 match &entry.slot {
683 LazySlot::Items(slot) => Some((slot, local_index)),
684 }
685 } else {
686 None
687 }
688 })
689 }
690}
691
692#[derive(Clone)]
693struct LazySlotEntry {
694 start: usize,
695 len: usize,
696 slot: LazySlot,
697}
698
699#[derive(Clone)]
700struct VisibleChild {
701 item_index: usize,
702 local_index: usize,
703 builder: Arc<dyn Fn(usize) + Send + Sync>,
704}
705
706fn build_child_closures(children: &[VisibleChild]) -> Vec<Box<dyn FnOnce()>> {
707 children
708 .iter()
709 .map(|child| {
710 let builder = child.builder.clone();
711 let local_index = child.local_index;
712 Box::new(move || (builder)(local_index)) as Box<dyn FnOnce()>
713 })
714 .collect()
715}
716
717#[derive(Default)]
718struct LazyListCache {
719 total_items: usize,
720 measured_main: Vec<Option<Px>>,
721 fenwick: FenwickTree,
722}
723
724impl LazyListCache {
725 fn set_item_count(&mut self, count: usize) {
726 if self.total_items == count {
727 return;
728 }
729 self.total_items = count;
730 self.measured_main = vec![None; count];
731 self.fenwick.resize(count);
732 }
733
734 fn record_measurement(&mut self, index: usize, actual: Px, estimated: Px) {
735 if index >= self.total_items {
736 return;
737 }
738 let previous = self.measured_main[index];
739 if previous == Some(actual) {
740 return;
741 }
742
743 let prev_delta = previous.map(|val| val - estimated).unwrap_or(Px::ZERO);
744 let new_delta = actual - estimated;
745 let delta_change = new_delta - prev_delta;
746 self.measured_main[index] = Some(actual);
747 self.fenwick.update(index, delta_change);
748 }
749
750 fn offset_for(&self, index: usize, estimated: Px, spacing: Px) -> Px {
751 if self.total_items == 0 {
752 return Px::ZERO;
753 }
754 let clamped = index.min(self.total_items);
755 let spacing_contrib = px_mul(spacing, clamped);
756 let estimated_contrib = px_mul(estimated, clamped);
757 spacing_contrib + estimated_contrib + self.fenwick.prefix_sum(clamped)
758 }
759
760 fn total_main_size(&self, estimated: Px, spacing: Px) -> Px {
761 if self.total_items == 0 {
762 return Px::ZERO;
763 }
764 let spacing_contrib = px_mul(spacing, self.total_items.saturating_sub(1));
765 let estimated_contrib = px_mul(estimated, self.total_items);
766 spacing_contrib + estimated_contrib + self.fenwick.prefix_sum(self.total_items)
767 }
768
769 fn index_for_offset(&self, offset: Px, estimated: Px, spacing: Px) -> usize {
770 if self.total_items == 0 {
771 return 0;
772 }
773
774 let mut low = 0usize;
775 let mut high = self.total_items;
776 while low < high {
777 let mid = (low + high) / 2;
778 if self.offset_for(mid, estimated, spacing) <= offset {
779 low = mid + 1;
780 } else {
781 high = mid;
782 }
783 }
784 low.saturating_sub(1)
785 .min(self.total_items.saturating_sub(1))
786 }
787}
788
789#[derive(Default, Clone)]
790struct FenwickTree {
791 data: Vec<i64>,
792}
793
794impl FenwickTree {
795 fn resize(&mut self, len: usize) {
796 self.data.clear();
797 self.data.resize(len + 1, 0);
798 }
799
800 fn update(&mut self, index: usize, delta: Px) {
801 if self.data.is_empty() {
802 return;
803 }
804 let mut i = index + 1;
805 let delta_i64 = delta.0 as i64;
806 while i < self.data.len() {
807 self.data[i] = self.data[i].saturating_add(delta_i64);
808 i += i & (!i + 1);
809 }
810 }
811
812 fn prefix_sum(&self, count: usize) -> Px {
813 if self.data.is_empty() {
814 return Px::ZERO;
815 }
816 let mut idx = count;
817 let mut sum = 0i64;
818 while idx > 0 {
819 sum = sum.saturating_add(self.data[idx]);
820 idx &= idx - 1;
821 }
822 px_from_i64(sum)
823 }
824}
825
826fn px_mul(px: Px, times: usize) -> Px {
827 if times == 0 {
828 return Px::ZERO;
829 }
830 px_from_i64(px.0 as i64 * times as i64)
831}
832
833fn px_from_i64(value: i64) -> Px {
834 if value > i64::from(i32::MAX) {
835 Px(i32::MAX)
836 } else if value < i64::from(i32::MIN) {
837 Px(i32::MIN)
838 } else {
839 Px(value as i32)
840 }
841}
842
843fn ensure_positive_px(px: Px) -> Px {
844 if px <= Px::ZERO { Px(1) } else { px }
845}
846
847fn sanitize_spacing(px: Px) -> Px {
848 if px < Px::ZERO { Px::ZERO } else { px }
849}
850
851fn apply_cross_padding(constraint: &mut Constraint, axis: LazyListAxis, padding: Px) {
852 let total_padding = padding + padding;
853 match axis {
854 LazyListAxis::Vertical => {
855 constraint.width = shrink_dimension_max(constraint.width, total_padding);
856 }
857 LazyListAxis::Horizontal => {
858 constraint.height = shrink_dimension_max(constraint.height, total_padding);
859 }
860 }
861}
862
863fn shrink_dimension_max(dim: DimensionValue, amount: Px) -> DimensionValue {
864 match dim {
865 DimensionValue::Fixed(px) => DimensionValue::Fixed(saturating_sub_px(px, amount)),
866 DimensionValue::Wrap { min, max } => DimensionValue::Wrap {
867 min,
868 max: max.map(|m| saturating_sub_px(m, amount)),
869 },
870 DimensionValue::Fill { min, max } => DimensionValue::Fill {
871 min,
872 max: max.map(|m| saturating_sub_px(m, amount)),
873 },
874 }
875}
876
877fn saturating_sub_px(lhs: Px, rhs: Px) -> Px {
878 Px(lhs.0.saturating_sub(rhs.0))
879}