Skip to main content

compose_rust/ui/
lazy.rs

1//! Lazy lists: the Renderer asks for the window it can show, and only that window is
2//! composed.
3//!
4//! The shape is Compose's: a list takes a builder that says what its items are, in one or
5//! more runs, and composes none of them itself.
6//!
7//! ```ignore
8//! LazyColumn().modifier(Modifier.fill_max_size()).content(|list| {
9//!     list.item(|| { Text("Header"); });
10//!     list.items_keyed(tasks.len(), move |index| ids[index].clone(), move |index| {
11//!         TaskRow(index);
12//!     });
13//! });
14//! ```
15//!
16//! On the wire it is the windowing the `rsx!` lists use: the list declares how many items
17//! there are, the Renderer reports the range it shows, and exactly that range is composed,
18//! each item in a `Box` carrying its key. Scrolling runs the list's items and nothing
19//! above it, because the range is a state only the items read.
20
21#![allow(non_snake_case)]
22
23use super::{Attrs, Modifier, Mods, emit_node, push_modifiers, widget_key};
24use crate::RangeRequest;
25use crate::runtime::__private::{RestartGroup, start_restart_group};
26use crate::runtime::composer::{EventCallback, OwnedAttr};
27use crate::runtime::{MutableState, group_guard, key, mutable_state_of, remember};
28use crate::schema::WidgetKind;
29use std::cell::RefCell;
30use std::rc::Rc;
31
32/// One run of items.
33struct Interval {
34    count: usize,
35    key: Option<Rc<dyn Fn(usize) -> String>>,
36    content: Rc<dyn Fn(usize)>,
37}
38
39/// What a lazy list's builder is handed. Compose's `LazyListScope`.
40#[derive(Default)]
41pub struct LazyListScope {
42    intervals: Vec<Interval>,
43}
44
45impl LazyListScope {
46    /// One item.
47    pub fn item(&mut self, content: impl Fn() + 'static) {
48        self.intervals.push(Interval {
49            count: 1,
50            key: None,
51            content: Rc::new(move |_| content()),
52        });
53    }
54
55    /// `count` items, each composed by `content` with its index in this run. An item's
56    /// key is its position in the whole list, so items that move are rebuilt; use
57    /// [`LazyListScope::items_keyed`] for a list whose items move.
58    pub fn items(&mut self, count: usize, content: impl Fn(usize) + 'static) {
59        self.intervals.push(Interval {
60            count,
61            key: None,
62            content: Rc::new(content),
63        });
64    }
65
66    /// The same, with a stable key per item: an item that moves keeps its node and what
67    /// it remembered.
68    pub fn items_keyed(
69        &mut self,
70        count: usize,
71        key: impl Fn(usize) -> String + 'static,
72        content: impl Fn(usize) + 'static,
73    ) {
74        self.intervals.push(Interval {
75            count,
76            key: Some(Rc::new(key)),
77            content: Rc::new(content),
78        });
79    }
80
81    fn total(&self) -> usize {
82        self.intervals.iter().map(|interval| interval.count).sum()
83    }
84}
85
86/// Which list it is.
87#[derive(Clone, Copy, PartialEq, Eq)]
88enum Kind {
89    Column,
90    Row,
91    Grid,
92}
93
94/// A lazy list and its builder.
95pub struct LazyList {
96    kind: Kind,
97    modifier: Modifier,
98    columns: Option<u32>,
99    min_column_width: Option<f32>,
100    done: bool,
101}
102
103/// A vertical list that composes only the items on screen. Compose's `LazyColumn`.
104pub fn LazyColumn() -> LazyList {
105    LazyList::new(Kind::Column)
106}
107
108/// A horizontal list that composes only the items on screen. Compose's `LazyRow`.
109pub fn LazyRow() -> LazyList {
110    LazyList::new(Kind::Row)
111}
112
113/// A grid that composes only the rows on screen. Compose's `LazyVerticalGrid`; say how
114/// wide a column is with `columns` or `min_column_width`.
115pub fn LazyGrid() -> LazyList {
116    LazyList::new(Kind::Grid)
117}
118
119impl LazyList {
120    fn new(kind: Kind) -> Self {
121        Self {
122            kind,
123            modifier: Modifier::new(),
124            columns: None,
125            min_column_width: None,
126            done: false,
127        }
128    }
129
130    pub fn modifier(mut self, modifier: Modifier) -> Self {
131        self.modifier = modifier;
132        self
133    }
134
135    /// A grid's fixed number of columns. Compose's `GridCells.Fixed`.
136    pub fn columns(mut self, columns: u32) -> Self {
137        self.columns = Some(columns);
138        self
139    }
140
141    /// The narrowest a grid column may be. Compose's `GridCells.Adaptive`.
142    pub fn min_column_width(mut self, dp: f32) -> Self {
143        self.min_column_width = Some(dp);
144        self
145    }
146
147    /// Composes the list with the items `build` declares.
148    pub fn content(mut self, build: impl FnOnce(&mut LazyListScope)) {
149        self.done = true;
150        let mut scope = LazyListScope::default();
151        build(&mut scope);
152        self.compose(scope);
153    }
154
155    fn compose(&mut self, scope: LazyListScope) {
156        let (kind, modifier, columns, min_column_width) = (
157            self.kind,
158            self.modifier,
159            self.columns,
160            self.min_column_width,
161        );
162        let (widget, name) = match kind {
163            Kind::Column => (WidgetKind::LazyColumn, "compose_rust::ui::LazyColumn"),
164            Kind::Row => (WidgetKind::LazyRow, "compose_rust::ui::LazyRow"),
165            Kind::Grid => (WidgetKind::LazyGrid, "compose_rust::ui::LazyGrid"),
166        };
167        let _group = group_guard(widget_key(name));
168        // The window the Renderer last asked for. Only the items read it, so a scroll runs
169        // them and nothing else.
170        let range = remember(|| mutable_state_of((0_usize, 0_usize)));
171        let total = scope.total();
172        let request = range.clone();
173        let listener = EventCallback::Range(Rc::new(RefCell::new(move |asked: RangeRequest| {
174            request.set((asked.start(), asked.count()));
175        })));
176        let intervals = Rc::new(scope.intervals);
177        emit_node(
178            widget,
179            |attrs: &mut Attrs| {
180                push_modifiers(&modifier, Mods::Standard, attrs);
181                attrs.push(("item_count", OwnedAttr::Int(total as i64)));
182                if kind == Kind::Grid {
183                    attrs.push((
184                        "columns",
185                        columns.map_or(OwnedAttr::None, |count| OwnedAttr::Int(i64::from(count))),
186                    ));
187                    attrs.push((
188                        "min_column_width",
189                        min_column_width
190                            .map_or(OwnedAttr::None, |dp| OwnedAttr::Float(f64::from(dp))),
191                    ));
192                }
193            },
194            vec![("onrangerequest", listener)],
195            || Items(range, intervals),
196        );
197    }
198}
199
200impl Drop for LazyList {
201    fn drop(&mut self) {
202        if !self.done && !std::thread::panicking() {
203            self.done = true;
204            self.compose(LazyListScope::default());
205        }
206    }
207}
208
209const ITEMS: u64 = widget_key("compose_rust::ui::LazyList::items");
210const ITEM: u64 = widget_key("compose_rust::ui::LazyList::item");
211const ITEM_BOX: u64 = widget_key("compose_rust::ui::LazyList::item_box");
212
213/// The items a list's builder declared, compared by identity: a list composed again
214/// declares them again, and a list whose window moved did not.
215#[derive(Clone)]
216struct SameIntervals(Rc<Vec<Interval>>);
217
218impl PartialEq for SameIntervals {
219    fn eq(&self, other: &Self) -> bool {
220        Rc::ptr_eq(&self.0, &other.0)
221    }
222}
223
224/// The window of items, as a scope of its own that the range state invalidates.
225fn Items(range: MutableState<(usize, usize)>, intervals: Rc<Vec<Interval>>) {
226    let scope: RestartGroup = start_restart_group(ITEMS);
227    let fresh = scope.param_changed(0, &SameIntervals(Rc::clone(&intervals)));
228    if !scope.must_run() && !fresh {
229        scope.skip();
230        return;
231    }
232    {
233        let (range, intervals) = (range.clone(), Rc::clone(&intervals));
234        scope.set_restart(move || Items(range.clone(), Rc::clone(&intervals)));
235    }
236    let total: usize = intervals.iter().map(|interval| interval.count).sum();
237    let (start, count) = range.get();
238    let first = start.min(total);
239    let last = first.saturating_add(count).min(total);
240    for index in first..last {
241        let mut offset = index;
242        let Some(interval) = intervals.iter().find(|interval| {
243            if offset < interval.count {
244                true
245            } else {
246                offset -= interval.count;
247                false
248            }
249        }) else {
250            continue;
251        };
252        let item_key = interval
253            .key
254            .as_ref()
255            .map_or_else(|| index.to_string(), |key| key(offset));
256        let content = Rc::clone(&interval.content);
257        let local = offset;
258        key(item_key.clone(), || {
259            let _group = group_guard(ITEM_BOX);
260            emit_node(
261                WidgetKind::Box,
262                |attrs: &mut Attrs| attrs.push(("item_key", OwnedAttr::Text(item_key))),
263                Vec::new(),
264                || Item(content, local),
265            );
266        });
267    }
268}
269
270/// One item's content, compared by the closure that composes it and its index.
271#[derive(Clone)]
272struct SameItem(Rc<dyn Fn(usize)>, usize);
273
274impl PartialEq for SameItem {
275    fn eq(&self, other: &Self) -> bool {
276        Rc::ptr_eq(&self.0, &other.0) && self.1 == other.1
277    }
278}
279
280/// One item, as a scope of its own: a state its content reads runs this item again and
281/// no other.
282fn Item(content: Rc<dyn Fn(usize)>, index: usize) {
283    let scope: RestartGroup = start_restart_group(ITEM);
284    let changed = scope.param_changed(0, &SameItem(Rc::clone(&content), index));
285    if !scope.must_run() && !changed {
286        scope.skip();
287        return;
288    }
289    {
290        let content = Rc::clone(&content);
291        scope.set_restart(move || Item(Rc::clone(&content), index));
292    }
293    content(index);
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn fr39_the_items_of_several_runs_are_one_index_space() {
302        let mut scope = LazyListScope::default();
303        scope.item(|| {});
304        scope.items(3, |_| {});
305        scope.items_keyed(2, |index| format!("k{index}"), |_| {});
306        assert_eq!(scope.total(), 6);
307    }
308}