Skip to main content

cranpose_foundation/lazy/
lazy_list_scope.rs

1//! DSL scope for building lazy list content.
2//!
3//! Provides [`LazyListScope`] trait and implementation for the ergonomic
4//! `item {}` / `items {}` API used in `LazyColumn` and `LazyRow`.
5//!
6//! Based on JC's `LazyLayoutIntervalContent` pattern.
7
8use std::{cell::RefCell, collections::HashMap, rc::Rc};
9
10/// Key type for lazy list items.
11///
12/// Separates user-provided keys from default index-based keys to prevent collisions.
13/// This matches JC's `getDefaultLazyLayoutKey()` pattern where a wrapper type
14/// (`DefaultLazyKey`) ensures default keys never collide with user-provided keys.
15///
16/// # JC Reference
17/// - `LazyLayoutIntervalContent.getKey()` returns `content.key?.invoke(localIndex) ?: getDefaultLazyLayoutKey(index)`
18/// - `Lazy.android.kt` defines `DefaultLazyKey(index)` as a wrapper data class
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub enum LazyLayoutKey {
21    /// User-provided key (from `scope.item(key: Some(k), ...)` or `scope.items(key: Some(|i| ...), ...)`)
22    User(u64),
23    /// Default key based on global index. Cannot collide with User keys due to enum separation.
24    Index(usize),
25}
26
27impl LazyLayoutKey {
28    const USER_TAG: u64 = 0b00 << 62;
29    const INDEX_TAG: u64 = 0b01 << 62;
30    const VALUE_MASK: u64 = (1u64 << 62) - 1;
31
32    /// Converts to u64 for slot ID usage with guaranteed non-overlapping ranges.
33    ///
34    /// # Encoding
35    /// Uses high 2 bits of the 64-bit slot ID as a type tag:
36    /// - User keys: `0b00` tag + 62-bit value (range: 0x0000... - 0x3FFF...)
37    /// - Index keys: `0b01` tag + 62-bit value (range: 0x4000... - 0x7FFF...)
38    ///
39    /// # ⚠️ Large Key Handling
40    /// Values larger than 62 bits are **mixed down to 62 bits**. This avoids panics
41    /// for extreme indices (e.g. `usize::MAX`) but introduces a small chance of
42    /// collisions for out-of-range keys. Prefer keys that fit in 62 bits when
43    /// you need guaranteed collision-free IDs.
44    ///
45    /// # Cross-Platform Safety
46    /// The slot ID is always `u64` regardless of target platform.
47    #[inline]
48    pub fn to_slot_id(self) -> u64 {
49        match self {
50            LazyLayoutKey::User(k) => {
51                let value = Self::normalize_value(k, "User");
52                Self::USER_TAG | value
53            }
54            LazyLayoutKey::Index(i) => {
55                let value = Self::normalize_value(i as u64, "Index");
56                Self::INDEX_TAG | value
57            }
58        }
59    }
60
61    #[inline]
62    fn normalize_value(value: u64, kind: &'static str) -> u64 {
63        if value <= Self::VALUE_MASK {
64            value
65        } else {
66            log::warn!(
67                "LazyList {kind} key {value:#018x} exceeds 62 bits; mixing to 62 bits to avoid overflow"
68            );
69            Self::mix_to_value_bits(value)
70        }
71    }
72
73    #[inline]
74    fn mix_to_value_bits(mut value: u64) -> u64 {
75        value ^= value >> 33;
76        value = value.wrapping_mul(0xff51afd7ed558ccd);
77        value ^= value >> 33;
78        value = value.wrapping_mul(0xc4ceb9fe1a85ec53);
79        value ^= value >> 33;
80        value & Self::VALUE_MASK
81    }
82
83    /// Returns true if this is a user-provided key.
84    #[inline]
85    pub fn is_user_key(self) -> bool {
86        matches!(self, LazyLayoutKey::User(_))
87    }
88}
89
90#[doc(hidden)]
91pub struct LazyScopeMarker;
92
93/// A run of lazy items: how many, and optionally how they are identified and
94/// reused.
95///
96/// A count converts into this on its own, so the ordinary list reads as
97/// `scope.items(20, |index| ...)` — the shape Compose gets from default
98/// arguments, which Rust does not have. A list whose contents move between
99/// compositions names a key so item state follows the item rather than the
100/// slot it happened to occupy:
101///
102/// ```rust,ignore
103/// scope.items(LazyItems::new(rows.len()).key(|index| rows[index].id), |index| {
104///     Row(&rows[index]);
105/// });
106/// ```
107#[derive(Clone, Default)]
108pub struct LazyItems {
109    count: usize,
110    key: Option<Rc<dyn Fn(usize) -> u64>>,
111    content_type: Option<Rc<dyn Fn(usize) -> u64>>,
112}
113
114impl LazyItems {
115    /// A run of `count` items with no key and no reuse class.
116    pub fn new(count: usize) -> Self {
117        Self {
118            count,
119            key: None,
120            content_type: None,
121        }
122    }
123
124    /// Gives each item a stable identity, so state, animations and scroll
125    /// position follow the item when the list is reordered or edited.
126    pub fn key(mut self, key: impl Fn(usize) -> u64 + 'static) -> Self {
127        self.key = Some(Rc::new(key));
128        self
129    }
130
131    /// Groups items into reuse classes, so a scrolling list recycles a row
132    /// into another row of the same shape rather than rebuilding it.
133    pub fn content_type(mut self, content_type: impl Fn(usize) -> u64 + 'static) -> Self {
134        self.content_type = Some(Rc::new(content_type));
135        self
136    }
137
138    /// How many items this run declares.
139    pub fn count(&self) -> usize {
140        self.count
141    }
142
143    /// The identity function, if the caller named one.
144    pub fn key_fn(&self) -> Option<Rc<dyn Fn(usize) -> u64>> {
145        self.key.clone()
146    }
147
148    /// The reuse-class function, if the caller named one.
149    pub fn content_type_fn(&self) -> Option<Rc<dyn Fn(usize) -> u64>> {
150        self.content_type.clone()
151    }
152}
153
154impl From<usize> for LazyItems {
155    fn from(count: usize) -> Self {
156        Self::new(count)
157    }
158}
159
160/// Receiver scope for lazy list content definition.
161///
162/// Used by `LazyColumn` and `LazyRow` to define list items.
163/// Matches Jetpack Compose's `LazyListScope`.
164///
165/// # Example
166///
167/// ```rust,ignore
168/// lazy_column(modifier, state, |scope| {
169///     // Single item
170///     scope.item_keyed(Some(0), None, || {
171///         Text::new("Header")
172///     });
173///
174///     // Multiple items
175///     scope.items(data.len(), Some(|i| data[i].id), None, |i| {
176///         Text::new(data[i].name.clone())
177///     });
178/// });
179/// ```
180pub trait LazyListScope {
181    /// Adds one item.
182    fn item<F>(&mut self, content: F)
183    where
184        F: Fn() + 'static,
185    {
186        self.item_keyed(None, None, content);
187    }
188
189    /// Adds one item with a stable identity and/or a reuse class.
190    ///
191    /// `key` makes item state follow the item across reorders; `content_type`
192    /// groups it with the rows it may be recycled into.
193    fn item_keyed<F>(&mut self, key: Option<u64>, content_type: Option<u64>, content: F)
194    where
195        F: Fn() + 'static;
196
197    /// Adds a run of items.
198    ///
199    /// A count is enough for the ordinary list — `scope.items(20, |index| ...)`.
200    /// Pass a [`LazyItems`] to name keys or reuse classes.
201    fn items<I, F>(&mut self, items: I, item_content: F)
202    where
203        I: Into<LazyItems>,
204        F: Fn(usize) + 'static;
205}
206
207/// Internal representation of a lazy list item interval.
208///
209/// Based on JC's `LazyLayoutIntervalContent.Interval`.
210/// Uses Rc for shared ownership of closures (not Clone).
211pub struct LazyListInterval {
212    /// Start index of this interval in the total item list.
213    pub start_index: usize,
214
215    /// Number of items in this interval.
216    pub count: usize,
217
218    /// Key generator for items in this interval.
219    /// Based on JC's `Interval.key: ((index: Int) -> Any)?`
220    pub key: Option<Rc<dyn Fn(usize) -> u64>>,
221
222    /// Content type generator for items in this interval.
223    /// Based on JC's `Interval.type: ((index: Int) -> Any?)`
224    pub content_type: Option<Rc<dyn Fn(usize) -> u64>>,
225
226    /// Content generator for items in this interval.
227    /// Takes the local index within the interval.
228    pub content: Rc<dyn Fn(usize)>,
229}
230
231impl std::fmt::Debug for LazyListInterval {
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        f.debug_struct("LazyListInterval")
234            .field("start_index", &self.start_index)
235            .field("count", &self.count)
236            .finish_non_exhaustive()
237    }
238}
239
240/// Builder that collects intervals during scope execution.
241///
242/// Based on JC's `LazyLayoutIntervalContent` with `IntervalList`.
243pub struct LazyListIntervalContent {
244    intervals: Vec<LazyListInterval>,
245    total_count: usize,
246    range_index: RefCell<Option<RangeKeyIndex>>,
247}
248
249/// The slot ids of one range of items mapped to their indices: the nearest
250/// range around the viewport that lookups ask for, mapped once however many
251/// keys a measure looks up in it and however long the list is.
252struct RangeKeyIndex {
253    range: std::ops::Range<usize>,
254    indices: HashMap<u64, usize>,
255}
256
257impl LazyListIntervalContent {
258    /// Creates a new empty interval content.
259    pub fn new() -> Self {
260        Self {
261            intervals: Vec::new(),
262            total_count: 0,
263            range_index: RefCell::new(None),
264        }
265    }
266
267    fn invalidate_cache(&self) {
268        *self.range_index.borrow_mut() = None;
269    }
270
271    #[cfg(test)]
272    fn mapped_keys(&self) -> usize {
273        self.range_index
274            .borrow()
275            .as_ref()
276            .map_or(0, |index| index.indices.len())
277    }
278
279    /// Returns the total number of items across all intervals.
280    /// Matches JC's `LazyLayoutIntervalContent.itemCount`.
281    pub fn item_count(&self) -> usize {
282        self.total_count
283    }
284
285    /// Returns the intervals.
286    pub fn intervals(&self) -> &[LazyListInterval] {
287        &self.intervals
288    }
289
290    /// Gets the key for an item at the given global index.
291    ///
292    /// Returns a [`LazyLayoutKey`] that distinguishes between user-provided keys
293    /// and default index-based keys to prevent collisions.
294    ///
295    /// Matches JC's `LazyLayoutIntervalContent.getKey(index)` pattern.
296    pub fn get_key(&self, index: usize) -> LazyLayoutKey {
297        if let Some((interval, local_index)) = self.find_interval(index)
298            && let Some(key_fn) = &interval.key
299        {
300            return LazyLayoutKey::User(key_fn(local_index));
301        }
302        LazyLayoutKey::Index(index)
303    }
304
305    /// Gets the content type for an item at the given global index.
306    /// Matches JC's `LazyLayoutIntervalContent.getContentType(index)`.
307    pub fn get_content_type(&self, index: usize) -> Option<u64> {
308        if let Some((interval, local_index)) = self.find_interval(index)
309            && let Some(type_fn) = &interval.content_type
310        {
311            return Some(type_fn(local_index));
312        }
313        None
314    }
315
316    /// Invokes the content closure for an item at the given global index.
317    ///
318    /// Matches JC's `withInterval` pattern where block is called with
319    /// local index and interval content.
320    pub fn invoke_content(&self, index: usize) {
321        if let Some((interval, local_index)) = self.find_interval(index) {
322            (interval.content)(local_index);
323        }
324    }
325
326    /// Executes a block with the interval containing the given global index.
327    /// Matches JC's `withInterval(globalIndex, block)`.
328    pub fn with_interval<T, F>(&self, global_index: usize, block: F) -> Option<T>
329    where
330        F: FnOnce(usize, &LazyListInterval) -> T,
331    {
332        self.find_interval(global_index)
333            .map(|(interval, local_index)| block(local_index, interval))
334    }
335
336    /// Returns the index of an item with the given key, or None if not found.
337    /// Matches JC's `LazyLayoutItemProvider.getIndex(key: Any): Int`.
338    ///
339    /// This is used for scroll position stability - when items are added/removed,
340    /// the scroll position can be maintained by finding the new index of the
341    /// item that was previously at the scroll position (identified by key).
342    ///
343    /// Uses cached HashMap for O(1) lookup when the list has <= 10000 items.
344    /// For larger lists, use `get_index_by_key_in_range` with a `NearestRangeState`.
345    #[must_use]
346    pub fn get_index_by_key(&self, key: LazyLayoutKey) -> Option<usize> {
347        let slot_id = key.to_slot_id();
348        self.get_index_by_slot_id(slot_id)
349    }
350
351    /// Returns the index of an item with the given key, searching only within the range.
352    /// Used with NearestRangeState for O(1) key lookup in large lists.
353    pub fn get_index_by_key_in_range(
354        &self,
355        key: LazyLayoutKey,
356        range: std::ops::Range<usize>,
357    ) -> Option<usize> {
358        self.get_index_by_slot_id_in_range(key.to_slot_id(), range)
359    }
360
361    /// Returns the index of an item with the given slot ID, or None if not found.
362    ///
363    /// Slot IDs are generated by `LazyLayoutKey::to_slot_id()`. This scans the
364    /// whole list and keeps nothing: a list may hold a million items, and a map
365    /// of every key would hold tens of megabytes. Lookups near the viewport
366    /// belong to `get_index_by_slot_id_in_range`, which maps its range once.
367    #[must_use]
368    pub fn get_index_by_slot_id(&self, slot_id: u64) -> Option<usize> {
369        (0..self.total_count).find(|&index| self.get_key(index).to_slot_id() == slot_id)
370    }
371
372    /// Returns the index of an item with the given slot ID, searching only
373    /// within the range. The range's keys are mapped on the first lookup in it
374    /// and kept until another range is asked for or the content changes.
375    pub fn get_index_by_slot_id_in_range(
376        &self,
377        slot_id: u64,
378        range: std::ops::Range<usize>,
379    ) -> Option<usize> {
380        let start = range.start.min(self.total_count);
381        let range = start..range.end.clamp(start, self.total_count);
382        let mut cached = self.range_index.borrow_mut();
383        if cached.as_ref().is_none_or(|index| index.range != range) {
384            let indices = range
385                .clone()
386                .map(|index| (self.get_key(index).to_slot_id(), index))
387                .collect();
388            *cached = Some(RangeKeyIndex { range, indices });
389        }
390        cached
391            .as_ref()
392            .and_then(|index| index.indices.get(&slot_id).copied())
393    }
394
395    fn find_interval(&self, index: usize) -> Option<(&LazyListInterval, usize)> {
396        if self.intervals.is_empty() || index >= self.total_count {
397            return None;
398        }
399
400        let pos = self
401            .intervals
402            .partition_point(|interval| interval.start_index + interval.count <= index);
403
404        if pos < self.intervals.len() {
405            let interval = &self.intervals[pos];
406            if index >= interval.start_index && index < interval.start_index + interval.count {
407                let local_index = index - interval.start_index;
408                return Some((interval, local_index));
409            }
410        }
411        None
412    }
413}
414
415impl Default for LazyListIntervalContent {
416    fn default() -> Self {
417        Self::new()
418    }
419}
420
421impl LazyListScope for LazyListIntervalContent {
422    fn item_keyed<F>(&mut self, key: Option<u64>, content_type: Option<u64>, content: F)
423    where
424        F: Fn() + 'static,
425    {
426        self.invalidate_cache();
427        let start_index = self.total_count;
428        self.intervals.push(LazyListInterval {
429            start_index,
430            count: 1,
431            key: key.map(|k| Rc::new(move |_| k) as Rc<dyn Fn(usize) -> u64>),
432            content_type: content_type.map(|t| Rc::new(move |_| t) as Rc<dyn Fn(usize) -> u64>),
433            content: Rc::new(move |_| content()),
434        });
435        self.total_count += 1;
436    }
437
438    fn items<I, F>(&mut self, items: I, item_content: F)
439    where
440        I: Into<LazyItems>,
441        F: Fn(usize) + 'static,
442    {
443        let items = items.into();
444        let count = items.count();
445        if count == 0 {
446            return;
447        }
448
449        self.invalidate_cache();
450        let start_index = self.total_count;
451        self.intervals.push(LazyListInterval {
452            start_index,
453            count,
454            key: items.key_fn(),
455            content_type: items.content_type_fn(),
456            content: Rc::new(item_content),
457        });
458        self.total_count += count;
459    }
460}
461
462use crate::lazy::item_provider::LazyLayoutItemProvider;
463
464/// Implements [`LazyLayoutItemProvider`] to formalize the item factory contract.
465/// This provides the same functionality as the existing methods but through
466/// the standardized trait interface.
467impl LazyLayoutItemProvider for LazyListIntervalContent {
468    fn item_count(&self) -> usize {
469        self.total_count
470    }
471
472    fn get_key(&self, index: usize) -> u64 {
473        LazyListIntervalContent::get_key(self, index).to_slot_id()
474    }
475
476    fn get_content_type(&self, index: usize) -> Option<u64> {
477        LazyListIntervalContent::get_content_type(self, index)
478    }
479
480    fn get_index(&self, key: u64) -> Option<usize> {
481        self.get_index_by_slot_id(key)
482    }
483}
484
485/// Extension trait for adding convenience methods to [`LazyListScope`].
486///
487/// Provides ergonomic APIs for common use cases with different performance tradeoffs:
488///
489/// | Method | Upfront Cost | Use Case |
490/// |--------|--------------|----------|
491/// | `items_slice` | O(n) copy | Convenience, small data |
492/// | `items_slice_rc` | O(1) | Data already in `Rc<[T]>` |
493/// | `items_with_provider` | O(1) | Lazy on-demand access |
494pub trait LazyListScopeExt: LazyListScope {
495    /// Adds items from a slice with an item-aware content closure.
496    ///
497    /// # ⚠️ Performance Warning
498    ///
499    /// **This method performs an O(n) allocation and copy of the entire slice upfront.**
500    ///
501    /// This copy is required to satisfy Rust's `'static` closure requirements for
502    /// the lazy list item factory. For small lists (< 1000 items) this is typically
503    /// acceptable, but for large datasets consider these alternatives:
504    ///
505    /// | Alternative | When to Use |
506    /// |-------------|-------------|
507    /// | `items_slice_rc` | Data is already in `Rc<[T]>` - **zero copy** |
508    /// | `items_vec` | Data is in a `Vec<T>` you can give up ownership of - **efficient** |
509    /// | `items_with_provider` | Need lazy on-demand access - **zero copy** |
510    ///
511    /// After the initial copy, the closure captures a reference-counted pointer,
512    /// so subsequent Rc clones are O(1).
513    ///
514    /// # Example
515    ///
516    /// ```rust,ignore
517    /// let data = vec!["Apple", "Banana", "Cherry"];
518    /// scope.items_slice(&data, |item| {
519    ///     Text(item.to_string(), Modifier::empty());
520    /// });
521    /// ```
522    fn items_slice<T, F>(&mut self, items: &[T], item_content: F)
523    where
524        T: Clone + 'static,
525        F: Fn(&T) + 'static,
526    {
527        let items_rc: Rc<[T]> = items.to_vec().into();
528        self.items(items.len(), move |index| {
529            if let Some(item) = items_rc.get(index) {
530                item_content(item);
531            }
532        });
533    }
534
535    /// Adds items from a `Vec<T>`, taking ownership.
536    ///
537    /// **Efficient ownership transfer**: Uses `Rc::from(vec)` which avoids copying
538    /// elements if the allocation fits (or does a simple realloc).
539    /// Use this when you have a `Vec` and want to pass it to the list.
540    ///
541    /// # Example
542    ///
543    /// ```rust,ignore
544    /// let data = vec!["Apple".to_string(), "Banana".to_string()];
545    /// scope.items_vec(data, |item| {
546    ///     Text(item.to_string(), Modifier::empty());
547    /// });
548    /// ```
549    fn items_vec<T, F>(&mut self, items: Vec<T>, item_content: F)
550    where
551        T: 'static,
552        F: Fn(&T) + 'static,
553    {
554        let len = items.len();
555        let items_rc: Rc<[T]> = Rc::from(items);
556        self.items(len, move |index| {
557            if let Some(item) = items_rc.get(index) {
558                item_content(item);
559            }
560        });
561    }
562
563    /// Adds indexed items from a collection (Slice, Vec, or Rc).
564    ///
565    /// This method is generic over the input type `L` which must be convertible to `Rc<[T]>`.
566    /// This allows for efficient ownership transfer (zero-copy for `Vec` and `Rc`) or
567    /// convenient usage with slices (which will perform a copy).
568    ///
569    /// # Performance Note
570    ///
571    /// - **`Vec<T>`**: Zero-copy (ownership transfer). Efficient.
572    /// - **`Rc<[T]>`**: Zero-copy (ownership transfer). Efficient.
573    /// - **`&[T]`**: **O(N) copy**. Convenient for small lists, but avoid for large datasets.
574    ///
575    /// # Example
576    ///
577    /// ```rust,ignore
578    /// // Efficient Vec usage (zero-copy)
579    /// let data = vec!["Apple".to_string(), "Banana".to_string()];
580    /// scope.items_indexed(data, |index, item| { ... });
581    ///
582    /// // Slice usage (performs copy)
583    /// let data_slice = &["Apple", "Banana"];
584    /// scope.items_indexed(data_slice, |index, item| { ... });
585    /// ```
586    fn items_indexed<T, L, F>(&mut self, items: L, item_content: F)
587    where
588        T: 'static,
589        L: Into<Rc<[T]>>,
590        F: Fn(usize, &T) + 'static,
591    {
592        let items_rc: Rc<[T]> = items.into();
593        self.items(items_rc.len(), move |index| {
594            if let Some(item) = items_rc.get(index) {
595                item_content(index, item);
596            }
597        });
598    }
599
600    /// Adds items from a pre-existing `Rc<[T]>` without cloning.
601    ///
602    /// **Zero-copy optimization**: If you already have your data in an `Rc<[T]>`,
603    /// use this method to avoid the O(n) clone that `items_slice` performs.
604    ///
605    /// # Example
606    ///
607    /// ```rust,ignore
608    /// let data: Rc<[String]> = Rc::from(vec!["Apple".into(), "Banana".into()]);
609    /// scope.items_slice_rc(Rc::clone(&data), |item| {
610    ///     Text(item.to_string(), Modifier::empty());
611    /// });
612    /// ```
613    fn items_slice_rc<T, F>(&mut self, items: Rc<[T]>, item_content: F)
614    where
615        T: 'static,
616        F: Fn(&T) + 'static,
617    {
618        let len = items.len();
619        self.items(len, move |index| {
620            if let Some(item) = items.get(index) {
621                item_content(item);
622            }
623        });
624    }
625
626    /// Adds indexed items from a pre-existing `Rc<[T]>` without cloning.
627    ///
628    /// **Zero-copy optimization**: If you already have your data in an `Rc<[T]>`,
629    /// use this method to avoid the O(n) clone that `items_indexed` performs.
630    ///
631    /// # Example
632    ///
633    /// ```rust,ignore
634    /// let data: Rc<[String]> = Rc::from(vec!["Apple".into(), "Banana".into()]);
635    /// scope.items_indexed_rc(Rc::clone(&data), |index, item| {
636    ///     Text(format!("{}. {}", index + 1, item), Modifier::empty());
637    /// });
638    /// ```
639    fn items_indexed_rc<T, F>(&mut self, items: Rc<[T]>, item_content: F)
640    where
641        T: 'static,
642        F: Fn(usize, &T) + 'static,
643    {
644        let len = items.len();
645        self.items(len, move |index| {
646            if let Some(item) = items.get(index) {
647                item_content(index, item);
648            }
649        });
650    }
651
652    /// Adds items using a provider function for on-demand data access.
653    ///
654    /// **Zero-allocation pattern**: Instead of storing data, the provider function
655    /// is called lazily when each item is rendered. This avoids any upfront
656    /// allocation or cloning.
657    ///
658    /// The provider should return `Some(T)` for valid indices and `None` for
659    /// out-of-bounds access. The item is passed by value to the content closure.
660    ///
661    /// # Example
662    ///
663    /// ```rust,ignore
664    /// let data = vec!["Apple", "Banana", "Cherry"];
665    /// scope.items_with_provider(
666    ///     data.len(),
667    ///     move |index| data.get(index).copied(),
668    ///     |item| {
669    ///         Text(item.to_string(), Modifier::empty());
670    ///     },
671    /// );
672    /// ```
673    fn items_with_provider<T, P, F>(&mut self, count: usize, provider: P, item_content: F)
674    where
675        T: 'static,
676        P: Fn(usize) -> Option<T> + 'static,
677        F: Fn(T) + 'static,
678    {
679        self.items(count, move |index| {
680            if let Some(item) = provider(index) {
681                item_content(item);
682            }
683        });
684    }
685
686    /// Adds indexed items using a provider function for on-demand data access.
687    ///
688    /// **Zero-allocation pattern**: Instead of storing data, the provider function
689    /// is called lazily when each item is rendered. This avoids any upfront
690    /// allocation or cloning.
691    ///
692    /// # Example
693    ///
694    /// ```rust,ignore
695    /// let data = vec!["Apple", "Banana", "Cherry"];
696    /// scope.items_indexed_with_provider(
697    ///     data.len(),
698    ///     move |index| data.get(index).copied(),
699    ///     |index, item| {
700    ///         Text(format!("{}. {}", index + 1, item), Modifier::empty());
701    ///     },
702    /// );
703    /// ```
704    fn items_indexed_with_provider<T, P, F>(&mut self, count: usize, provider: P, item_content: F)
705    where
706        T: 'static,
707        P: Fn(usize) -> Option<T> + 'static,
708        F: Fn(usize, T) + 'static,
709    {
710        self.items(count, move |index| {
711            if let Some(item) = provider(index) {
712                item_content(index, item);
713            }
714        });
715    }
716}
717
718impl<T: LazyListScope + ?Sized> LazyListScopeExt for T {}
719
720#[cfg(test)]
721#[path = "tests/lazy_list_scope_tests.rs"]
722mod tests;