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