Skip to main content

cranpose_foundation/lazy/
lazy_list_scope.rs

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