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