uniform_list.rs

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