tessera_ui_basic_components/
lazy_list.rs

1//! Virtualized list components for displaying long scrolling feeds.
2//!
3//! ## Usage
4//!
5//! Use `lazy_column` or `lazy_row` to efficiently display large datasets.
6use 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/// Persistent state shared by lazy list components.
22#[derive(Default, Clone)]
23pub struct LazyListState {
24    scrollable_state: ScrollableState,
25    cache: Arc<RwLock<LazyListCache>>,
26}
27
28impl LazyListState {
29    /// Creates a new lazy list state with default scroll offsets and caches.
30    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/// Arguments shared between lazy lists.
49#[derive(Builder, Clone)]
50#[builder(pattern = "owned")]
51pub struct LazyColumnArgs {
52    /// Scroll container arguments. Vertical scrolling is enforced.
53    #[builder(default = "ScrollableArgs::default()")]
54    pub scrollable: ScrollableArgs,
55    /// How children are aligned along the cross axis (horizontal for columns).
56    #[builder(default = "CrossAxisAlignment::Start")]
57    pub cross_axis_alignment: CrossAxisAlignment,
58    /// Gap between successive items.
59    #[builder(default = "Dp(0.0)")]
60    pub item_spacing: Dp,
61    /// Number of extra items instantiated before/after the viewport.
62    #[builder(default = "2")]
63    pub overscan: usize,
64    /// Estimated main-axis size for each item, used before real measurements exist.
65    #[builder(default = "Dp(48.0)")]
66    pub estimated_item_size: Dp,
67    /// Symmetric padding applied around the lazy list content.
68    #[builder(default = "Dp(0.0)")]
69    pub content_padding: Dp,
70    /// Maximum viewport length reported back to parents. Prevents gigantic textures
71    /// when nesting the list inside wrap/auto-sized surfaces.
72    #[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/// Arguments for `lazy_row`. Identical to [`LazyColumnArgs`] but horizontal scrolling is enforced.
85#[derive(Builder, Clone)]
86#[builder(pattern = "owned")]
87pub struct LazyRowArgs {
88    /// Scroll container arguments. Horizontal scrolling is enforced.
89    #[builder(default = "ScrollableArgs::default()")]
90    pub scrollable: ScrollableArgs,
91    /// How children are aligned along the cross axis (vertical for rows).
92    #[builder(default = "CrossAxisAlignment::Start")]
93    pub cross_axis_alignment: CrossAxisAlignment,
94    /// Gap between successive items.
95    #[builder(default = "Dp(0.0)")]
96    pub item_spacing: Dp,
97    /// Number of extra items instantiated before/after the viewport.
98    #[builder(default = "2")]
99    pub overscan: usize,
100    /// Estimated main-axis size for each item, used before real measurements exist.
101    #[builder(default = "Dp(48.0)")]
102    pub estimated_item_size: Dp,
103    /// Symmetric padding applied around the lazy list content.
104    #[builder(default = "Dp(0.0)")]
105    pub content_padding: Dp,
106    /// Maximum viewport length reported back to parents for horizontal lists.
107    #[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
119/// Scope used to add lazily generated children to a lazy list.
120pub struct LazyListScope<'a> {
121    slots: &'a mut Vec<LazySlot>,
122}
123
124impl<'a> LazyListScope<'a> {
125    /// Adds a batch of lazily generated items.
126    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    /// Add a single lazily generated item.
134    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    /// Adds lazily generated items from an iterator, providing both index and element reference.
144    ///
145    /// The iterator is eagerly collected so it can be accessed on demand while virtualizing.
146    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    /// Convenience helper for iterators when only the element is needed.
170    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
180/// Scope alias for vertical lazy lists.
181pub type LazyColumnScope<'a> = LazyListScope<'a>;
182/// Scope alias for horizontal lazy lists.
183pub type LazyRowScope<'a> = LazyListScope<'a>;
184
185/// # lazy_column
186///
187/// A vertically scrolling list that only renders items visible in the viewport.
188///
189/// ## Usage
190///
191/// Display a long, vertical list of items without incurring the performance cost of rendering every item at once.
192///
193/// ## Parameters
194///
195/// - `args` — configures the list's layout and scrolling behavior; see [`LazyColumnArgs`].
196/// - `state` — a clonable [`LazyListState`] to manage scroll position and item measurement caching.
197/// - `configure` — a closure that receives a [`LazyColumnScope`] for adding items to the list.
198///
199/// ## Examples
200///
201/// ```
202/// // No Arc wrapper; `LazyListState` encapsulates internal Arc/RwLock state.
203/// use tessera_ui_basic_components::{
204///     lazy_list::{lazy_column, LazyColumnArgs, LazyListState},
205///     text::{text, TextArgsBuilder},
206/// };
207///
208/// let list_state = LazyListState::new();
209///
210/// lazy_column(LazyColumnArgs::default(), list_state, |scope| {
211///     scope.items(1000, |i| {
212///         let text_content = format!("Item #{}", i);
213///         text(TextArgsBuilder::default().text(text_content).build().expect("builder construction failed"));
214///     });
215/// });
216/// ```
217#[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/// # lazy_row
250///
251/// A horizontally scrolling list that only renders items visible in the viewport.
252///
253/// ## Usage
254///
255/// Display a long, horizontal list of items, such as a gallery or a set of chips.
256///
257/// ## Parameters
258///
259/// - `args` — configures the list's layout and scrolling behavior; see [`LazyRowArgs`].
260/// - `state` — a clonable [`LazyListState`] to manage scroll position and item measurement caching.
261/// - `configure` — a closure that receives a [`LazyRowScope`] for adding items to the list.
262///
263/// ## Examples
264///
265/// ```
266/// use tessera_ui_basic_components::{
267///     lazy_list::{lazy_row, LazyRowArgs, LazyListState},
268///     text::{text, TextArgsBuilder},
269/// };
270///
271/// let list_state = LazyListState::new();
272///
273/// lazy_row(LazyRowArgs::default(), list_state, |scope| {
274///     scope.items(100, |i| {
275///         let text_content = format!("Item {}", i);
276///         text(TextArgsBuilder::default().text(text_content).build().expect("builder construction failed"));
277///     });
278/// });
279/// ```
280#[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}