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