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 {} key {:#018x} exceeds 62 bits; mixing to 62 bits to avoid overflow",
68                kind,
69                value
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}
115
116impl LazyItems {
117    /// A run of `count` items with no key and no reuse class.
118    pub fn new(count: usize) -> Self {
119        Self {
120            count,
121            key: None,
122            content_type: None,
123        }
124    }
125
126    /// Gives each item a stable identity, so state, animations and scroll
127    /// position follow the item when the list is reordered or edited.
128    pub fn key(mut self, key: impl Fn(usize) -> u64 + 'static) -> Self {
129        self.key = Some(Rc::new(key));
130        self
131    }
132
133    /// Groups items into reuse classes, so a scrolling list recycles a row
134    /// into another row of the same shape rather than rebuilding it.
135    pub fn content_type(mut self, content_type: impl Fn(usize) -> u64 + 'static) -> Self {
136        self.content_type = Some(Rc::new(content_type));
137        self
138    }
139
140    /// How many items this run declares.
141    pub fn count(&self) -> usize {
142        self.count
143    }
144
145    /// The identity function, if the caller named one.
146    pub fn key_fn(&self) -> Option<Rc<dyn Fn(usize) -> u64>> {
147        self.key.clone()
148    }
149
150    /// The reuse-class function, if the caller named one.
151    pub fn content_type_fn(&self) -> Option<Rc<dyn Fn(usize) -> u64>> {
152        self.content_type.clone()
153    }
154}
155
156impl From<usize> for LazyItems {
157    fn from(count: usize) -> Self {
158        Self::new(count)
159    }
160}
161
162/// Receiver scope for lazy list content definition.
163///
164/// Used by `LazyColumn` and `LazyRow` to define list items.
165/// Matches Jetpack Compose's `LazyListScope`.
166///
167/// # Example
168///
169/// ```rust,ignore
170/// lazy_column(modifier, state, |scope| {
171///     // Single item
172///     scope.item_keyed(Some(0), None, || {
173///         Text::new("Header")
174///     });
175///
176///     // Multiple items
177///     scope.items(data.len(), Some(|i| data[i].id), None, |i| {
178///         Text::new(data[i].name.clone())
179///     });
180/// });
181/// ```
182pub trait LazyListScope {
183    /// Adds one item.
184    fn item<F>(&mut self, content: F)
185    where
186        F: Fn() + 'static,
187    {
188        self.item_keyed(None, None, content);
189    }
190
191    /// Adds one item with a stable identity and/or a reuse class.
192    ///
193    /// `key` makes item state follow the item across reorders; `content_type`
194    /// groups it with the rows it may be recycled into.
195    fn item_keyed<F>(&mut self, key: Option<u64>, content_type: Option<u64>, content: F)
196    where
197        F: Fn() + 'static;
198
199    /// Adds a run of items.
200    ///
201    /// A count is enough for the ordinary list — `scope.items(20, |index| ...)`.
202    /// Pass a [`LazyItems`] to name keys or reuse classes.
203    fn items<I, F>(&mut self, items: I, item_content: F)
204    where
205        I: Into<LazyItems>,
206        F: Fn(usize) + 'static;
207}
208
209/// Internal representation of a lazy list item interval.
210///
211/// Based on JC's `LazyLayoutIntervalContent.Interval`.
212/// Uses Rc for shared ownership of closures (not Clone).
213pub struct LazyListInterval {
214    /// Start index of this interval in the total item list.
215    pub start_index: usize,
216
217    /// Number of items in this interval.
218    pub count: usize,
219
220    /// Key generator for items in this interval.
221    /// Based on JC's `Interval.key: ((index: Int) -> Any)?`
222    pub key: Option<Rc<dyn Fn(usize) -> u64>>,
223
224    /// Content type generator for items in this interval.
225    /// Based on JC's `Interval.type: ((index: Int) -> Any?)`
226    pub content_type: Option<Rc<dyn Fn(usize) -> u64>>,
227
228    /// Content generator for items in this interval.
229    /// Takes the local index within the interval.
230    pub content: Rc<dyn Fn(usize)>,
231}
232
233impl std::fmt::Debug for LazyListInterval {
234    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235        f.debug_struct("LazyListInterval")
236            .field("start_index", &self.start_index)
237            .field("count", &self.count)
238            .finish_non_exhaustive()
239    }
240}
241
242/// Builder that collects intervals during scope execution.
243///
244/// Based on JC's `LazyLayoutIntervalContent` with `IntervalList`.
245pub struct LazyListIntervalContent {
246    intervals: Vec<LazyListInterval>,
247    total_count: usize,
248    key_cache: RefCell<Option<HashMap<u64, usize>>>,
249}
250
251impl LazyListIntervalContent {
252    /// Creates a new empty interval content.
253    pub fn new() -> Self {
254        Self {
255            intervals: Vec::new(),
256            total_count: 0,
257            key_cache: RefCell::new(None),
258        }
259    }
260
261    fn invalidate_cache(&self) {
262        *self.key_cache.borrow_mut() = None;
263    }
264
265    fn ensure_cache(&self) {
266        let mut cache = self.key_cache.borrow_mut();
267        if cache.is_some() {
268            return;
269        }
270
271        let mut map = HashMap::with_capacity(self.total_count);
272        for index in 0..self.total_count {
273            let slot_id = self.get_key(index).to_slot_id();
274            map.insert(slot_id, index);
275        }
276        *cache = Some(map);
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        let start = range.start.min(self.total_count);
359        let end = range.end.min(self.total_count);
360        (start..end).find(|&index| self.get_key(index) == key)
361    }
362
363    const CACHE_THRESHOLD: usize = 64;
364
365    /// Returns the index of an item with the given slot ID, or None if not found.
366    ///
367    /// This is used for scroll position stability when the stored key is a slot ID (u64).
368    /// Slot IDs are generated by `LazyLayoutKey::to_slot_id()`.
369    ///
370    /// Uses cached HashMap for O(1) lookup on large lists. For small lists (< 64 items),
371    /// uses linear search to avoid HashMap allocation overhead.
372    /// For hot paths during scrolling, prefer `get_index_by_slot_id_in_range` first.
373    #[must_use]
374    pub fn get_index_by_slot_id(&self, slot_id: u64) -> Option<usize> {
375        if self.total_count <= Self::CACHE_THRESHOLD {
376            return (0..self.total_count)
377                .find(|&index| self.get_key(index).to_slot_id() == slot_id);
378        }
379
380        self.ensure_cache();
381        if let Some(cache) = self.key_cache.borrow().as_ref() {
382            return cache.get(&slot_id).copied();
383        }
384
385        log::warn!(
386            "get_index_by_slot_id: cache unexpectedly missing ({} items), using linear search",
387            self.total_count
388        );
389        (0..self.total_count).find(|&index| self.get_key(index).to_slot_id() == slot_id)
390    }
391
392    /// Returns the index of an item with the given slot ID, searching only within the range.
393    pub fn get_index_by_slot_id_in_range(
394        &self,
395        slot_id: u64,
396        range: std::ops::Range<usize>,
397    ) -> Option<usize> {
398        let start = range.start.min(self.total_count);
399        let end = range.end.min(self.total_count);
400        (start..end).find(|&index| self.get_key(index).to_slot_id() == slot_id)
401    }
402
403    fn find_interval(&self, index: usize) -> Option<(&LazyListInterval, usize)> {
404        if self.intervals.is_empty() || index >= self.total_count {
405            return None;
406        }
407
408        let pos = self
409            .intervals
410            .partition_point(|interval| interval.start_index + interval.count <= index);
411
412        if pos < self.intervals.len() {
413            let interval = &self.intervals[pos];
414            if index >= interval.start_index && index < interval.start_index + interval.count {
415                let local_index = index - interval.start_index;
416                return Some((interval, local_index));
417            }
418        }
419        None
420    }
421}
422
423impl Default for LazyListIntervalContent {
424    fn default() -> Self {
425        Self::new()
426    }
427}
428
429impl LazyListScope for LazyListIntervalContent {
430    fn item_keyed<F>(&mut self, key: Option<u64>, content_type: Option<u64>, content: F)
431    where
432        F: Fn() + 'static,
433    {
434        self.invalidate_cache();
435        let start_index = self.total_count;
436        self.intervals.push(LazyListInterval {
437            start_index,
438            count: 1,
439            key: key.map(|k| Rc::new(move |_| k) as Rc<dyn Fn(usize) -> u64>),
440            content_type: content_type.map(|t| Rc::new(move |_| t) as Rc<dyn Fn(usize) -> u64>),
441            content: Rc::new(move |_| content()),
442        });
443        self.total_count += 1;
444    }
445
446    fn items<I, F>(&mut self, items: I, item_content: F)
447    where
448        I: Into<LazyItems>,
449        F: Fn(usize) + 'static,
450    {
451        let items = items.into();
452        let count = items.count();
453        if count == 0 {
454            return;
455        }
456
457        self.invalidate_cache();
458        let start_index = self.total_count;
459        self.intervals.push(LazyListInterval {
460            start_index,
461            count,
462            key: items.key_fn(),
463            content_type: items.content_type_fn(),
464            content: Rc::new(item_content),
465        });
466        self.total_count += count;
467    }
468}
469
470use crate::lazy::item_provider::LazyLayoutItemProvider;
471
472/// Implements [`LazyLayoutItemProvider`] to formalize the item factory contract.
473/// This provides the same functionality as the existing methods but through
474/// the standardized trait interface.
475impl LazyLayoutItemProvider for LazyListIntervalContent {
476    fn item_count(&self) -> usize {
477        self.total_count
478    }
479
480    fn get_key(&self, index: usize) -> u64 {
481        LazyListIntervalContent::get_key(self, index).to_slot_id()
482    }
483
484    fn get_content_type(&self, index: usize) -> Option<u64> {
485        LazyListIntervalContent::get_content_type(self, index)
486    }
487
488    fn get_index(&self, key: u64) -> Option<usize> {
489        self.get_index_by_slot_id(key)
490    }
491}
492
493/// Extension trait for adding convenience methods to [`LazyListScope`].
494///
495/// Provides ergonomic APIs for common use cases with different performance tradeoffs:
496///
497/// | Method | Upfront Cost | Use Case |
498/// |--------|--------------|----------|
499/// | `items_slice` | O(n) copy | Convenience, small data |
500/// | `items_slice_rc` | O(1) | Data already in `Rc<[T]>` |
501/// | `items_with_provider` | O(1) | Lazy on-demand access |
502pub trait LazyListScopeExt: LazyListScope {
503    /// Adds items from a slice with an item-aware content closure.
504    ///
505    /// # ⚠️ Performance Warning
506    ///
507    /// **This method performs an O(n) allocation and copy of the entire slice upfront.**
508    ///
509    /// This copy is required to satisfy Rust's `'static` closure requirements for
510    /// the lazy list item factory. For small lists (< 1000 items) this is typically
511    /// acceptable, but for large datasets consider these alternatives:
512    ///
513    /// | Alternative | When to Use |
514    /// |-------------|-------------|
515    /// | `items_slice_rc` | Data is already in `Rc<[T]>` - **zero copy** |
516    /// | `items_vec` | Data is in a `Vec<T>` you can give up ownership of - **efficient** |
517    /// | `items_with_provider` | Need lazy on-demand access - **zero copy** |
518    ///
519    /// After the initial copy, the closure captures a reference-counted pointer,
520    /// so subsequent Rc clones are O(1).
521    ///
522    /// # Example
523    ///
524    /// ```rust,ignore
525    /// let data = vec!["Apple", "Banana", "Cherry"];
526    /// scope.items_slice(&data, |item| {
527    ///     Text(item.to_string(), Modifier::empty());
528    /// });
529    /// ```
530    fn items_slice<T, F>(&mut self, items: &[T], item_content: F)
531    where
532        T: Clone + 'static,
533        F: Fn(&T) + 'static,
534    {
535        let items_rc: Rc<[T]> = items.to_vec().into();
536        self.items(items.len(), move |index| {
537            if let Some(item) = items_rc.get(index) {
538                item_content(item);
539            }
540        });
541    }
542
543    /// Adds items from a `Vec<T>`, taking ownership.
544    ///
545    /// **Efficient ownership transfer**: Uses `Rc::from(vec)` which avoids copying
546    /// elements if the allocation fits (or does a simple realloc).
547    /// Use this when you have a `Vec` and want to pass it to the list.
548    ///
549    /// # Example
550    ///
551    /// ```rust,ignore
552    /// let data = vec!["Apple".to_string(), "Banana".to_string()];
553    /// scope.items_vec(data, |item| {
554    ///     Text(item.to_string(), Modifier::empty());
555    /// });
556    /// ```
557    fn items_vec<T, F>(&mut self, items: Vec<T>, item_content: F)
558    where
559        T: 'static,
560        F: Fn(&T) + 'static,
561    {
562        let len = items.len();
563        let items_rc: Rc<[T]> = Rc::from(items);
564        self.items(len, move |index| {
565            if let Some(item) = items_rc.get(index) {
566                item_content(item);
567            }
568        });
569    }
570
571    /// Adds indexed items from a collection (Slice, Vec, or Rc).
572    ///
573    /// This method is generic over the input type `L` which must be convertible to `Rc<[T]>`.
574    /// This allows for efficient ownership transfer (zero-copy for `Vec` and `Rc`) or
575    /// convenient usage with slices (which will perform a copy).
576    ///
577    /// # Performance Note
578    ///
579    /// - **`Vec<T>`**: Zero-copy (ownership transfer). Efficient.
580    /// - **`Rc<[T]>`**: Zero-copy (ownership transfer). Efficient.
581    /// - **`&[T]`**: **O(N) copy**. Convenient for small lists, but avoid for large datasets.
582    ///
583    /// # Example
584    ///
585    /// ```rust,ignore
586    /// // Efficient Vec usage (zero-copy)
587    /// let data = vec!["Apple".to_string(), "Banana".to_string()];
588    /// scope.items_indexed(data, |index, item| { ... });
589    ///
590    /// // Slice usage (performs copy)
591    /// let data_slice = &["Apple", "Banana"];
592    /// scope.items_indexed(data_slice, |index, item| { ... });
593    /// ```
594    fn items_indexed<T, L, F>(&mut self, items: L, item_content: F)
595    where
596        T: 'static,
597        L: Into<Rc<[T]>>,
598        F: Fn(usize, &T) + 'static,
599    {
600        let items_rc: Rc<[T]> = items.into();
601        self.items(items_rc.len(), move |index| {
602            if let Some(item) = items_rc.get(index) {
603                item_content(index, item);
604            }
605        });
606    }
607
608    /// Adds items from a pre-existing `Rc<[T]>` without cloning.
609    ///
610    /// **Zero-copy optimization**: If you already have your data in an `Rc<[T]>`,
611    /// use this method to avoid the O(n) clone that `items_slice` performs.
612    ///
613    /// # Example
614    ///
615    /// ```rust,ignore
616    /// let data: Rc<[String]> = Rc::from(vec!["Apple".into(), "Banana".into()]);
617    /// scope.items_slice_rc(Rc::clone(&data), |item| {
618    ///     Text(item.to_string(), Modifier::empty());
619    /// });
620    /// ```
621    fn items_slice_rc<T, F>(&mut self, items: Rc<[T]>, item_content: F)
622    where
623        T: 'static,
624        F: Fn(&T) + 'static,
625    {
626        let len = items.len();
627        self.items(len, move |index| {
628            if let Some(item) = items.get(index) {
629                item_content(item);
630            }
631        });
632    }
633
634    /// Adds indexed items from a pre-existing `Rc<[T]>` without cloning.
635    ///
636    /// **Zero-copy optimization**: If you already have your data in an `Rc<[T]>`,
637    /// use this method to avoid the O(n) clone that `items_indexed` performs.
638    ///
639    /// # Example
640    ///
641    /// ```rust,ignore
642    /// let data: Rc<[String]> = Rc::from(vec!["Apple".into(), "Banana".into()]);
643    /// scope.items_indexed_rc(Rc::clone(&data), |index, item| {
644    ///     Text(format!("{}. {}", index + 1, item), Modifier::empty());
645    /// });
646    /// ```
647    fn items_indexed_rc<T, F>(&mut self, items: Rc<[T]>, item_content: F)
648    where
649        T: 'static,
650        F: Fn(usize, &T) + 'static,
651    {
652        let len = items.len();
653        self.items(len, move |index| {
654            if let Some(item) = items.get(index) {
655                item_content(index, item);
656            }
657        });
658    }
659
660    /// Adds items using a provider function for on-demand data access.
661    ///
662    /// **Zero-allocation pattern**: Instead of storing data, the provider function
663    /// is called lazily when each item is rendered. This avoids any upfront
664    /// allocation or cloning.
665    ///
666    /// The provider should return `Some(T)` for valid indices and `None` for
667    /// out-of-bounds access. The item is passed by value to the content closure.
668    ///
669    /// # Example
670    ///
671    /// ```rust,ignore
672    /// let data = vec!["Apple", "Banana", "Cherry"];
673    /// scope.items_with_provider(
674    ///     data.len(),
675    ///     move |index| data.get(index).copied(),
676    ///     |item| {
677    ///         Text(item.to_string(), Modifier::empty());
678    ///     },
679    /// );
680    /// ```
681    fn items_with_provider<T, P, F>(&mut self, count: usize, provider: P, item_content: F)
682    where
683        T: 'static,
684        P: Fn(usize) -> Option<T> + 'static,
685        F: Fn(T) + 'static,
686    {
687        self.items(count, move |index| {
688            if let Some(item) = provider(index) {
689                item_content(item);
690            }
691        });
692    }
693
694    /// Adds indexed items using a provider function for on-demand data access.
695    ///
696    /// **Zero-allocation pattern**: Instead of storing data, the provider function
697    /// is called lazily when each item is rendered. This avoids any upfront
698    /// allocation or cloning.
699    ///
700    /// # Example
701    ///
702    /// ```rust,ignore
703    /// let data = vec!["Apple", "Banana", "Cherry"];
704    /// scope.items_indexed_with_provider(
705    ///     data.len(),
706    ///     move |index| data.get(index).copied(),
707    ///     |index, item| {
708    ///         Text(format!("{}. {}", index + 1, item), Modifier::empty());
709    ///     },
710    /// );
711    /// ```
712    fn items_indexed_with_provider<T, P, F>(&mut self, count: usize, provider: P, item_content: F)
713    where
714        T: 'static,
715        P: Fn(usize) -> Option<T> + 'static,
716        F: Fn(usize, T) + 'static,
717    {
718        self.items(count, move |index| {
719            if let Some(item) = provider(index) {
720                item_content(index, item);
721            }
722        });
723    }
724}
725
726impl<T: LazyListScope + ?Sized> LazyListScopeExt for T {}
727
728#[cfg(test)]
729mod tests {
730    use std::cell::Cell;
731
732    use super::*;
733
734    #[test]
735    fn key_overflow_warning_suppression_has_no_process_global_state() {
736        let source = include_str!("lazy_list_scope.rs");
737        let user_logged = ["USER_OVERFLOW", "_LOGGED"].concat();
738        let index_logged = ["INDEX_OVERFLOW", "_LOGGED"].concat();
739        let atomic_bool = ["Atomic", "Bool"].concat();
740
741        assert!(
742            !source.contains(&user_logged)
743                && !source.contains(&index_logged)
744                && !source.contains(&atomic_bool),
745            "lazy-list key overflow diagnostics must not use process-global suppression state"
746        );
747    }
748
749    #[test]
750    fn test_single_item() {
751        let mut content = LazyListIntervalContent::new();
752        let called = Rc::new(Cell::new(false));
753        let called_clone = Rc::clone(&called);
754
755        content.item_keyed(Some(42), None, move || {
756            called_clone.set(true);
757        });
758
759        assert_eq!(content.item_count(), 1);
760        assert_eq!(content.get_key(0), LazyLayoutKey::User(42));
761
762        content.invoke_content(0);
763        assert!(called.get());
764    }
765
766    #[test]
767    fn test_multiple_items() {
768        let mut content = LazyListIntervalContent::new();
769
770        content.items(LazyItems::new(5).key(|i| (i * 10) as u64), |_i| {});
771
772        assert_eq!(content.item_count(), 5);
773        assert_eq!(content.get_key(0), LazyLayoutKey::User(0));
774        assert_eq!(content.get_key(1), LazyLayoutKey::User(10));
775        assert_eq!(content.get_key(4), LazyLayoutKey::User(40));
776    }
777
778    #[test]
779    fn test_mixed_intervals() {
780        let mut content = LazyListIntervalContent::new();
781
782        content.item_keyed(Some(100), None, || {});
783
784        content.items(LazyItems::new(3).key(|i| i as u64), |_| {});
785
786        content.item_keyed(Some(200), None, || {});
787
788        assert_eq!(content.item_count(), 5);
789        assert_eq!(content.get_key(0), LazyLayoutKey::User(100));
790        assert_eq!(content.get_key(1), LazyLayoutKey::User(0));
791        assert_eq!(content.get_key(2), LazyLayoutKey::User(1));
792        assert_eq!(content.get_key(3), LazyLayoutKey::User(2));
793        assert_eq!(content.get_key(4), LazyLayoutKey::User(200));
794    }
795
796    #[test]
797    fn test_with_interval() {
798        let mut content = LazyListIntervalContent::new();
799        content.items(5, |_| {});
800
801        let result = content.with_interval(3, |local_idx, interval| (local_idx, interval.count));
802
803        assert_eq!(result, Some((3, 5)));
804    }
805
806    #[test]
807    fn test_user_keys_dont_collide_with_default_keys() {
808        let mut content = LazyListIntervalContent::new();
809
810        content.item_keyed(Some(0), None, || {});
811        content.item(|| {});
812        content.item_keyed(Some(1), None, || {});
813
814        assert_eq!(content.get_key(0), LazyLayoutKey::User(0));
815        assert_eq!(content.get_key(1), LazyLayoutKey::Index(1));
816        assert_eq!(content.get_key(2), LazyLayoutKey::User(1));
817
818        assert_ne!(content.get_key(0), content.get_key(1));
819        assert_ne!(content.get_key(2), content.get_key(1));
820
821        assert_ne!(
822            content.get_key(0).to_slot_id(),
823            content.get_key(1).to_slot_id()
824        );
825    }
826
827    #[test]
828    fn test_slot_id_collision_prevention() {
829        let user_key = LazyLayoutKey::User(0);
830        let index_key = LazyLayoutKey::Index(0);
831
832        assert_ne!(user_key.to_slot_id(), index_key.to_slot_id());
833
834        assert_eq!(user_key.to_slot_id(), 0);
835        assert_eq!(index_key.to_slot_id(), 1u64 << 62);
836
837        assert!(user_key.to_slot_id() < (1u64 << 62));
838        assert!(index_key.to_slot_id() >= (1u64 << 62));
839        assert!(index_key.to_slot_id() < (2u64 << 62));
840
841        let user_max = LazyLayoutKey::User((1u64 << 62) - 1);
842        assert!(
843            user_max.to_slot_id() < (1u64 << 62),
844            "User keys stay in user range"
845        );
846        assert_eq!(user_max.to_slot_id(), (1u64 << 62) - 1);
847
848        let index_large = LazyLayoutKey::Index(((1u64 << 62) - 1) as usize);
849        assert!(
850            index_large.to_slot_id() >= (1u64 << 62),
851            "Index keys stay in index range"
852        );
853        assert!(
854            index_large.to_slot_id() < (2u64 << 62),
855            "Index keys below reserved range"
856        );
857    }
858
859    #[test]
860    fn test_user_key_overflow_is_stable_and_tagged() {
861        let user_max = LazyLayoutKey::User(u64::MAX);
862        let slot = user_max.to_slot_id();
863        assert_eq!(slot, user_max.to_slot_id());
864        assert!(slot < (1u64 << 62));
865    }
866
867    #[test]
868    fn test_index_key_overflow_is_stable_and_tagged() {
869        let index_max = LazyLayoutKey::Index(usize::MAX);
870        let slot = index_max.to_slot_id();
871        assert_eq!(slot, index_max.to_slot_id());
872        assert!(slot >= (1u64 << 62));
873        assert!(slot < (2u64 << 62));
874    }
875
876    #[test]
877    fn test_user_key_high_bits_influence_slot_id() {
878        let key_low = LazyLayoutKey::User(0x0000_0000_0000_0001);
879        let key_high = LazyLayoutKey::User(0x4000_0000_0000_0001);
880        assert_ne!(
881            key_low.to_slot_id(),
882            key_high.to_slot_id(),
883            "High bits are mixed into the slot id to avoid truncation collisions"
884        );
885    }
886
887    #[test]
888    fn test_items_slice() {
889        let mut content = LazyListIntervalContent::new();
890        let data = vec!["Apple", "Banana", "Cherry"];
891        let items_visited = Rc::new(RefCell::new(Vec::new()));
892        let items_clone = items_visited.clone();
893
894        content.items_slice(&data, move |item: &&str| {
895            items_clone.borrow_mut().push((*item).to_string());
896        });
897
898        assert_eq!(content.item_count(), 3);
899
900        for i in 0..3 {
901            content.invoke_content(i);
902        }
903
904        let visited = items_visited.borrow();
905        assert_eq!(*visited, vec!["Apple", "Banana", "Cherry"]);
906    }
907
908    #[test]
909    fn test_items_indexed() {
910        let mut content = LazyListIntervalContent::new();
911        let data = vec![
912            "Apple".to_string(),
913            "Banana".to_string(),
914            "Cherry".to_string(),
915        ];
916        let items_visited = Rc::new(RefCell::new(Vec::new()));
917        let items_clone = items_visited.clone();
918
919        content.items_indexed(data, move |index, item: &String| {
920            items_clone.borrow_mut().push((index, item.clone()));
921        });
922
923        assert_eq!(content.item_count(), 3);
924
925        for i in 0..3 {
926            content.invoke_content(i);
927        }
928
929        let visited = items_visited.borrow();
930        assert_eq!(
931            *visited,
932            vec![
933                (0, "Apple".to_string()),
934                (1, "Banana".to_string()),
935                (2, "Cherry".to_string())
936            ]
937        );
938    }
939
940    #[test]
941    fn test_items_indexed_slice() {
942        let mut content = LazyListIntervalContent::new();
943        let data = vec!["Apple", "Banana", "Cherry"];
944        let items_visited = Rc::new(RefCell::new(Vec::new()));
945        let items_clone = items_visited.clone();
946
947        content.items_indexed(data.as_slice(), move |index, item: &&str| {
948            items_clone.borrow_mut().push((index, (*item).to_string()));
949        });
950
951        assert_eq!(content.item_count(), 3);
952
953        for i in 0..3 {
954            content.invoke_content(i);
955        }
956
957        let visited = items_visited.borrow();
958        assert_eq!(
959            *visited,
960            vec![
961                (0, "Apple".to_string()),
962                (1, "Banana".to_string()),
963                (2, "Cherry".to_string())
964            ]
965        );
966    }
967
968    #[test]
969    fn test_items_slice_rc() {
970        let mut content = LazyListIntervalContent::new();
971        let data: Rc<[String]> = Rc::from(vec!["Apple".into(), "Banana".into()]);
972        let items_visited = Rc::new(RefCell::new(Vec::new()));
973        let items_clone = items_visited.clone();
974
975        content.items_slice_rc(Rc::clone(&data), move |item: &String| {
976            items_clone.borrow_mut().push(item.clone());
977        });
978
979        assert_eq!(content.item_count(), 2);
980
981        for i in 0..2 {
982            content.invoke_content(i);
983        }
984
985        let visited = items_visited.borrow();
986        assert_eq!(*visited, vec!["Apple", "Banana"]);
987    }
988
989    #[test]
990    fn test_items_indexed_rc() {
991        let mut content = LazyListIntervalContent::new();
992        let data: Rc<[String]> = Rc::from(vec!["Apple".into(), "Banana".into()]);
993        let items_visited = Rc::new(RefCell::new(Vec::new()));
994        let items_clone = items_visited.clone();
995
996        content.items_indexed_rc(Rc::clone(&data), move |index, item: &String| {
997            items_clone.borrow_mut().push((index, item.clone()));
998        });
999
1000        assert_eq!(content.item_count(), 2);
1001
1002        for i in 0..2 {
1003            content.invoke_content(i);
1004        }
1005
1006        let visited = items_visited.borrow();
1007        assert_eq!(
1008            *visited,
1009            vec![(0, "Apple".to_string()), (1, "Banana".to_string())]
1010        );
1011    }
1012
1013    #[test]
1014    fn test_items_with_provider() {
1015        let mut content = LazyListIntervalContent::new();
1016        let data = ["Apple", "Banana", "Cherry"];
1017        let items_visited = Rc::new(RefCell::new(Vec::new()));
1018        let items_clone = items_visited.clone();
1019
1020        content.items_with_provider(
1021            data.len(),
1022            move |index| data.get(index).copied(),
1023            move |item: &str| {
1024                items_clone.borrow_mut().push(item.to_string());
1025            },
1026        );
1027
1028        assert_eq!(content.item_count(), 3);
1029
1030        for i in 0..3 {
1031            content.invoke_content(i);
1032        }
1033
1034        let visited = items_visited.borrow();
1035        assert_eq!(*visited, vec!["Apple", "Banana", "Cherry"]);
1036    }
1037
1038    #[test]
1039    fn test_items_indexed_with_provider() {
1040        let mut content = LazyListIntervalContent::new();
1041        let data = ["Apple", "Banana", "Cherry"];
1042        let items_visited = Rc::new(RefCell::new(Vec::new()));
1043        let items_clone = items_visited.clone();
1044
1045        content.items_indexed_with_provider(
1046            data.len(),
1047            move |index| data.get(index).copied(),
1048            move |index, item: &str| {
1049                items_clone.borrow_mut().push((index, item.to_string()));
1050            },
1051        );
1052
1053        assert_eq!(content.item_count(), 3);
1054
1055        for i in 0..3 {
1056            content.invoke_content(i);
1057        }
1058
1059        let visited = items_visited.borrow();
1060        assert_eq!(
1061            *visited,
1062            vec![
1063                (0, "Apple".to_string()),
1064                (1, "Banana".to_string()),
1065                (2, "Cherry".to_string())
1066            ]
1067        );
1068    }
1069
1070    #[test]
1071    fn test_large_list_cache_works() {
1072        let mut content = LazyListIntervalContent::new();
1073
1074        content.items(LazyItems::new(20_000).key(|i| (i * 7) as u64), |_| {});
1075
1076        let key_19999 = content.get_key(19999);
1077        assert_eq!(key_19999, LazyLayoutKey::User(19999 * 7));
1078
1079        let slot_id = key_19999.to_slot_id();
1080        let found_index = content.get_index_by_slot_id(slot_id);
1081        assert_eq!(found_index, Some(19999));
1082
1083        let key_10000 = content.get_key(10000);
1084        let slot_id_mid = key_10000.to_slot_id();
1085        let found_mid = content.get_index_by_slot_id(slot_id_mid);
1086        assert_eq!(found_mid, Some(10000));
1087    }
1088}