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