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