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