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