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