uniform_list.rs

  1use crate::{
  2    point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element,
  3    ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
  4    Point, Size, StyleRefinement, Styled, ViewContext,
  5};
  6use parking_lot::Mutex;
  7use smallvec::SmallVec;
  8use std::{cmp, mem, ops::Range, sync::Arc};
  9use taffy::style::Overflow;
 10
 11/// uniform_list provides lazy rendering for a set of items that are of uniform height.
 12/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
 13/// uniform_list will only render the visibile subset of items.
 14pub fn uniform_list<I, V, C>(
 15    id: I,
 16    item_count: usize,
 17    f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<C>,
 18) -> UniformList<V>
 19where
 20    I: Into<ElementId>,
 21    V: 'static,
 22    C: Component<V>,
 23{
 24    let id = id.into();
 25    let mut style = StyleRefinement::default();
 26    style.overflow.y = Some(Overflow::Hidden);
 27
 28    UniformList {
 29        id: id.clone(),
 30        style,
 31        item_count,
 32        item_to_measure_index: 0,
 33        render_items: Box::new(move |view, visible_range, cx| {
 34            f(view, visible_range, cx)
 35                .into_iter()
 36                .map(|component| component.render())
 37                .collect()
 38        }),
 39        interactivity: Interactivity {
 40            element_id: Some(id.into()),
 41            ..Default::default()
 42        },
 43        scroll_handle: None,
 44    }
 45}
 46
 47pub struct UniformList<V: 'static> {
 48    id: ElementId,
 49    style: StyleRefinement,
 50    item_count: usize,
 51    item_to_measure_index: usize,
 52    render_items: Box<
 53        dyn for<'a> Fn(
 54            &'a mut V,
 55            Range<usize>,
 56            &'a mut ViewContext<V>,
 57        ) -> SmallVec<[AnyElement<V>; 64]>,
 58    >,
 59    interactivity: Interactivity<V>,
 60    scroll_handle: Option<UniformListScrollHandle>,
 61}
 62
 63#[derive(Clone, Default)]
 64pub struct UniformListScrollHandle(Arc<Mutex<Option<ScrollHandleState>>>);
 65
 66#[derive(Clone, Debug)]
 67struct ScrollHandleState {
 68    item_height: Pixels,
 69    list_height: Pixels,
 70    scroll_offset: Arc<Mutex<Point<Pixels>>>,
 71}
 72
 73impl UniformListScrollHandle {
 74    pub fn new() -> Self {
 75        Self(Arc::new(Mutex::new(None)))
 76    }
 77
 78    pub fn scroll_to_item(&self, ix: usize) {
 79        if let Some(state) = &*self.0.lock() {
 80            let mut scroll_offset = state.scroll_offset.lock();
 81            let item_top = state.item_height * ix;
 82            let item_bottom = item_top + state.item_height;
 83            let scroll_top = -scroll_offset.y;
 84            if item_top < scroll_top {
 85                scroll_offset.y = -item_top;
 86            } else if item_bottom > scroll_top + state.list_height {
 87                scroll_offset.y = -(item_bottom - state.list_height);
 88            }
 89        }
 90    }
 91}
 92
 93impl<V: 'static> Styled for UniformList<V> {
 94    fn style(&mut self) -> &mut StyleRefinement {
 95        &mut self.style
 96    }
 97}
 98
 99#[derive(Default)]
