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