uniform_list.rs

  1use super::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint};
  2use crate::{
  3    geometry::{
  4        rect::RectF,
  5        vector::{vec2f, Vector2F},
  6    },
  7    json::{self, json},
  8    presenter::MeasurementContext,
  9    ElementBox, RenderContext, ScrollWheelEvent, View,
 10};
 11use json::ToJson;
 12use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
 13
 14#[derive(Clone, Default)]
 15pub struct UniformListState(Rc<RefCell<StateInner>>);
 16
 17#[derive(Debug)]
 18pub enum ScrollTarget {
 19    Show(usize),
 20    Center(usize),
 21}
 22
 23impl UniformListState {
 24    pub fn scroll_to(&self, scroll_to: ScrollTarget) {
 25        self.0.borrow_mut().scroll_to = Some(scroll_to);
 26    }
 27
 28    pub fn scroll_top(&self) -> f32 {
 29        self.0.borrow().scroll_top
 30    }
 31}
 32
 33#[derive(Default)]
 34struct StateInner {
 35    scroll_top: f32,
 36    scroll_to: Option<ScrollTarget>,
 37}
 38
 39pub struct LayoutState {
 40    scroll_max: f32,
 41    item_height: f32,
 42    items: Vec<ElementBox>,
 43}
 44
 45pub struct UniformList {
 46    state: UniformListState,
 47    item_count: usize,
 48    append_items: Box<dyn Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext)>,
 49    padding_top: f32,
 50    padding_bottom: f32,
 51    get_width_from_item: Option<usize>,
 52}
 53
 54impl UniformList {
 55    pub fn new<F, V>(
 56        state: UniformListState,
 57        item_count: usize,
 58        cx: &mut RenderContext<V>,
 59        append_items: F,
 60    ) -> Self
 61    where
 62        V: View,
 63        F: 'static + Fn(&mut V, Range<usize>, &mut Vec<ElementBox>, &mut RenderContext<V>),
 64    {
 65        let handle = cx.handle();
 66        Self {
 67            state,
 68            item_count,
 69            append_items: Box::new(move |range, items, cx| {
 70                if let Some(handle) = handle.upgrade(cx) {
 71                    cx.render(&handle, |view, cx| {
 72                        append_items(view, range, items, cx);
 73                    });
 74                }
 75            }),
 76            padding_top: 0.,
 77            padding_bottom: 0.,
 78            get_width_from_item: None,
 79        }
 80    }
 81
 82    pub fn with_width_from_item(mut self, item_ix: Option<usize>) -> Self {
 83        self.get_width_from_item = item_ix;
 84        self
 85    }
 86
 87    pub fn with_padding_top(mut self, padding: f32) -> Self {
 88        self.padding_top = padding;
 89        self
 90    }
 91
 92    pub fn with_padding_bottom(mut self, padding: f32) -> Self {
 93        self.padding_bottom = padding;
 94        self
 95    }
 96
 97    fn scroll(
 98        &self,
 99        _: Vector2F,
100        mut delta: Vector2F,
101        precise: bool,
102        scroll_max: f32,
103        cx: &mut EventContext,
104    ) -> bool {
105        if !precise {
106            delta *= 20.;
107        }
108
109        let mut state = self.state.0.borrow_mut();
110        state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max);
111        cx.notify();
112
113        true
114    }
115
116    fn autoscroll(&mut self, scroll_max: f32, list_height: f32, item_height: f32) {
117        let mut state = self.state.0.borrow_mut();
118
119        if let Some(scroll_to) = state.scroll_to.take() {
120            let item_ix;
121            let center;
122            match scroll_to {
123                ScrollTarget::Show(ix) => {
124                    item_ix = ix;
125                    center = false;
126                }
127                ScrollTarget::Center(ix) => {
128                    item_ix = ix;
129                    center = true;
130                }
131            }
132
133            let item_top = self.padding_top + item_ix as f32 * item_height;
134            let item_bottom = item_top + item_height;
135            if center {
136                let item_center = item_top + item_height / 2.;
137                state.scroll_top = (item_center - list_height / 2.).max(0.);
138            } else {
139                let scroll_bottom = state.scroll_top + list_height;
140                if item_top < state.scroll_top {
141                    state.scroll_top = item_top;
142                } else if item_bottom > scroll_bottom {
143                    state.scroll_top = item_bottom - list_height;
144                }
145            }
146        }
147
148        if state.scroll_top > scroll_max {
149            state.scroll_top = scroll_max;
150        }
151    }
152
153    fn scroll_top(&self) -> f32 {
154        self.state.0.borrow().scroll_top
155    }
156}
157
158impl Element for UniformList {
159    type LayoutState = LayoutState;
160    type PaintState = ();
161
162    fn layout(
163        &mut self,
164        constraint: SizeConstraint,
165        cx: &mut LayoutContext,
166    ) -> (Vector2F, Self::LayoutState) {
167        if constraint.max.y().is_infinite() {
168            unimplemented!(
169                "UniformList does not support being rendered with an unconstrained height"
170            );
171        }
172
173        let no_items = (
174            constraint.min,
175            LayoutState {
176                item_height: 0.,
177                scroll_max: 0.,
178                items: Default::default(),
179            },
180        );
181
182        if self.item_count == 0 {
183            return no_items;
184        }
185
186        let mut items = Vec::new();
187        let mut size = constraint.max;
188        let mut item_size;
189        let sample_item_ix;
190        let sample_item;
191        if let Some(sample_ix) = self.get_width_from_item {
192            (self.append_items)(sample_ix..sample_ix + 1, &mut items, cx);
193            sample_item_ix = sample_ix;
194
195            if let Some(mut item) = items.pop() {
196                item_size = item.layout(constraint, cx);
197                size.set_x(item_size.x());
198                sample_item = item;
199            } else {
200                return no_items;
201            }
202        } else {
203            (self.append_items)(0..1, &mut items, cx);
204            sample_item_ix = 0;
205            if let Some(mut item) = items.pop() {
206                item_size = item.layout(
207                    SizeConstraint::new(
208                        vec2f(constraint.max.x(), 0.0),
209                        vec2f(constraint.max.x(), f32::INFINITY),
210                    ),
211                    cx,
212                );
213                item_size.set_x(size.x());
214                sample_item = item
215            } else {
216                return no_items;
217            }
218        }
219
220        let item_constraint = SizeConstraint {
221            min: item_size,
222            max: vec2f(constraint.max.x(), item_size.y()),
223        };
224        let item_height = item_size.y();
225
226        let scroll_height = self.item_count as f32 * item_height;
227        if scroll_height < size.y() {
228            size.set_y(size.y().min(scroll_height).max(constraint.min.y()));
229        }
230
231        let scroll_height =
232            item_height * self.item_count as f32 + self.padding_top + self.padding_bottom;
233        let scroll_max = (scroll_height - size.y()).max(0.);
234        self.autoscroll(scroll_max, size.y(), item_height);
235
236        let start = cmp::min(
237            ((self.scroll_top() - self.padding_top) / item_height.max(1.)) as usize,
238            self.item_count,
239        );
240        let end = cmp::min(
241            self.item_count,
242            start + (size.y() / item_height.max(1.)).ceil() as usize + 1,
243        );
244
245        if (start..end).contains(&sample_item_ix) {
246            if sample_item_ix > start {
247                (self.append_items)(start..sample_item_ix, &mut items, cx);
248            }
249
250            items.push(sample_item);
251
252            if sample_item_ix < end {
253                (self.append_items)(sample_item_ix + 1..end, &mut items, cx);
254            }
255        } else {
256            (self.append_items)(start..end, &mut items, cx);
257        }
258
259        for item in &mut items {
260            let item_size = item.layout(item_constraint, cx);
261            if item_size.x() > size.x() {
262                size.set_x(item_size.x());
263            }
264        }
265
266        (
267            size,
268            LayoutState {
269                item_height,
270                scroll_max,
271                items,
272            },
273        )
274    }
275
276    fn paint(
277        &mut self,
278        bounds: RectF,
279        visible_bounds: RectF,
280        layout: &mut Self::LayoutState,
281        cx: &mut PaintContext,
282    ) -> Self::PaintState {
283        cx.scene.push_layer(Some(bounds));
284
285        let mut item_origin = bounds.origin()
286            - vec2f(
287                0.,
288                (self.state.scroll_top() - self.padding_top) % layout.item_height,
289            );
290
291        for item in &mut layout.items {
292            item.paint(item_origin, visible_bounds, cx);
293            item_origin += vec2f(0.0, layout.item_height);
294        }
295
296        cx.scene.pop_layer();
297    }
298
299    fn dispatch_event(
300        &mut self,
301        event: &Event,
302        bounds: RectF,
303        _: RectF,
304        layout: &mut Self::LayoutState,
305        _: &mut Self::PaintState,
306        cx: &mut EventContext,
307    ) -> bool {
308        let mut handled = false;
309        for item in &mut layout.items {
310            handled = item.dispatch_event(event, cx) || handled;
311        }
312
313        match event {
314            Event::ScrollWheel(ScrollWheelEvent {
315                position,
316                delta,
317                precise,
318            }) => {
319                if bounds.contains_point(*position) {
320                    if self.scroll(*position, *delta, *precise, layout.scroll_max, cx) {
321                        handled = true;
322                    }
323                }
324            }
325            _ => {}
326        }
327
328        handled
329    }
330
331    fn rect_for_text_range(
332        &self,
333        range: Range<usize>,
334        _: RectF,
335        _: RectF,
336        layout: &Self::LayoutState,
337        _: &Self::PaintState,
338        cx: &MeasurementContext,
339    ) -> Option<RectF> {
340        layout
341            .items
342            .iter()
343            .find_map(|child| child.rect_for_text_range(range.clone(), cx))
344    }
345
346    fn debug(
347        &self,
348        bounds: RectF,
349        layout: &Self::LayoutState,
350        _: &Self::PaintState,
351        cx: &crate::DebugContext,
352    ) -> json::Value {
353        json!({
354            "type": "UniformList",
355            "bounds": bounds.to_json(),
356            "scroll_max": layout.scroll_max,
357            "item_height": layout.item_height,
358            "items": layout.items.iter().map(|item| item.debug(cx)).collect::<Vec<json::Value>>()
359
360        })
361    }
362}