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