100pub struct UniformListState {
101    interactive: InteractiveElementState,
102    item_size: Size<Pixels>,
103}
104
105impl<V: 'static> Element<V> for UniformList<V> {
106    type ElementState = UniformListState;
107
108    fn element_id(&self) -> Option<crate::ElementId> {
109        Some(self.id.clone())
110    }
111
112    fn initialize(
113        &mut self,
114        view_state: &mut V,
115        element_state: Option<Self::ElementState>,
116        cx: &mut ViewContext<V>,
117    ) -> Self::ElementState {
118        if let Some(mut element_state) = element_state {
119            element_state.interactive = self
120                .interactivity
121                .initialize(Some(element_state.interactive), cx);
122            element_state
123        } else {
124            let item_size = self.measure_item(view_state, None, cx);
125            UniformListState {
126                interactive: self.interactivity.initialize(None, cx),
127                item_size,
128            }
129        }
130    }
131
132    fn layout(
133        &mut self,
134        _view_state: &mut V,
135        element_state: &mut Self::ElementState,
136        cx: &mut ViewContext<V>,
137    ) -> LayoutId {
138        let max_items = self.item_count;
139        let item_size = element_state.item_size;
140        let rem_size = cx.rem_size();
141
142        self.interactivity
143            .layout(&mut element_state.interactive, cx, |style, cx| {
144                cx.request_measured_layout(
145                    style,
146                    rem_size,
147                    move |known_dimensions: Size<Option<Pixels>>,
148                          available_space: Size<AvailableSpace>| {
149                        let desired_height = item_size.height * max_items;
150                        let width = known_dimensions
151                            .width
152                            .unwrap_or(match available_space.width {
153                                AvailableSpace::Definite(x) => x,
154                                AvailableSpace::MinContent | AvailableSpace::MaxContent => {
155                                    item_size.width
156                                }
157                            });
158                        let height = match available_space.height {
159                            AvailableSpace::Definite(x) => desired_height.min(x),
160                            AvailableSpace::MinContent | AvailableSpace::MaxContent => {
161                                desired_height
162                            }
163                        };
164                        size(width, height)
165                    },
166                )
167            })
168    }
169
170    fn paint(
171        &mut self,
172        bounds: Bounds<crate::Pixels>,
173        view_state: &mut V,
174        element_state: &mut Self::ElementState,
175        cx: &mut ViewContext<V>,
176    ) {
177        let style =
178            self.interactivity
179                .compute_style(Some(bounds), &mut element_state.interactive, cx);
180        let border = style.border_widths.to_pixels(cx.rem_size());
181        let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
182
183        let padded_bounds = Bounds::from_corners(
184            bounds.origin + point(border.left + padding.left, border.top + padding.top),
185            bounds.lower_right()
186                - point(border.right + padding.right, border.bottom + padding.bottom),
187        );
188
189        let item_size = element_state.item_size;
190        let content_size = Size {
191            width: padded_bounds.size.width,
192            height: item_size.height * self.item_count,
193        };
194
195        let mut interactivity = mem::take(&mut self.interactivity);
196        let shared_scroll_offset = element_state
197            .interactive
198            .scroll_offset
199            .get_or_insert_with(Arc::default)
200            .clone();
201
202        interactivity.paint(
203            bounds,
204            content_size,
205            &mut element_state.interactive,
206            cx,
207            |style, scroll_offset, cx| {
208                let border = style.border_widths.to_pixels(cx.rem_size());
209                let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
210
211                let padded_bounds = Bounds::from_corners(
212                    bounds.origin + point(border.left + padding.left, border.top + padding.top),
213                    bounds.lower_right()
214                        - point(border.right + padding.right, border.bottom + padding.bottom),
215                );
216
217                cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
218                    style.paint(bounds, cx);
219
220                    if self.item_count > 0 {
221                        let item_height = self
222                            .measure_item(view_state, Some(padded_bounds.size.width), cx)
223                            .height;
224                        if let Some(scroll_handle) = self.scroll_handle.clone() {
225                            scroll_handle.0.lock().replace(ScrollHandleState {
226                                item_height,
227                                list_height: padded_bounds.size.height,
228                                scroll_offset: shared_scroll_offset,
229                            });
230                        }
231                        let visible_item_count = if item_height > px(0.) {
232                            (padded_bounds.size.height / item_height).ceil() as usize + 1
233                        } else {
234                            0
235                        };
236
237                        let first_visible_element_ix =
238                            (-scroll_offset.y / item_height).floor() as usize;
239                        let visible_range = first_visible_element_ix
240                            ..cmp::min(
241                                first_visible_element_ix + visible_item_count,
242                                self.item_count,
243                            );
244
245                        let mut items = (self.render_items)(view_state, visible_range.clone(), cx);
246                        cx.with_z_index(1, |cx| {
247                            for (item, ix) in items.iter_mut().zip(visible_range) {
248                                let item_origin = padded_bounds.origin
249                                    + point(px(0.), item_height * ix + scroll_offset.y);
250                                let available_space = size(
251                                    AvailableSpace::Definite(padded_bounds.size.width),
252                                    AvailableSpace::Definite(item_height),
253                                );
254                                item.draw(item_origin, available_space, view_state, cx);
255                            }
256                        });
257                    }
258                })
259            },
260        );
261        self.interactivity = interactivity;
262    }
263}
264
265impl<V> UniformList<V> {
266    pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
267        self.item_to_measure_index = item_index.unwrap_or(0);
268        self
269    }
270
271    fn measure_item(
272        &self,
273        view_state: &mut V,
274        list_width: Option<Pixels>,
275        cx: &mut ViewContext<V>,
276    ) -> Size<Pixels> {
277        if self.item_count == 0 {
278            return Size::default();
279        }
280
281        let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
282        let mut items = (self.render_items)(view_state, item_ix..item_ix + 1, cx);
283        let mut item_to_measure = items.pop().unwrap();
284        let available_space = size(
285            list_width.map_or(AvailableSpace::MinContent, |width| {
286                AvailableSpace::Definite(width)
287            }),
288            AvailableSpace::MinContent,
289        );
290        item_to_measure.measure(available_space, view_state, cx)
291    }
292
293    pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
294        self.scroll_handle = Some(handle);
295        self
296    }
297}
298
299impl<V> InteractiveComponent<V> for UniformList<V> {
300    fn interactivity(&mut self) -> &mut crate::Interactivity<V> {
301        &mut self.interactivity
302    }
303}
304
305impl<V: 'static> Component<V> for UniformList<V> {
306    fn render(self) -> AnyElement<V> {
307        AnyElement::new(self)
308    }
309}