list.rs

  1use crate::{
  2    geometry::{
  3        rect::RectF,
  4        vector::{vec2f, Vector2F},
  5    },
  6    json::json,
  7    sum_tree::{self, Bias, SumTree},
  8    DebugContext, Element, ElementBox, ElementRc, Event, EventContext, LayoutContext, PaintContext,
  9    RenderContext, SizeConstraint, View,
 10};
 11use std::{cell::RefCell, ops::Range, rc::Rc};
 12
 13pub struct List {
 14    state: ListState,
 15}
 16
 17#[derive(Clone)]
 18pub struct ListState(Rc<RefCell<StateInner>>);
 19
 20#[derive(Eq, PartialEq)]
 21pub enum Orientation {
 22    Top,
 23    Bottom,
 24}
 25
 26struct StateInner {
 27    last_layout_width: Option<f32>,
 28    elements: Vec<Option<ElementRc>>,
 29    heights: SumTree<ElementHeight>,
 30    scroll_position: f32,
 31    orientation: Orientation,
 32    scroll_handler: Option<Box<dyn FnMut(Range<usize>, &mut EventContext)>>,
 33}
 34
 35#[derive(Clone, Debug)]
 36enum ElementHeight {
 37    Pending,
 38    Ready(f32),
 39}
 40
 41#[derive(Clone, Debug, Default, PartialEq)]
 42struct ElementHeightSummary {
 43    count: usize,
 44    pending_count: usize,
 45    height: f32,
 46}
 47
 48#[derive(Clone, Debug, Default)]
 49struct Count(usize);
 50
 51#[derive(Clone, Debug, Default)]
 52struct PendingCount(usize);
 53
 54#[derive(Clone, Debug, Default)]
 55struct Height(f32);
 56
 57impl List {
 58    pub fn new<F, I, V>(state: ListState, cx: &RenderContext<V>, build_items: F) -> Self
 59    where
 60        F: Fn(Range<usize>) -> I,
 61        I: IntoIterator<Item = ElementBox>,
 62        V: View,
 63    {
 64        {
 65            let state = &mut *state.0.borrow_mut();
 66            if cx.refreshing {
 67                let elements = (build_items)(0..state.elements.len());
 68                state.last_layout_width = None;
 69                state.elements.clear();
 70                state
 71                    .elements
 72                    .extend(elements.into_iter().map(|e| Some(e.into())));
 73                state.heights = SumTree::new();
 74                state.heights.extend(
 75                    (0..state.elements.len()).map(|_| ElementHeight::Pending),
 76                    &(),
 77                );
 78            } else {
 79                let mut cursor = state.heights.cursor::<PendingCount, Count>();
 80                cursor.seek(&PendingCount(1), sum_tree::Bias::Left, &());
 81
 82                while cursor.item().is_some() {
 83                    let start_ix = cursor.sum_start().0;
 84                    while cursor.item().map_or(false, |h| h.is_pending()) {
 85                        cursor.next(&());
 86                    }
 87                    let end_ix = cursor.sum_start().0;
 88                    if end_ix > start_ix {
 89                        state.elements.splice(
 90                            start_ix..end_ix,
 91                            (build_items)(start_ix..end_ix)
 92                                .into_iter()
 93                                .map(|e| Some(e.into())),
 94                        );
 95                    }
 96
 97                    cursor.seek(&PendingCount(cursor.seek_start().0 + 1), Bias::Left, &());
 98                }
 99            }
100        }
101
102        Self { state }
103    }
104}
105
106impl Element for List {
107    type LayoutState = Vec<ElementRc>;
108
109    type PaintState = ();
110
111    fn layout(
112        &mut self,
113        constraint: SizeConstraint,
114        cx: &mut LayoutContext,
115    ) -> (Vector2F, Self::LayoutState) {
116        let state = &mut *self.state.0.borrow_mut();
117        let mut item_constraint = constraint;
118        item_constraint.min.set_y(0.);
119        item_constraint.max.set_y(f32::INFINITY);
120
121        let size = constraint.max;
122
123        let visible_top = state.scroll_top(size.y());
124        let visible_bottom = visible_top + size.y();
125
126        if state.last_layout_width == Some(constraint.max.x()) {
127            let mut old_heights = state.heights.cursor::<PendingCount, ElementHeightSummary>();
128            let mut new_heights = old_heights.slice(&PendingCount(1), sum_tree::Bias::Left, &());
129
130            while let Some(height) = old_heights.item() {
131                if height.is_pending() {
132                    let element = &mut state.elements[old_heights.sum_start().count];
133                    let element_size = element.as_mut().unwrap().layout(item_constraint, cx);
134                    new_heights.push(ElementHeight::Ready(element_size.y()), &());
135
136                    // Adjust scroll position to keep visible elements stable
137                    match state.orientation {
138                        Orientation::Top => {
139                            if new_heights.summary().height < visible_top {
140                                state.scroll_position += element_size.y();
141                            }
142                        }
143                        Orientation::Bottom => {
144                            if new_heights.summary().height - element_size.y() > visible_bottom {
145                                state.scroll_position += element_size.y();
146                            }
147                        }
148                    }
149
150                    old_heights.next(&());
151                } else {
152                    new_heights.push_tree(
153                        old_heights.slice(
154                            &PendingCount(old_heights.sum_start().pending_count + 1),
155                            Bias::Left,
156                            &(),
157                        ),
158                        &(),
159                    );
160                }
161            }
162
163            drop(old_heights);
164            state.heights = new_heights;
165        } else {
166            state.heights = SumTree::new();
167            for element in &mut state.elements {
168                let element = element.as_mut().unwrap();
169                let size = element.layout(item_constraint, cx);
170                state.heights.push(ElementHeight::Ready(size.y()), &());
171            }
172            state.last_layout_width = Some(constraint.max.x());
173        }
174
175        let visible_elements = state.elements[state.visible_range(size.y())]
176            .iter()
177            .map(|e| e.clone().unwrap())
178            .collect();
179        (size, visible_elements)
180    }
181
182    fn paint(
183        &mut self,
184        bounds: RectF,
185        visible_elements: &mut Self::LayoutState,
186        cx: &mut PaintContext,
187    ) {
188        cx.scene.push_layer(Some(bounds));
189        let state = &mut *self.state.0.borrow_mut();
190        let visible_range = state.visible_range(bounds.height());
191
192        let mut item_top = {
193            let mut cursor = state.heights.cursor::<Count, Height>();
194            cursor.seek(&Count(visible_range.start), Bias::Right, &());
195            cursor.sum_start().0
196        };
197        if state.orientation == Orientation::Bottom
198            && bounds.height() > state.heights.summary().height
199        {
200            item_top += bounds.height() - state.heights.summary().height;
201        }
202        let scroll_top = state.scroll_top(bounds.height());
203
204        for element in visible_elements {
205            let origin = bounds.origin() + vec2f(0., item_top - scroll_top);
206            element.paint(origin, cx);
207            item_top += element.size().y();
208        }
209        cx.scene.pop_layer();
210    }
211
212    fn dispatch_event(
213        &mut self,
214        event: &Event,
215        bounds: RectF,
216        visible_elements: &mut Self::LayoutState,
217        _: &mut (),
218        cx: &mut EventContext,
219    ) -> bool {
220        let mut handled = false;
221
222        let mut state = self.state.0.borrow_mut();
223        for element in visible_elements {
224            handled = element.dispatch_event(event, cx) || handled;
225        }
226
227        match event {
228            Event::ScrollWheel {
229                position,
230                delta,
231                precise,
232            } => {
233                if bounds.contains_point(*position) {
234                    if state.scroll(*position, *delta, *precise, bounds.height(), cx) {
235                        handled = true;
236                    }
237                }
238            }
239            _ => {}
240        }
241
242        handled
243    }
244
245    fn debug(
246        &self,
247        bounds: RectF,
248        visible_elements: &Self::LayoutState,
249        _: &(),
250        cx: &DebugContext,
251    ) -> serde_json::Value {
252        let state = self.state.0.borrow_mut();
253        let visible_range = state.visible_range(bounds.height());
254        let visible_elements = visible_elements
255            .iter()
256            .map(|e| e.debug(cx))
257            .collect::<Vec<_>>();
258        json!({
259            "visible_range": visible_range,
260            "visible_elements": visible_elements,
261            "scroll_position": state.scroll_position,
262        })
263    }
264}
265
266impl ListState {
267    pub fn new(element_count: usize, orientation: Orientation) -> Self {
268        let mut heights = SumTree::new();
269        heights.extend((0..element_count).map(|_| ElementHeight::Pending), &());
270        Self(Rc::new(RefCell::new(StateInner {
271            last_layout_width: None,
272            elements: (0..element_count).map(|_| None).collect(),
273            heights,
274            scroll_position: 0.,
275            orientation,
276            scroll_handler: None,
277        })))
278    }
279
280    pub fn splice(&self, old_range: Range<usize>, count: usize) {
281        let state = &mut *self.0.borrow_mut();
282
283        let mut old_heights = state.heights.cursor::<Count, ()>();
284        let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
285        old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
286
287        let old_elements = state.elements.splice(old_range, (0..count).map(|_| None));
288        drop(old_elements);
289
290        new_heights.extend((0..count).map(|_| ElementHeight::Pending), &());
291        new_heights.push_tree(old_heights.suffix(&()), &());
292        drop(old_heights);
293        state.heights = new_heights;
294    }
295
296    pub fn set_scroll_handler(
297        &mut self,
298        handler: impl FnMut(Range<usize>, &mut EventContext) + 'static,
299    ) {
300        self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
301    }
302}
303
304impl StateInner {
305    fn visible_range(&self, height: f32) -> Range<usize> {
306        let mut cursor = self.heights.cursor::<Height, Count>();
307        cursor.seek(&Height(self.scroll_top(height)), Bias::Right, &());
308        let start_ix = cursor.sum_start().0;
309        cursor.seek(&Height(self.scroll_top(height) + height), Bias::Left, &());
310        let end_ix = cursor.sum_start().0;
311        start_ix..self.elements.len().min(end_ix + 1)
312    }
313
314    fn scroll(
315        &mut self,
316        _: Vector2F,
317        delta: Vector2F,
318        precise: bool,
319        height: f32,
320        cx: &mut EventContext,
321    ) -> bool {
322        if !precise {
323            todo!("still need to handle non-precise scroll events from a mouse wheel");
324        }
325
326        let scroll_max = (self.heights.summary().height - height).max(0.);
327        let delta_y = match self.orientation {
328            Orientation::Top => -delta.y(),
329            Orientation::Bottom => delta.y(),
330        };
331        self.scroll_position = (self.scroll_position + delta_y).max(0.).min(scroll_max);
332
333        if self.scroll_handler.is_some() {
334            let range = self.visible_range(height);
335            self.scroll_handler.as_mut().unwrap()(range, cx);
336        }
337        cx.notify();
338
339        true
340    }
341
342    fn scroll_top(&self, height: f32) -> f32 {
343        match self.orientation {
344            Orientation::Top => self.scroll_position,
345            Orientation::Bottom => {
346                (self.heights.summary().height - height - self.scroll_position).max(0.)
347            }
348        }
349    }
350}
351
352impl ElementHeight {
353    fn is_pending(&self) -> bool {
354        matches!(self, ElementHeight::Pending)
355    }
356}
357
358impl sum_tree::Item for ElementHeight {
359    type Summary = ElementHeightSummary;
360
361    fn summary(&self) -> Self::Summary {
362        match self {
363            ElementHeight::Pending => ElementHeightSummary {
364                count: 1,
365                pending_count: 1,
366                height: 0.,
367            },
368            ElementHeight::Ready(height) => ElementHeightSummary {
369                count: 1,
370                pending_count: 0,
371                height: *height,
372            },
373        }
374    }
375}
376
377impl sum_tree::Summary for ElementHeightSummary {
378    type Context = ();
379
380    fn add_summary(&mut self, summary: &Self, _: &()) {
381        self.count += summary.count;
382        self.pending_count += summary.pending_count;
383        self.height += summary.height;
384    }
385}
386
387impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for ElementHeightSummary {
388    fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) {
389        sum_tree::Summary::add_summary(self, summary, &());
390    }
391}
392
393impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for Count {
394    fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) {
395        self.0 += summary.count;
396    }
397}
398
399impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for Count {
400    fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering {
401        self.0.cmp(&other.0)
402    }
403}
404
405impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for PendingCount {
406    fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) {
407        self.0 += summary.pending_count;
408    }
409}
410
411impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for PendingCount {
412    fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering {
413        self.0.cmp(&other.0)
414    }
415}
416
417impl<'a> sum_tree::Dimension<'a, ElementHeightSummary> for Height {
418    fn add_summary(&mut self, summary: &'a ElementHeightSummary, _: &()) {
419        self.0 += summary.height;
420    }
421}
422
423impl<'a> sum_tree::SeekDimension<'a, ElementHeightSummary> for Height {
424    fn cmp(&self, other: &Self, _: &()) -> std::cmp::Ordering {
425        self.0.partial_cmp(&other.0).unwrap()
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432    use crate::{elements::*, geometry::vector::vec2f, Entity};
433
434    #[crate::test(self)]
435    fn test_layout(cx: &mut crate::MutableAppContext) {
436        let mut presenter = cx.build_presenter(0, 0.);
437
438        let mut elements = vec![20., 30., 10.];
439        let state = ListState::new(elements.len(), Orientation::Top);
440
441        let mut list = List::new(
442            state.clone(),
443            &cx.build_render_context::<TestView>(0, 0, 0., false),
444            |range| elements[range].iter().copied().map(item),
445        )
446        .boxed();
447        let size = list.layout(
448            SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)),
449            &mut presenter.build_layout_context(cx),
450        );
451        assert_eq!(size, vec2f(100., 40.));
452        assert_eq!(
453            state.0.borrow().heights.summary(),
454            ElementHeightSummary {
455                count: 3,
456                pending_count: 0,
457                height: 60.
458            }
459        );
460
461        elements.splice(1..2, vec![40., 50.]);
462        elements.push(60.);
463        state.splice(1..2, 2);
464        state.splice(4..4, 1);
465        assert_eq!(
466            state.0.borrow().heights.summary(),
467            ElementHeightSummary {
468                count: 5,
469                pending_count: 3,
470                height: 30.
471            }
472        );
473
474        let mut list = List::new(
475            state.clone(),
476            &cx.build_render_context::<TestView>(0, 0, 0., false),
477            |range| elements[range].iter().copied().map(item),
478        )
479        .boxed();
480        let size = list.layout(
481            SizeConstraint::new(vec2f(0., 0.), vec2f(100., 40.)),
482            &mut presenter.build_layout_context(cx),
483        );
484        assert_eq!(size, vec2f(100., 40.));
485        assert_eq!(
486            state.0.borrow().heights.summary(),
487            ElementHeightSummary {
488                count: 5,
489                pending_count: 0,
490                height: 180.
491            }
492        );
493    }
494
495    fn item(height: f32) -> ElementBox {
496        ConstrainedBox::new(Empty::new().boxed())
497            .with_height(height)
498            .with_width(100.)
499            .boxed()
500    }
501
502    struct TestView;
503
504    impl Entity for TestView {
505        type Event = ();
506    }
507
508    impl View for TestView {
509        fn ui_name() -> &'static str {
510            "TestView"
511        }
512
513        fn render(&self, _: &mut RenderContext<'_, Self>) -> ElementBox {
514            unimplemented!()
515        }
516    }
517}