uniform_list.rs

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