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}