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().build().unwrap()
79    }
80}
81
82/// Arguments for `lazy_row`. Identical to [`LazyColumnArgs`] but horizontal scrolling is enforced.
83#[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    /// Symmetric padding applied around the lazy list content.
97    #[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
109/// Scope used to add lazily generated children to a lazy list.
110pub struct LazyListScope<'a> {
111    slots: &'a mut Vec<LazySlot>,
112}
113
114impl<'a> LazyListScope<'a> {
115    /// Adds a batch of lazily generated items.
116    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    /// Adds lazily generated items from an iterator, providing both index and element reference.
124    ///
125    /// The iterator is eagerly collected so it can be accessed on demand while virtualizing.
126    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    /// Convenience helper for iterators when only the element is needed.
150    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/// # lazy_column
164///
165/// A vertically scrolling list that only renders items visible in the viewport.
166///
167/// ## Usage
168///
169/// Display a long, vertical list of items without incurring the performance cost of rendering every item at once.
170///
171/// ## Parameters
172///
173/// - `args` — configures the list's layout and scrolling behavior; see [`LazyColumnArgs`].
174/// - `state` — a clonable [`LazyListState`] to manage scroll position and item measurement caching.
175/// - `configure` — a closure that receives a [`LazyColumnScope`] for adding items to the list.
176///
177/// ## Examples
178///
179/// ```
180/// // No Arc wrapper; `LazyListState` encapsulates internal Arc/RwLock state.
181/// use tessera_ui_basic_components::{
182///     lazy_list::{lazy_column, LazyColumnArgs, LazyListState},
183///     text::{text, TextArgsBuilder},
184/// };
185///
186/// let list_state = LazyListState::new();
187///
188/// lazy_column(LazyColumnArgs::default(), list_state, |scope| {
189///     scope.items(1000, |i| {
190///         let text_content = format!("Item #{}", i);
191///         text(TextArgsBuilder::default().text(text_content).build().unwrap());
192///     });
193/// });
194/// ```
195#[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/// # lazy_row
228///
229/// A horizontally scrolling list that only renders items visible in the viewport.
230///
231/// ## Usage
232///
233/// Display a long, horizontal list of items, such as a gallery or a set of chips.
234///
235/// ## Parameters
236///
237/// - `args` — configures the list's layout and scrolling behavior; see [`LazyRowArgs`].
238/// - `state` — a clonable [`LazyListState`] to manage scroll position and item measurement caching.
239/// - `configure` — a closure that receives a [`LazyRowScope`] for adding items to the list.
240///
241/// ## Examples
242///
243/// ```
244/// // No Arc wrapper; `LazyListState` encapsulates internal Arc/RwLock state.
245/// use tessera_ui_basic_components::{
246///     lazy_list::{lazy_row, LazyRowArgs, LazyListState},
247///     text::{text, TextArgsBuilder},
248/// };
249///
250/// let list_state = LazyListState::new();
251///
252/// lazy_row(LazyRowArgs::default(), list_state, |scope| {
253///     scope.items(100, |i| {
254///         let text_content = format!("Item {}", i);
255///         text(TextArgsBuilder::default().text(text_content).build().unwrap());
256///     });
257/// });
258/// ```
259#[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}