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 const USER_TAG: u64 = 0b00 << 62;
29 const INDEX_TAG: u64 = 0b01 << 62;
30 const VALUE_MASK: u64 = (1u64 << 62) - 1;
31
32 /// Converts to u64 for slot ID usage with guaranteed non-overlapping ranges.
33 ///
34 /// # Encoding
35 /// Uses high 2 bits of the 64-bit slot ID as a type tag:
36 /// - User keys: `0b00` tag + 62-bit value (range: 0x0000... - 0x3FFF...)
37 /// - Index keys: `0b01` tag + 62-bit value (range: 0x4000... - 0x7FFF...)
38 ///
39 /// # ⚠️ Large Key Handling
40 /// Values larger than 62 bits are **mixed down to 62 bits**. This avoids panics
41 /// for extreme indices (e.g. `usize::MAX`) but introduces a small chance of
42 /// collisions for out-of-range keys. Prefer keys that fit in 62 bits when
43 /// you need guaranteed collision-free IDs.
44 ///
45 /// # Cross-Platform Safety
46 /// The slot ID is always `u64` regardless of target platform.
47 #[inline]
48 pub fn to_slot_id(self) -> u64 {
49 match self {
50 LazyLayoutKey::User(k) => {
51 let value = Self::normalize_value(k, "User");
52 Self::USER_TAG | value
53 }
54 LazyLayoutKey::Index(i) => {
55 let value = Self::normalize_value(i as u64, "Index");
56 Self::INDEX_TAG | value
57 }
58 }
59 }
60
61 #[inline]
62 fn normalize_value(value: u64, kind: &'static str) -> u64 {
63 if value <= Self::VALUE_MASK {
64 value
65 } else {
66 log::warn!(
67 "LazyList {kind} key {value:#018x} exceeds 62 bits; mixing to 62 bits to avoid overflow"
68 );
69 Self::mix_to_value_bits(value)
70 }
71 }
72
73 #[inline]
74 fn mix_to_value_bits(mut value: u64) -> u64 {
75 value ^= value >> 33;
76 value = value.wrapping_mul(0xff51afd7ed558ccd);
77 value ^= value >> 33;
78 value = value.wrapping_mul(0xc4ceb9fe1a85ec53);
79 value ^= value >> 33;
80 value & Self::VALUE_MASK
81 }
82
83 /// Returns true if this is a user-provided key.
84 #[inline]
85 pub fn is_user_key(self) -> bool {
86 matches!(self, LazyLayoutKey::User(_))
87 }
88}
89
90#[doc(hidden)]
91pub struct LazyScopeMarker;
92
93/// A run of lazy items: how many, and optionally how they are identified and
94/// reused.
95///
96/// A count converts into this on its own, so the ordinary list reads as
97/// `scope.items(20, |index| ...)` — the shape Compose gets from default
98/// arguments, which Rust does not have. A list whose contents move between
99/// compositions names a key so item state follows the item rather than the
100/// slot it happened to occupy:
101///
102/// ```rust,ignore
103/// scope.items(LazyItems::new(rows.len()).key(|index| rows[index].id), |index| {
104/// Row(&rows[index]);
105/// });
106/// ```
107#[derive(Clone, Default)]
108pub struct LazyItems {
109 count: usize,
110 key: Option<Rc<dyn Fn(usize) -> u64>>,
111 content_type: Option<Rc<dyn Fn(usize) -> u64>>,
112}
113
114impl LazyItems {
115 /// A run of `count` items with no key and no reuse class.
116 pub fn new(count: usize) -> Self {
117 Self {
118 count,
119 key: None,
120 content_type: None,
121 }
122 }
123
124 /// Gives each item a stable identity, so state, animations and scroll
125 /// position follow the item when the list is reordered or edited.
126 pub fn key(mut self, key: impl Fn(usize) -> u64 + 'static) -> Self {
127 self.key = Some(Rc::new(key));
128 self
129 }
130
131 /// Groups items into reuse classes, so a scrolling list recycles a row
132 /// into another row of the same shape rather than rebuilding it.
133 pub fn content_type(mut self, content_type: impl Fn(usize) -> u64 + 'static) -> Self {
134 self.content_type = Some(Rc::new(content_type));
135 self
136 }
137
138 /// How many items this run declares.
139 pub fn count(&self) -> usize {
140 self.count
141 }
142
143 /// The identity function, if the caller named one.
144 pub fn key_fn(&self) -> Option<Rc<dyn Fn(usize) -> u64>> {
145 self.key.clone()
146 }
147
148 /// The reuse-class function, if the caller named one.
149 pub fn content_type_fn(&self) -> Option<Rc<dyn Fn(usize) -> u64>> {
150 self.content_type.clone()
151 }
152}
153
154impl From<usize> for LazyItems {
155 fn from(count: usize) -> Self {
156 Self::new(count)
157 }
158}
159
160/// Receiver scope for lazy list content definition.
161///
162/// Used by `LazyColumn` and `LazyRow` to define list items.
163/// Matches Jetpack Compose's `LazyListScope`.
164///
165/// # Example
166///
167/// ```rust,ignore
168/// lazy_column(modifier, state, |scope| {
169/// // Single item
170/// scope.item_keyed(Some(0), None, || {
171/// Text::new("Header")
172/// });
173///
174/// // Multiple items
175/// scope.items(data.len(), Some(|i| data[i].id), None, |i| {
176/// Text::new(data[i].name.clone())
177/// });
178/// });
179/// ```
180pub trait LazyListScope {
181 /// Adds one item.
182 fn item<F>(&mut self, content: F)
183 where
184 F: Fn() + 'static,
185 {
186 self.item_keyed(None, None, content);
187 }
188
189 /// Adds one item with a stable identity and/or a reuse class.
190 ///
191 /// `key` makes item state follow the item across reorders; `content_type`
192 /// groups it with the rows it may be recycled into.
193 fn item_keyed<F>(&mut self, key: Option<u64>, content_type: Option<u64>, content: F)
194 where
195 F: Fn() + 'static;
196
197 /// Adds a run of items.
198 ///
199 /// A count is enough for the ordinary list — `scope.items(20, |index| ...)`.
200 /// Pass a [`LazyItems`] to name keys or reuse classes.
201 fn items<I, F>(&mut self, items: I, item_content: F)
202 where
203 I: Into<LazyItems>,
204 F: Fn(usize) + 'static;
205}
206
207/// Internal representation of a lazy list item interval.
208///
209/// Based on JC's `LazyLayoutIntervalContent.Interval`.
210/// Uses Rc for shared ownership of closures (not Clone).
211pub struct LazyListInterval {
212 /// Start index of this interval in the total item list.
213 pub start_index: usize,
214
215 /// Number of items in this interval.
216 pub count: usize,
217
218 /// Key generator for items in this interval.
219 /// Based on JC's `Interval.key: ((index: Int) -> Any)?`
220 pub key: Option<Rc<dyn Fn(usize) -> u64>>,
221
222 /// Content type generator for items in this interval.
223 /// Based on JC's `Interval.type: ((index: Int) -> Any?)`
224 pub content_type: Option<Rc<dyn Fn(usize) -> u64>>,
225
226 /// Content generator for items in this interval.
227 /// Takes the local index within the interval.
228 pub content: Rc<dyn Fn(usize)>,
229}
230
231impl std::fmt::Debug for LazyListInterval {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 f.debug_struct("LazyListInterval")
234 .field("start_index", &self.start_index)
235 .field("count", &self.count)
236 .finish_non_exhaustive()
237 }
238}
239
240/// Builder that collects intervals during scope execution.
241///
242/// Based on JC's `LazyLayoutIntervalContent` with `IntervalList`.
243pub struct LazyListIntervalContent {
244 intervals: Vec<LazyListInterval>,
245 total_count: usize,
246 key_cache: RefCell<Option<HashMap<u64, usize>>>,
247}
248
249impl LazyListIntervalContent {
250 /// Creates a new empty interval content.
251 pub fn new() -> Self {
252 Self {
253 intervals: Vec::new(),
254 total_count: 0,
255 key_cache: RefCell::new(None),
256 }
257 }
258
259 fn invalidate_cache(&self) {
260 *self.key_cache.borrow_mut() = None;
261 }
262
263 fn ensure_cache(&self) {
264 let mut cache = self.key_cache.borrow_mut();
265 if cache.is_some() {
266 return;
267 }
268
269 let mut map = HashMap::with_capacity(self.total_count);
270 for index in 0..self.total_count {
271 let slot_id = self.get_key(index).to_slot_id();
272 map.insert(slot_id, index);
273 }
274 *cache = Some(map);
275 }
276
277 /// Returns the total number of items across all intervals.
278 /// Matches JC's `LazyLayoutIntervalContent.itemCount`.
279 pub fn item_count(&self) -> usize {
280 self.total_count
281 }
282
283 /// Returns the intervals.
284 pub fn intervals(&self) -> &[LazyListInterval] {
285 &self.intervals
286 }
287
288 /// Gets the key for an item at the given global index.
289 ///
290 /// Returns a [`LazyLayoutKey`] that distinguishes between user-provided keys
291 /// and default index-based keys to prevent collisions.
292 ///
293 /// Matches JC's `LazyLayoutIntervalContent.getKey(index)` pattern.
294 pub fn get_key(&self, index: usize) -> LazyLayoutKey {
295 if let Some((interval, local_index)) = self.find_interval(index)
296 && let Some(key_fn) = &interval.key
297 {
298 return LazyLayoutKey::User(key_fn(local_index));
299 }
300 LazyLayoutKey::Index(index)
301 }
302
303 /// Gets the content type for an item at the given global index.
304 /// Matches JC's `LazyLayoutIntervalContent.getContentType(index)`.
305 pub fn get_content_type(&self, index: usize) -> Option<u64> {
306 if let Some((interval, local_index)) = self.find_interval(index)
307 && let Some(type_fn) = &interval.content_type
308 {
309 return Some(type_fn(local_index));
310 }
311 None
312 }
313
314 /// Invokes the content closure for an item at the given global index.
315 ///
316 /// Matches JC's `withInterval` pattern where block is called with
317 /// local index and interval content.
318 pub fn invoke_content(&self, index: usize) {
319 if let Some((interval, local_index)) = self.find_interval(index) {
320 (interval.content)(local_index);
321 }
322 }
323
324 /// Executes a block with the interval containing the given global index.
325 /// Matches JC's `withInterval(globalIndex, block)`.
326 pub fn with_interval<T, F>(&self, global_index: usize, block: F) -> Option<T>
327 where
328 F: FnOnce(usize, &LazyListInterval) -> T,
329 {
330 self.find_interval(global_index)
331 .map(|(interval, local_index)| block(local_index, interval))
332 }
333
334 /// Returns the index of an item with the given key, or None if not found.
335 /// Matches JC's `LazyLayoutItemProvider.getIndex(key: Any): Int`.
336 ///
337 /// This is used for scroll position stability - when items are added/removed,
338 /// the scroll position can be maintained by finding the new index of the
339 /// item that was previously at the scroll position (identified by key).
340 ///
341 /// Uses cached HashMap for O(1) lookup when the list has <= 10000 items.
342 /// For larger lists, use `get_index_by_key_in_range` with a `NearestRangeState`.
343 #[must_use]
344 pub fn get_index_by_key(&self, key: LazyLayoutKey) -> Option<usize> {
345 let slot_id = key.to_slot_id();
346 self.get_index_by_slot_id(slot_id)
347 }
348
349 /// Returns the index of an item with the given key, searching only within the range.
350 /// Used with NearestRangeState for O(1) key lookup in large lists.
351 pub fn get_index_by_key_in_range(
352 &self,
353 key: LazyLayoutKey,
354 range: std::ops::Range<usize>,
355 ) -> Option<usize> {
356 let start = range.start.min(self.total_count);
357 let end = range.end.min(self.total_count);
358 (start..end).find(|&index| self.get_key(index) == key)
359 }
360
361 const CACHE_THRESHOLD: usize = 64;
362
363 /// Returns the index of an item with the given slot ID, or None if not found.
364 ///
365 /// This is used for scroll position stability when the stored key is a slot ID (u64).
366 /// Slot IDs are generated by `LazyLayoutKey::to_slot_id()`.
367 ///
368 /// Uses cached HashMap for O(1) lookup on large lists. For small lists (< 64 items),
369 /// uses linear search to avoid HashMap allocation overhead.
370 /// For hot paths during scrolling, prefer `get_index_by_slot_id_in_range` first.
371 #[must_use]
372 pub fn get_index_by_slot_id(&self, slot_id: u64) -> Option<usize> {
373 if self.total_count <= Self::CACHE_THRESHOLD {
374 return (0..self.total_count)
375 .find(|&index| self.get_key(index).to_slot_id() == slot_id);
376 }
377
378 self.ensure_cache();
379 if let Some(cache) = self.key_cache.borrow().as_ref() {
380 return cache.get(&slot_id).copied();
381 }
382
383 log::warn!(
384 "get_index_by_slot_id: cache unexpectedly missing ({} items), using linear search",
385 self.total_count
386 );
387 (0..self.total_count).find(|&index| self.get_key(index).to_slot_id() == slot_id)
388 }
389
390 /// Returns the index of an item with the given slot ID, searching only within the range.
391 pub fn get_index_by_slot_id_in_range(
392 &self,
393 slot_id: u64,
394 range: std::ops::Range<usize>,
395 ) -> Option<usize> {
396 let start = range.start.min(self.total_count);
397 let end = range.end.min(self.total_count);
398 (start..end).find(|&index| self.get_key(index).to_slot_id() == slot_id)
399 }
400
401 fn find_interval(&self, index: usize) -> Option<(&LazyListInterval, usize)> {
402 if self.intervals.is_empty() || index >= self.total_count {
403 return None;
404 }
405
406 let pos = self
407 .intervals
408 .partition_point(|interval| interval.start_index + interval.count <= index);
409
410 if pos < self.intervals.len() {
411 let interval = &self.intervals[pos];
412 if index >= interval.start_index && index < interval.start_index + interval.count {
413 let local_index = index - interval.start_index;
414 return Some((interval, local_index));
415 }
416 }
417 None
418 }
419}
420
421impl Default for LazyListIntervalContent {
422 fn default() -> Self {
423 Self::new()
424 }
425}
426
427impl LazyListScope for LazyListIntervalContent {
428 fn item_keyed<F>(&mut self, key: Option<u64>, content_type: Option<u64>, content: F)
429 where
430 F: Fn() + 'static,
431 {
432 self.invalidate_cache();
433 let start_index = self.total_count;
434 self.intervals.push(LazyListInterval {
435 start_index,
436 count: 1,
437 key: key.map(|k| Rc::new(move |_| k) as Rc<dyn Fn(usize) -> u64>),
438 content_type: content_type.map(|t| Rc::new(move |_| t) as Rc<dyn Fn(usize) -> u64>),
439 content: Rc::new(move |_| content()),
440 });
441 self.total_count += 1;
442 }
443
444 fn items<I, F>(&mut self, items: I, item_content: F)
445 where
446 I: Into<LazyItems>,
447 F: Fn(usize) + 'static,
448 {
449 let items = items.into();
450 let count = items.count();
451 if count == 0 {
452 return;
453 }
454
455 self.invalidate_cache();
456 let start_index = self.total_count;
457 self.intervals.push(LazyListInterval {
458 start_index,
459 count,
460 key: items.key_fn(),
461 content_type: items.content_type_fn(),
462 content: Rc::new(item_content),
463 });
464 self.total_count += count;
465 }
466}
467
468use crate::lazy::item_provider::LazyLayoutItemProvider;
469
470/// Implements [`LazyLayoutItemProvider`] to formalize the item factory contract.
471/// This provides the same functionality as the existing methods but through
472/// the standardized trait interface.
473impl LazyLayoutItemProvider for LazyListIntervalContent {
474 fn item_count(&self) -> usize {
475 self.total_count
476 }
477
478 fn get_key(&self, index: usize) -> u64 {
479 LazyListIntervalContent::get_key(self, index).to_slot_id()
480 }
481
482 fn get_content_type(&self, index: usize) -> Option<u64> {
483 LazyListIntervalContent::get_content_type(self, index)
484 }
485
486 fn get_index(&self, key: u64) -> Option<usize> {
487 self.get_index_by_slot_id(key)
488 }
489}
490
491/// Extension trait for adding convenience methods to [`LazyListScope`].
492///
493/// Provides ergonomic APIs for common use cases with different performance tradeoffs:
494///
495/// | Method | Upfront Cost | Use Case |
496/// |--------|--------------|----------|
497/// | `items_slice` | O(n) copy | Convenience, small data |
498/// | `items_slice_rc` | O(1) | Data already in `Rc<[T]>` |
499/// | `items_with_provider` | O(1) | Lazy on-demand access |
500pub trait LazyListScopeExt: LazyListScope {
501 /// Adds items from a slice with an item-aware content closure.
502 ///
503 /// # ⚠️ Performance Warning
504 ///
505 /// **This method performs an O(n) allocation and copy of the entire slice upfront.**
506 ///
507 /// This copy is required to satisfy Rust's `'static` closure requirements for
508 /// the lazy list item factory. For small lists (< 1000 items) this is typically
509 /// acceptable, but for large datasets consider these alternatives:
510 ///
511 /// | Alternative | When to Use |
512 /// |-------------|-------------|
513 /// | `items_slice_rc` | Data is already in `Rc<[T]>` - **zero copy** |
514 /// | `items_vec` | Data is in a `Vec<T>` you can give up ownership of - **efficient** |
515 /// | `items_with_provider` | Need lazy on-demand access - **zero copy** |
516 ///
517 /// After the initial copy, the closure captures a reference-counted pointer,
518 /// so subsequent Rc clones are O(1).
519 ///
520 /// # Example
521 ///
522 /// ```rust,ignore
523 /// let data = vec!["Apple", "Banana", "Cherry"];
524 /// scope.items_slice(&data, |item| {
525 /// Text(item.to_string(), Modifier::empty());
526 /// });
527 /// ```
528 fn items_slice<T, F>(&mut self, items: &[T], item_content: F)
529 where
530 T: Clone + 'static,
531 F: Fn(&T) + 'static,
532 {
533 let items_rc: Rc<[T]> = items.to_vec().into();
534 self.items(items.len(), move |index| {
535 if let Some(item) = items_rc.get(index) {
536 item_content(item);
537 }
538 });
539 }
540
541 /// Adds items from a `Vec<T>`, taking ownership.
542 ///
543 /// **Efficient ownership transfer**: Uses `Rc::from(vec)` which avoids copying
544 /// elements if the allocation fits (or does a simple realloc).
545 /// Use this when you have a `Vec` and want to pass it to the list.
546 ///
547 /// # Example
548 ///
549 /// ```rust,ignore
550 /// let data = vec!["Apple".to_string(), "Banana".to_string()];
551 /// scope.items_vec(data, |item| {
552 /// Text(item.to_string(), Modifier::empty());
553 /// });
554 /// ```
555 fn items_vec<T, F>(&mut self, items: Vec<T>, item_content: F)
556 where
557 T: 'static,
558 F: Fn(&T) + 'static,
559 {
560 let len = items.len();
561 let items_rc: Rc<[T]> = Rc::from(items);
562 self.items(len, move |index| {
563 if let Some(item) = items_rc.get(index) {
564 item_content(item);
565 }
566 });
567 }
568
569 /// Adds indexed items from a collection (Slice, Vec, or Rc).
570 ///
571 /// This method is generic over the input type `L` which must be convertible to `Rc<[T]>`.
572 /// This allows for efficient ownership transfer (zero-copy for `Vec` and `Rc`) or
573 /// convenient usage with slices (which will perform a copy).
574 ///
575 /// # Performance Note
576 ///
577 /// - **`Vec<T>`**: Zero-copy (ownership transfer). Efficient.
578 /// - **`Rc<[T]>`**: Zero-copy (ownership transfer). Efficient.
579 /// - **`&[T]`**: **O(N) copy**. Convenient for small lists, but avoid for large datasets.
580 ///
581 /// # Example
582 ///
583 /// ```rust,ignore
584 /// // Efficient Vec usage (zero-copy)
585 /// let data = vec!["Apple".to_string(), "Banana".to_string()];
586 /// scope.items_indexed(data, |index, item| { ... });
587 ///
588 /// // Slice usage (performs copy)
589 /// let data_slice = &["Apple", "Banana"];
590 /// scope.items_indexed(data_slice, |index, item| { ... });
591 /// ```
592 fn items_indexed<T, L, F>(&mut self, items: L, item_content: F)
593 where
594 T: 'static,
595 L: Into<Rc<[T]>>,
596 F: Fn(usize, &T) + 'static,
597 {
598 let items_rc: Rc<[T]> = items.into();
599 self.items(items_rc.len(), move |index| {
600 if let Some(item) = items_rc.get(index) {
601 item_content(index, item);
602 }
603 });
604 }
605
606 /// Adds items from a pre-existing `Rc<[T]>` without cloning.
607 ///
608 /// **Zero-copy optimization**: If you already have your data in an `Rc<[T]>`,
609 /// use this method to avoid the O(n) clone that `items_slice` performs.
610 ///
611 /// # Example
612 ///
613 /// ```rust,ignore
614 /// let data: Rc<[String]> = Rc::from(vec!["Apple".into(), "Banana".into()]);
615 /// scope.items_slice_rc(Rc::clone(&data), |item| {
616 /// Text(item.to_string(), Modifier::empty());
617 /// });
618 /// ```
619 fn items_slice_rc<T, F>(&mut self, items: Rc<[T]>, item_content: F)
620 where
621 T: 'static,
622 F: Fn(&T) + 'static,
623 {
624 let len = items.len();
625 self.items(len, move |index| {
626 if let Some(item) = items.get(index) {
627 item_content(item);
628 }
629 });
630 }
631
632 /// Adds indexed items from a pre-existing `Rc<[T]>` without cloning.
633 ///
634 /// **Zero-copy optimization**: If you already have your data in an `Rc<[T]>`,
635 /// use this method to avoid the O(n) clone that `items_indexed` performs.
636 ///
637 /// # Example
638 ///
639 /// ```rust,ignore
640 /// let data: Rc<[String]> = Rc::from(vec!["Apple".into(), "Banana".into()]);
641 /// scope.items_indexed_rc(Rc::clone(&data), |index, item| {
642 /// Text(format!("{}. {}", index + 1, item), Modifier::empty());
643 /// });
644 /// ```
645 fn items_indexed_rc<T, F>(&mut self, items: Rc<[T]>, item_content: F)
646 where
647 T: 'static,
648 F: Fn(usize, &T) + 'static,
649 {
650 let len = items.len();
651 self.items(len, move |index| {
652 if let Some(item) = items.get(index) {
653 item_content(index, item);
654 }
655 });
656 }
657
658 /// Adds items using a provider function for on-demand data access.
659 ///
660 /// **Zero-allocation pattern**: Instead of storing data, the provider function
661 /// is called lazily when each item is rendered. This avoids any upfront
662 /// allocation or cloning.
663 ///
664 /// The provider should return `Some(T)` for valid indices and `None` for
665 /// out-of-bounds access. The item is passed by value to the content closure.
666 ///
667 /// # Example
668 ///
669 /// ```rust,ignore
670 /// let data = vec!["Apple", "Banana", "Cherry"];
671 /// scope.items_with_provider(
672 /// data.len(),
673 /// move |index| data.get(index).copied(),
674 /// |item| {
675 /// Text(item.to_string(), Modifier::empty());
676 /// },
677 /// );
678 /// ```
679 fn items_with_provider<T, P, F>(&mut self, count: usize, provider: P, item_content: F)
680 where
681 T: 'static,
682 P: Fn(usize) -> Option<T> + 'static,
683 F: Fn(T) + 'static,
684 {
685 self.items(count, move |index| {
686 if let Some(item) = provider(index) {
687 item_content(item);
688 }
689 });
690 }
691
692 /// Adds indexed items using a provider function for on-demand data access.
693 ///
694 /// **Zero-allocation pattern**: Instead of storing data, the provider function
695 /// is called lazily when each item is rendered. This avoids any upfront
696 /// allocation or cloning.
697 ///
698 /// # Example
699 ///
700 /// ```rust,ignore
701 /// let data = vec!["Apple", "Banana", "Cherry"];
702 /// scope.items_indexed_with_provider(
703 /// data.len(),
704 /// move |index| data.get(index).copied(),
705 /// |index, item| {
706 /// Text(format!("{}. {}", index + 1, item), Modifier::empty());
707 /// },
708 /// );
709 /// ```
710 fn items_indexed_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(usize, T) + 'static,
715 {
716 self.items(count, move |index| {
717 if let Some(item) = provider(index) {
718 item_content(index, item);
719 }
720 });
721 }
722}
723
724impl<T: LazyListScope + ?Sized> LazyListScopeExt for T {}
725
726#[cfg(test)]
727#[path = "tests/lazy_list_scope_tests.rs"]
728mod tests;