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