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 span: Option<GridItemSpanFn>,
113}
114
115/// How many cells of its line a grid item takes, from the item's index in
116/// its run.
117pub type GridItemSpanFn = Rc<dyn Fn(LazyGridItemSpanScope, usize) -> usize>;
118
119/// The cells a grid item's span can take — Compose's
120/// `LazyGridItemSpanScope`.
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub struct LazyGridItemSpanScope {
123 /// The cells of a whole line.
124 pub max_line_span: usize,
125 /// The cells left in the line the item would join. An item that takes
126 /// more starts the next line.
127 pub max_current_line_span: usize,
128}
129
130impl LazyItems {
131 /// A run of `count` items with no key and no reuse class.
132 pub fn new(count: usize) -> Self {
133 Self {
134 count,
135 key: None,
136 content_type: None,
137 span: None,
138 }
139 }
140
141 /// Gives each item a stable identity, so state, animations and scroll
142 /// position follow the item when the list is reordered or edited.
143 pub fn key(mut self, key: impl Fn(usize) -> u64 + 'static) -> Self {
144 self.key = Some(Rc::new(key));
145 self
146 }
147
148 /// Groups items into reuse classes, so a scrolling list recycles a row
149 /// into another row of the same shape rather than rebuilding it.
150 pub fn content_type(mut self, content_type: impl Fn(usize) -> u64 + 'static) -> Self {
151 self.content_type = Some(Rc::new(content_type));
152 self
153 }
154
155 /// Gives each item the number of cells it takes in a grid's line —
156 /// Compose's `span`. A list ignores it, and a grid clamps it to a line.
157 pub fn span(mut self, span: impl Fn(LazyGridItemSpanScope, usize) -> usize + 'static) -> Self {
158 self.span = Some(Rc::new(span));
159 self
160 }
161
162 /// How many items this run declares.
163 pub fn count(&self) -> usize {
164 self.count
165 }
166
167 /// The identity function, if the caller named one.
168 pub fn key_fn(&self) -> Option<Rc<dyn Fn(usize) -> u64>> {
169 self.key.clone()
170 }
171
172 /// The reuse-class function, if the caller named one.
173 pub fn content_type_fn(&self) -> Option<Rc<dyn Fn(usize) -> u64>> {
174 self.content_type.clone()
175 }
176}
177
178impl From<usize> for LazyItems {
179 fn from(count: usize) -> Self {
180 Self::new(count)
181 }
182}
183
184/// Receiver scope for lazy list content definition.
185///
186/// Used by `LazyColumn` and `LazyRow` to define list items.
187/// Matches Jetpack Compose's `LazyListScope`.
188///
189/// # Example
190///
191/// ```rust,ignore
192/// lazy_column(modifier, state, |scope| {
193/// // Single item
194/// scope.item_keyed(Some(0), None, || {
195/// Text::new("Header")
196/// });
197///
198/// // Multiple items
199/// scope.items(data.len(), Some(|i| data[i].id), None, |i| {
200/// Text::new(data[i].name.clone())
201/// });
202/// });
203/// ```
204pub trait LazyListScope {
205 /// Adds one item.
206 fn item<F>(&mut self, content: F)
207 where
208 F: Fn() + 'static,
209 {
210 self.item_keyed(None, None, content);
211 }
212
213 /// Adds one item with a stable identity and/or a reuse class.
214 ///
215 /// `key` makes item state follow the item across reorders; `content_type`
216 /// groups it with the rows it may be recycled into.
217 fn item_keyed<F>(&mut self, key: Option<u64>, content_type: Option<u64>, content: F)
218 where
219 F: Fn() + 'static;
220
221 /// Adds a run of items.
222 ///
223 /// A count is enough for the ordinary list — `scope.items(20, |index| ...)`.
224 /// Pass a [`LazyItems`] to name keys or reuse classes.
225 fn items<I, F>(&mut self, items: I, item_content: F)
226 where
227 I: Into<LazyItems>,
228 F: Fn(usize) + 'static;
229
230 /// Adds a header that stays at the start of the viewport while the items
231 /// after it, up to the next header, scroll under it. The next header
232 /// pushes it out. Compose's `stickyHeader`.
233 fn sticky_header<F>(&mut self, content: F)
234 where
235 F: Fn() + 'static,
236 {
237 self.sticky_header_keyed(None, None, content);
238 }
239
240 /// [`LazyListScope::sticky_header`] with a stable identity and/or a reuse
241 /// class, as [`LazyListScope::item_keyed`] takes.
242 fn sticky_header_keyed<F>(&mut self, key: Option<u64>, content_type: Option<u64>, content: F)
243 where
244 F: Fn() + 'static;
245}
246
247/// Internal representation of a lazy list item interval.
248///
249/// Based on JC's `LazyLayoutIntervalContent.Interval`.
250/// Uses Rc for shared ownership of closures (not Clone).
251pub struct LazyListInterval {
252 /// Start index of this interval in the total item list.
253 pub start_index: usize,
254
255 /// Number of items in this interval.
256 pub count: usize,
257
258 /// Key generator for items in this interval.
259 /// Based on JC's `Interval.key: ((index: Int) -> Any)?`
260 pub key: Option<Rc<dyn Fn(usize) -> u64>>,
261
262 /// Content type generator for items in this interval.
263 /// Based on JC's `Interval.type: ((index: Int) -> Any?)`
264 pub content_type: Option<Rc<dyn Fn(usize) -> u64>>,
265
266 /// The cells each item takes in a grid's line; one when absent.
267 pub span: Option<GridItemSpanFn>,
268
269 /// Content generator for items in this interval.
270 /// Takes the local index within the interval.
271 pub content: Rc<dyn Fn(usize)>,
272}
273
274impl std::fmt::Debug for LazyListInterval {
275 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276 f.debug_struct("LazyListInterval")
277 .field("start_index", &self.start_index)
278 .field("count", &self.count)
279 .finish_non_exhaustive()
280 }
281}
282
283/// Builder that collects intervals during scope execution.
284///
285/// Based on JC's `LazyLayoutIntervalContent` with `IntervalList`.
286pub struct LazyListIntervalContent {
287 intervals: Vec<LazyListInterval>,
288 total_count: usize,
289 /// The indices of the sticky headers, ascending.
290 header_indices: Vec<usize>,
291 range_index: RefCell<Option<RangeKeyIndex>>,
292}
293
294/// The slot ids of one range of items mapped to their indices: the nearest
295/// range around the viewport that lookups ask for, mapped once however many
296/// keys a measure looks up in it and however long the list is.
297struct RangeKeyIndex {
298 range: std::ops::Range<usize>,
299 indices: HashMap<u64, usize>,
300}
301
302impl LazyListIntervalContent {
303 /// Creates a new empty interval content.
304 pub fn new() -> Self {
305 Self {
306 intervals: Vec::new(),
307 total_count: 0,
308 header_indices: Vec::new(),
309 range_index: RefCell::new(None),
310 }
311 }
312
313 fn invalidate_cache(&self) {
314 *self.range_index.borrow_mut() = None;
315 }
316
317 #[cfg(test)]
318 fn mapped_keys(&self) -> usize {
319 self.range_index
320 .borrow()
321 .as_ref()
322 .map_or(0, |index| index.indices.len())
323 }
324
325 /// Returns the total number of items across all intervals.
326 /// Matches JC's `LazyLayoutIntervalContent.itemCount`.
327 pub fn item_count(&self) -> usize {
328 self.total_count
329 }
330
331 /// Returns the intervals.
332 pub fn intervals(&self) -> &[LazyListInterval] {
333 &self.intervals
334 }
335
336 /// The indices of the sticky headers, ascending.
337 pub fn header_indices(&self) -> &[usize] {
338 &self.header_indices
339 }
340
341 /// Gets the key for an item at the given global index.
342 ///
343 /// Returns a [`LazyLayoutKey`] that distinguishes between user-provided keys
344 /// and default index-based keys to prevent collisions.
345 ///
346 /// Matches JC's `LazyLayoutIntervalContent.getKey(index)` pattern.
347 pub fn get_key(&self, index: usize) -> LazyLayoutKey {
348 if let Some((interval, local_index)) = self.find_interval(index)
349 && let Some(key_fn) = &interval.key
350 {
351 return LazyLayoutKey::User(key_fn(local_index));
352 }
353 LazyLayoutKey::Index(index)
354 }
355
356 /// Gets the content type for an item at the given global index.
357 /// Matches JC's `LazyLayoutIntervalContent.getContentType(index)`.
358 pub fn get_content_type(&self, index: usize) -> Option<u64> {
359 if let Some((interval, local_index)) = self.find_interval(index)
360 && let Some(type_fn) = &interval.content_type
361 {
362 return Some(type_fn(local_index));
363 }
364 None
365 }
366
367 /// Invokes the content closure for an item at the given global index.
368 ///
369 /// Matches JC's `withInterval` pattern where block is called with
370 /// local index and interval content.
371 pub fn invoke_content(&self, index: usize) {
372 if let Some((interval, local_index)) = self.find_interval(index) {
373 (interval.content)(local_index);
374 }
375 }
376
377 /// Executes a block with the interval containing the given global index.
378 /// Matches JC's `withInterval(globalIndex, block)`.
379 pub fn with_interval<T, F>(&self, global_index: usize, block: F) -> Option<T>
380 where
381 F: FnOnce(usize, &LazyListInterval) -> T,
382 {
383 self.find_interval(global_index)
384 .map(|(interval, local_index)| block(local_index, interval))
385 }
386
387 /// Returns the index of an item with the given key, or None if not found.
388 /// Matches JC's `LazyLayoutItemProvider.getIndex(key: Any): Int`.
389 ///
390 /// This is used for scroll position stability - when items are added/removed,
391 /// the scroll position can be maintained by finding the new index of the
392 /// item that was previously at the scroll position (identified by key).
393 ///
394 /// Uses cached HashMap for O(1) lookup when the list has <= 10000 items.
395 /// For larger lists, use `get_index_by_key_in_range` with a `NearestRangeState`.
396 #[must_use]
397 pub fn get_index_by_key(&self, key: LazyLayoutKey) -> Option<usize> {
398 let slot_id = key.to_slot_id();
399 self.get_index_by_slot_id(slot_id)
400 }
401
402 /// Returns the index of an item with the given key, searching only within the range.
403 /// Used with NearestRangeState for O(1) key lookup in large lists.
404 pub fn get_index_by_key_in_range(
405 &self,
406 key: LazyLayoutKey,
407 range: std::ops::Range<usize>,
408 ) -> Option<usize> {
409 self.get_index_by_slot_id_in_range(key.to_slot_id(), range)
410 }
411
412 /// Returns the index of an item with the given slot ID, or None if not found.
413 ///
414 /// Slot IDs are generated by `LazyLayoutKey::to_slot_id()`. This scans the
415 /// whole list and keeps nothing: a list may hold a million items, and a map
416 /// of every key would hold tens of megabytes. Lookups near the viewport
417 /// belong to `get_index_by_slot_id_in_range`, which maps its range once.
418 #[must_use]
419 pub fn get_index_by_slot_id(&self, slot_id: u64) -> Option<usize> {
420 (0..self.total_count).find(|&index| self.get_key(index).to_slot_id() == slot_id)
421 }
422
423 /// Returns the index of an item with the given slot ID, searching only
424 /// within the range. The range's keys are mapped on the first lookup in it
425 /// and kept until another range is asked for or the content changes.
426 pub fn get_index_by_slot_id_in_range(
427 &self,
428 slot_id: u64,
429 range: std::ops::Range<usize>,
430 ) -> Option<usize> {
431 let start = range.start.min(self.total_count);
432 let range = start..range.end.clamp(start, self.total_count);
433 let mut cached = self.range_index.borrow_mut();
434 if cached.as_ref().is_none_or(|index| index.range != range) {
435 let indices = range
436 .clone()
437 .map(|index| (self.get_key(index).to_slot_id(), index))
438 .collect();
439 *cached = Some(RangeKeyIndex { range, indices });
440 }
441 cached
442 .as_ref()
443 .and_then(|index| index.indices.get(&slot_id).copied())
444 }
445
446 fn find_interval(&self, index: usize) -> Option<(&LazyListInterval, usize)> {
447 if self.intervals.is_empty() || index >= self.total_count {
448 return None;
449 }
450
451 let pos = self
452 .intervals
453 .partition_point(|interval| interval.start_index + interval.count <= index);
454
455 if pos < self.intervals.len() {
456 let interval = &self.intervals[pos];
457 if index >= interval.start_index && index < interval.start_index + interval.count {
458 let local_index = index - interval.start_index;
459 return Some((interval, local_index));
460 }
461 }
462 None
463 }
464}
465
466impl Default for LazyListIntervalContent {
467 fn default() -> Self {
468 Self::new()
469 }
470}
471
472impl LazyListScope for LazyListIntervalContent {
473 fn item_keyed<F>(&mut self, key: Option<u64>, content_type: Option<u64>, content: F)
474 where
475 F: Fn() + 'static,
476 {
477 self.invalidate_cache();
478 let start_index = self.total_count;
479 self.intervals.push(LazyListInterval {
480 start_index,
481 count: 1,
482 key: key.map(|k| Rc::new(move |_| k) as Rc<dyn Fn(usize) -> u64>),
483 content_type: content_type.map(|t| Rc::new(move |_| t) as Rc<dyn Fn(usize) -> u64>),
484 span: None,
485 content: Rc::new(move |_| content()),
486 });
487 self.total_count += 1;
488 }
489
490 fn items<I, F>(&mut self, items: I, item_content: F)
491 where
492 I: Into<LazyItems>,
493 F: Fn(usize) + 'static,
494 {
495 let items = items.into();
496 let count = items.count();
497 if count == 0 {
498 return;
499 }
500
501 self.invalidate_cache();
502 let start_index = self.total_count;
503 self.intervals.push(LazyListInterval {
504 start_index,
505 count,
506 key: items.key,
507 content_type: items.content_type,
508 span: items.span,
509 content: Rc::new(item_content),
510 });
511 self.total_count += count;
512 }
513
514 fn sticky_header_keyed<F>(&mut self, key: Option<u64>, content_type: Option<u64>, content: F)
515 where
516 F: Fn() + 'static,
517 {
518 self.header_indices.push(self.total_count);
519 self.item_keyed(key, content_type, content);
520 }
521}
522
523use crate::lazy::item_provider::LazyLayoutItemProvider;
524
525/// Implements [`LazyLayoutItemProvider`] to formalize the item factory contract.
526/// This provides the same functionality as the existing methods but through
527/// the standardized trait interface.
528impl LazyLayoutItemProvider for LazyListIntervalContent {
529 fn item_count(&self) -> usize {
530 self.total_count
531 }
532
533 fn get_key(&self, index: usize) -> u64 {
534 LazyListIntervalContent::get_key(self, index).to_slot_id()
535 }
536
537 fn get_content_type(&self, index: usize) -> Option<u64> {
538 LazyListIntervalContent::get_content_type(self, index)
539 }
540
541 fn get_index(&self, key: u64) -> Option<usize> {
542 self.get_index_by_slot_id(key)
543 }
544}
545
546/// Extension trait for adding convenience methods to [`LazyListScope`].
547///
548/// Provides ergonomic APIs for common use cases with different performance tradeoffs:
549///
550/// | Method | Upfront Cost | Use Case |
551/// |--------|--------------|----------|
552/// | `items_slice` | O(n) copy | Convenience, small data |
553/// | `items_slice_rc` | O(1) | Data already in `Rc<[T]>` |
554/// | `items_with_provider` | O(1) | Lazy on-demand access |
555pub trait LazyListScopeExt: LazyListScope {
556 /// Adds items from a slice with an item-aware content closure.
557 ///
558 /// # ⚠️ Performance Warning
559 ///
560 /// **This method performs an O(n) allocation and copy of the entire slice upfront.**
561 ///
562 /// This copy is required to satisfy Rust's `'static` closure requirements for
563 /// the lazy list item factory. For small lists (< 1000 items) this is typically
564 /// acceptable, but for large datasets consider these alternatives:
565 ///
566 /// | Alternative | When to Use |
567 /// |-------------|-------------|
568 /// | `items_slice_rc` | Data is already in `Rc<[T]>` - **zero copy** |
569 /// | `items_vec` | Data is in a `Vec<T>` you can give up ownership of - **efficient** |
570 /// | `items_with_provider` | Need lazy on-demand access - **zero copy** |
571 ///
572 /// After the initial copy, the closure captures a reference-counted pointer,
573 /// so subsequent Rc clones are O(1).
574 ///
575 /// # Example
576 ///
577 /// ```rust,ignore
578 /// let data = vec!["Apple", "Banana", "Cherry"];
579 /// scope.items_slice(&data, |item| {
580 /// Text(item.to_string(), Modifier::empty());
581 /// });
582 /// ```
583 fn items_slice<T, F>(&mut self, items: &[T], item_content: F)
584 where
585 T: Clone + 'static,
586 F: Fn(&T) + 'static,
587 {
588 let items_rc: Rc<[T]> = items.to_vec().into();
589 self.items(items.len(), move |index| {
590 if let Some(item) = items_rc.get(index) {
591 item_content(item);
592 }
593 });
594 }
595
596 /// Adds items from a `Vec<T>`, taking ownership.
597 ///
598 /// **Efficient ownership transfer**: Uses `Rc::from(vec)` which avoids copying
599 /// elements if the allocation fits (or does a simple realloc).
600 /// Use this when you have a `Vec` and want to pass it to the list.
601 ///
602 /// # Example
603 ///
604 /// ```rust,ignore
605 /// let data = vec!["Apple".to_string(), "Banana".to_string()];
606 /// scope.items_vec(data, |item| {
607 /// Text(item.to_string(), Modifier::empty());
608 /// });
609 /// ```
610 fn items_vec<T, F>(&mut self, items: Vec<T>, item_content: F)
611 where
612 T: 'static,
613 F: Fn(&T) + 'static,
614 {
615 let len = items.len();
616 let items_rc: Rc<[T]> = Rc::from(items);
617 self.items(len, move |index| {
618 if let Some(item) = items_rc.get(index) {
619 item_content(item);
620 }
621 });
622 }
623
624 /// Adds indexed items from a collection (Slice, Vec, or Rc).
625 ///
626 /// This method is generic over the input type `L` which must be convertible to `Rc<[T]>`.
627 /// This allows for efficient ownership transfer (zero-copy for `Vec` and `Rc`) or
628 /// convenient usage with slices (which will perform a copy).
629 ///
630 /// # Performance Note
631 ///
632 /// - **`Vec<T>`**: Zero-copy (ownership transfer). Efficient.
633 /// - **`Rc<[T]>`**: Zero-copy (ownership transfer). Efficient.
634 /// - **`&[T]`**: **O(N) copy**. Convenient for small lists, but avoid for large datasets.
635 ///
636 /// # Example
637 ///
638 /// ```rust,ignore
639 /// // Efficient Vec usage (zero-copy)
640 /// let data = vec!["Apple".to_string(), "Banana".to_string()];
641 /// scope.items_indexed(data, |index, item| { ... });
642 ///
643 /// // Slice usage (performs copy)
644 /// let data_slice = &["Apple", "Banana"];
645 /// scope.items_indexed(data_slice, |index, item| { ... });
646 /// ```
647 fn items_indexed<T, L, F>(&mut self, items: L, item_content: F)
648 where
649 T: 'static,
650 L: Into<Rc<[T]>>,
651 F: Fn(usize, &T) + 'static,
652 {
653 let items_rc: Rc<[T]> = items.into();
654 self.items(items_rc.len(), move |index| {
655 if let Some(item) = items_rc.get(index) {
656 item_content(index, item);
657 }
658 });
659 }
660
661 /// Adds items from a pre-existing `Rc<[T]>` without cloning.
662 ///
663 /// **Zero-copy optimization**: If you already have your data in an `Rc<[T]>`,
664 /// use this method to avoid the O(n) clone that `items_slice` performs.
665 ///
666 /// # Example
667 ///
668 /// ```rust,ignore
669 /// let data: Rc<[String]> = Rc::from(vec!["Apple".into(), "Banana".into()]);
670 /// scope.items_slice_rc(Rc::clone(&data), |item| {
671 /// Text(item.to_string(), Modifier::empty());
672 /// });
673 /// ```
674 fn items_slice_rc<T, F>(&mut self, items: Rc<[T]>, item_content: F)
675 where
676 T: 'static,
677 F: Fn(&T) + 'static,
678 {
679 let len = items.len();
680 self.items(len, move |index| {
681 if let Some(item) = items.get(index) {
682 item_content(item);
683 }
684 });
685 }
686
687 /// Adds indexed items from a pre-existing `Rc<[T]>` without cloning.
688 ///
689 /// **Zero-copy optimization**: If you already have your data in an `Rc<[T]>`,
690 /// use this method to avoid the O(n) clone that `items_indexed` performs.
691 ///
692 /// # Example
693 ///
694 /// ```rust,ignore
695 /// let data: Rc<[String]> = Rc::from(vec!["Apple".into(), "Banana".into()]);
696 /// scope.items_indexed_rc(Rc::clone(&data), |index, item| {
697 /// Text(format!("{}. {}", index + 1, item), Modifier::empty());
698 /// });
699 /// ```
700 fn items_indexed_rc<T, F>(&mut self, items: Rc<[T]>, item_content: F)
701 where
702 T: 'static,
703 F: Fn(usize, &T) + 'static,
704 {
705 let len = items.len();
706 self.items(len, move |index| {
707 if let Some(item) = items.get(index) {
708 item_content(index, item);
709 }
710 });
711 }
712
713 /// Adds items using a provider function for on-demand data access.
714 ///
715 /// **Zero-allocation pattern**: Instead of storing data, the provider function
716 /// is called lazily when each item is rendered. This avoids any upfront
717 /// allocation or cloning.
718 ///
719 /// The provider should return `Some(T)` for valid indices and `None` for
720 /// out-of-bounds access. The item is passed by value to the content closure.
721 ///
722 /// # Example
723 ///
724 /// ```rust,ignore
725 /// let data = vec!["Apple", "Banana", "Cherry"];
726 /// scope.items_with_provider(
727 /// data.len(),
728 /// move |index| data.get(index).copied(),
729 /// |item| {
730 /// Text(item.to_string(), Modifier::empty());
731 /// },
732 /// );
733 /// ```
734 fn items_with_provider<T, P, F>(&mut self, count: usize, provider: P, item_content: F)
735 where
736 T: 'static,
737 P: Fn(usize) -> Option<T> + 'static,
738 F: Fn(T) + 'static,
739 {
740 self.items(count, move |index| {
741 if let Some(item) = provider(index) {
742 item_content(item);
743 }
744 });
745 }
746
747 /// Adds indexed items using a provider function for on-demand data access.
748 ///
749 /// **Zero-allocation pattern**: Instead of storing data, the provider function
750 /// is called lazily when each item is rendered. This avoids any upfront
751 /// allocation or cloning.
752 ///
753 /// # Example
754 ///
755 /// ```rust,ignore
756 /// let data = vec!["Apple", "Banana", "Cherry"];
757 /// scope.items_indexed_with_provider(
758 /// data.len(),
759 /// move |index| data.get(index).copied(),
760 /// |index, item| {
761 /// Text(format!("{}. {}", index + 1, item), Modifier::empty());
762 /// },
763 /// );
764 /// ```
765 fn items_indexed_with_provider<T, P, F>(&mut self, count: usize, provider: P, item_content: F)
766 where
767 T: 'static,
768 P: Fn(usize) -> Option<T> + 'static,
769 F: Fn(usize, T) + 'static,
770 {
771 self.items(count, move |index| {
772 if let Some(item) = provider(index) {
773 item_content(index, item);
774 }
775 });
776 }
777}
778
779impl<T: LazyListScope + ?Sized> LazyListScopeExt for T {}
780
781#[cfg(test)]
782#[path = "tests/lazy_list_scope_tests.rs"]
783mod tests;