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