uniform_table.rs

  1use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
  2
  3use smallvec::SmallVec;
  4
  5use crate::{
  6    AnyElement, App, AvailableSpace, Bounds, ContentMask, Div, Element, ElementId, GlobalElementId,
  7    Hitbox, InspectorElementId, Interactivity, IntoElement, IsZero as _, LayoutId, Length,
  8    Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled as _, Window, div, point, px,
  9    size,
 10};
 11
 12/// todo!
 13pub struct UniformTable<const COLS: usize> {
 14    id: ElementId,
 15    row_count: usize,
 16    render_rows:
 17        Rc<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]> + 'static>,
 18    interactivity: Interactivity,
 19    source_location: &'static std::panic::Location<'static>,
 20    item_to_measure_index: usize,
 21    scroll_handle: Option<UniformTableScrollHandle>, // todo! we either want to make our own or make a shared scroll handle between list and table
 22    sizings: [Length; COLS],
 23}
 24
 25/// TODO
 26#[track_caller]
 27pub fn uniform_table<const COLS: usize, F>(
 28    id: impl Into<ElementId>,
 29    row_count: usize,
 30    render_rows: F,
 31) -> UniformTable<COLS>
 32where
 33    F: 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>,
 34{
 35    let mut base_style = StyleRefinement::default();
 36    base_style.overflow.y = Some(Overflow::Scroll);
 37    let id = id.into();
 38
 39    let mut interactivity = Interactivity::new();
 40    interactivity.element_id = Some(id.clone());
 41
 42    UniformTable {
 43        id: id.clone(),
 44        row_count,
 45        render_rows: Rc::new(render_rows),
 46        interactivity: Interactivity {
 47            element_id: Some(id),
 48            base_style: Box::new(base_style),
 49            ..Interactivity::new()
 50        },
 51        source_location: core::panic::Location::caller(),
 52        item_to_measure_index: 0,
 53        scroll_handle: None,
 54        sizings: [Length::Auto; COLS],
 55    }
 56}
 57
 58impl<const COLS: usize> UniformTable<COLS> {
 59    /// todo!
 60    pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
 61        self.item_to_measure_index = item_index.unwrap_or(0);
 62        self
 63    }
 64}
 65
 66impl<const COLS: usize> IntoElement for UniformTable<COLS> {
 67    type Element = Self;
 68
 69    fn into_element(self) -> Self::Element {
 70        self
 71    }
 72}
 73
 74impl<const COLS: usize> Element for UniformTable<COLS> {
 75    type RequestLayoutState = ();
 76
 77    type PrepaintState = (Option<Hitbox>, SmallVec<[AnyElement; 32]>);
 78
 79    fn id(&self) -> Option<ElementId> {
 80        Some(self.id.clone())
 81    }
 82
 83    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
 84        Some(self.source_location)
 85    }
 86
 87    fn request_layout(
 88        &mut self,
 89        global_id: Option<&GlobalElementId>,
 90        inspector_id: Option<&InspectorElementId>,
 91        window: &mut Window,
 92        cx: &mut App,
 93    ) -> (LayoutId, Self::RequestLayoutState) {
 94        let measure_cx = MeasureContext::new(self);
 95        let item_size = measure_cx.measure_item(AvailableSpace::MinContent, None, window, cx);
 96        let layout_id = self.interactivity.request_layout(
 97            global_id,
 98            inspector_id,
 99            window,
100            cx,
101            |style, window, _cx| {
102                window.with_text_style(style.text_style().cloned(), |window| {
103                    window.request_measured_layout(
104                        style,
105                        move |known_dimensions, available_space, window, cx| {
106                            let desired_height = item_size.height * measure_cx.row_count;
107                            let width =
108                                known_dimensions
109                                    .width
110                                    .unwrap_or(match available_space.width {
111                                        AvailableSpace::Definite(x) => x,
112                                        AvailableSpace::MinContent | AvailableSpace::MaxContent => {
113                                            item_size.width
114                                        }
115                                    });
116                            let height = match available_space.height {
117                                AvailableSpace::Definite(height) => desired_height.min(height),
118                                AvailableSpace::MinContent | AvailableSpace::MaxContent => {
119                                    desired_height
120                                }
121                            };
122                            size(width, height)
123                        },
124                    )
125                })
126            },
127        );
128
129        (layout_id, ())
130    }
131
132    fn prepaint(
133        &mut self,
134        global_id: Option<&GlobalElementId>,
135        inspector_id: Option<&InspectorElementId>,
136        bounds: Bounds<Pixels>,
137        _request_layout: &mut Self::RequestLayoutState,
138        window: &mut Window,
139        cx: &mut App,
140    ) -> Self::PrepaintState {
141        let style = self
142            .interactivity
143            .compute_style(global_id, None, window, cx);
144        let border = style.border_widths.to_pixels(window.rem_size());
145        let padding = style
146            .padding
147            .to_pixels(bounds.size.into(), window.rem_size());
148
149        let padded_bounds = Bounds::from_corners(
150            bounds.origin + point(border.left + padding.left, border.top + padding.top),
151            bounds.bottom_right()
152                - point(border.right + padding.right, border.bottom + padding.bottom),
153        );
154
155        let can_scroll_horizontally = true;
156
157        let mut column_widths = [Pixels::default(); COLS];
158        let longest_row_size = MeasureContext::new(self).measure_item(
159            AvailableSpace::Definite(bounds.size.width),
160            Some(&mut column_widths),
161            window,
162            cx,
163        );
164
165        // We need to run this for each column:
166        let content_width = padded_bounds.size.width.max(longest_row_size.width);
167
168        let content_size = Size {
169            width: content_width,
170            height: longest_row_size.height * self.row_count + padding.top + padding.bottom,
171        };
172
173        let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap();
174        let row_height = longest_row_size.height;
175        let shared_scroll_to_item = self.scroll_handle.as_mut().and_then(|handle| {
176            let mut handle = handle.0.borrow_mut();
177            handle.last_row_size = Some(RowSize {
178                row: padded_bounds.size,
179                contents: content_size,
180            });
181            handle.deferred_scroll_to_item.take()
182        });
183
184        let mut rendered_rows = SmallVec::default();
185
186        let hitbox = self.interactivity.prepaint(
187            global_id,
188            inspector_id,
189            bounds,
190            content_size,
191            window,
192            cx,
193            |style, mut scroll_offset, hitbox, window, cx| {
194                let border = style.border_widths.to_pixels(window.rem_size());
195                let padding = style
196                    .padding
197                    .to_pixels(bounds.size.into(), window.rem_size());
198
199                let padded_bounds = Bounds::from_corners(
200                    bounds.origin + point(border.left + padding.left, border.top),
201                    bounds.bottom_right() - point(border.right + padding.right, border.bottom),
202                );
203
204                let y_flipped = if let Some(scroll_handle) = self.scroll_handle.as_mut() {
205                    let mut scroll_state = scroll_handle.0.borrow_mut();
206                    scroll_state.base_handle.set_bounds(bounds);
207                    scroll_state.y_flipped
208                } else {
209                    false
210                };
211
212                if self.row_count > 0 {
213                    let content_height = row_height * self.row_count + padding.top + padding.bottom;
214                    let is_scrolled_vertically = !scroll_offset.y.is_zero();
215                    let min_vertical_scroll_offset = padded_bounds.size.height - content_height;
216                    if is_scrolled_vertically && scroll_offset.y < min_vertical_scroll_offset {
217                        shared_scroll_offset.borrow_mut().y = min_vertical_scroll_offset;
218                        scroll_offset.y = min_vertical_scroll_offset;
219                    }
220
221                    let content_width = content_size.width + padding.left + padding.right;
222                    let is_scrolled_horizontally =
223                        can_scroll_horizontally && !scroll_offset.x.is_zero();
224                    if is_scrolled_horizontally && content_width <= padded_bounds.size.width {
225                        shared_scroll_offset.borrow_mut().x = Pixels::ZERO;
226                        scroll_offset.x = Pixels::ZERO;
227                    }
228
229                    if let Some((mut ix, scroll_strategy)) = shared_scroll_to_item {
230                        if y_flipped {
231                            ix = self.row_count.saturating_sub(ix + 1);
232                        }
233                        let list_height = padded_bounds.size.height;
234                        let mut updated_scroll_offset = shared_scroll_offset.borrow_mut();
235                        let item_top = row_height * ix + padding.top;
236                        let item_bottom = item_top + row_height;
237                        let scroll_top = -updated_scroll_offset.y;
238                        let mut scrolled_to_top = false;
239                        if item_top < scroll_top + padding.top {
240                            scrolled_to_top = true;
241                            updated_scroll_offset.y = -(item_top) + padding.top;
242                        } else if item_bottom > scroll_top + list_height - padding.bottom {
243                            scrolled_to_top = true;
244                            updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom;
245                        }
246
247                        match scroll_strategy {
248                            ScrollStrategy::Top => {}
249                            ScrollStrategy::Center => {
250                                if scrolled_to_top {
251                                    let item_center = item_top + row_height / 2.0;
252                                    let target_scroll_top = item_center - list_height / 2.0;
253
254                                    if item_top < scroll_top
255                                        || item_bottom > scroll_top + list_height
256                                    {
257                                        updated_scroll_offset.y = -target_scroll_top
258                                            .max(Pixels::ZERO)
259                                            .min(content_height - list_height)
260                                            .max(Pixels::ZERO);
261                                    }
262                                }
263                            }
264                        }
265                        scroll_offset = *updated_scroll_offset
266                    }
267
268                    let first_visible_element_ix =
269                        (-(scroll_offset.y + padding.top) / row_height).floor() as usize;
270                    let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
271                        / row_height)
272                        .ceil() as usize;
273                    let visible_range =
274                        first_visible_element_ix..cmp::min(last_visible_element_ix, self.row_count);
275
276                    let rows = if y_flipped {
277                        let flipped_range = self.row_count.saturating_sub(visible_range.end)
278                            ..self.row_count.saturating_sub(visible_range.start);
279                        let mut items = (self.render_rows)(flipped_range, window, cx);
280                        items.reverse();
281                        items
282                    } else {
283                        (self.render_rows)(visible_range.clone(), window, cx)
284                    };
285
286                    let content_mask = ContentMask { bounds };
287                    window.with_content_mask(Some(content_mask), |window| {
288                        let available_width = if can_scroll_horizontally {
289                            padded_bounds.size.width + scroll_offset.x.abs()
290                        } else {
291                            padded_bounds.size.width
292                        };
293                        let available_space = size(
294                            AvailableSpace::Definite(available_width),
295                            AvailableSpace::Definite(row_height),
296                        );
297                        for (mut row, ix) in rows.into_iter().zip(visible_range.clone()) {
298                            let row_origin = padded_bounds.origin
299                                + point(
300                                    if can_scroll_horizontally {
301                                        scroll_offset.x + padding.left
302                                    } else {
303                                        scroll_offset.x
304                                    },
305                                    row_height * ix + scroll_offset.y + padding.top,
306                                );
307
308                            let mut item = render_row(row, column_widths, row_height).into_any();
309
310                            item.layout_as_root(available_space, window, cx);
311                            item.prepaint_at(row_origin, window, cx);
312                            rendered_rows.push(item);
313                        }
314                    });
315                }
316
317                hitbox
318            },
319        );
320        return (hitbox, rendered_rows);
321    }
322
323    fn paint(
324        &mut self,
325        global_id: Option<&GlobalElementId>,
326        inspector_id: Option<&InspectorElementId>,
327        bounds: Bounds<Pixels>,
328        _: &mut Self::RequestLayoutState,
329        (hitbox, rendered_rows): &mut Self::PrepaintState,
330        window: &mut Window,
331        cx: &mut App,
332    ) {
333        self.interactivity.paint(
334            global_id,
335            inspector_id,
336            bounds,
337            hitbox.as_ref(),
338            window,
339            cx,
340            |_, window, cx| {
341                for item in rendered_rows {
342                    item.paint(window, cx);
343                }
344            },
345        )
346    }
347}
348
349const DIVIDER_PADDING_PX: Pixels = px(2.0);
350
351fn render_row<const COLS: usize>(
352    row: [AnyElement; COLS],
353    column_widths: [Pixels; COLS],
354    row_height: Pixels,
355) -> Div {
356    use crate::ParentElement;
357    let mut div = crate::div().flex().flex_row().gap(DIVIDER_PADDING_PX);
358
359    for (ix, cell) in row.into_iter().enumerate() {
360        div = div.child(
361            crate::div()
362                .w(column_widths[ix])
363                .h(row_height)
364                .overflow_hidden()
365                .child(cell),
366        )
367    }
368
369    div
370}
371
372struct MeasureContext<const COLS: usize> {
373    row_count: usize,
374    item_to_measure_index: usize,
375    render_rows:
376        Rc<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]> + 'static>,
377    sizings: [Length; COLS],
378}
379
380impl<const COLS: usize> MeasureContext<COLS> {
381    fn new(table: &UniformTable<COLS>) -> Self {
382        Self {
383            row_count: table.row_count,
384            item_to_measure_index: table.item_to_measure_index,
385            render_rows: table.render_rows.clone(),
386            sizings: table.sizings,
387        }
388    }
389
390    fn measure_item(
391        &self,
392        table_width: AvailableSpace,
393        column_sizes: Option<&mut [Pixels; COLS]>,
394        window: &mut Window,
395        cx: &mut App,
396    ) -> Size<Pixels> {
397        if self.row_count == 0 {
398            return Size::default();
399        }
400
401        let item_ix = cmp::min(self.item_to_measure_index, self.row_count - 1);
402        let mut items = (self.render_rows)(item_ix..item_ix + 1, window, cx);
403        let Some(mut item_to_measure) = items.pop() else {
404            return Size::default();
405        };
406        let mut default_column_sizes = [Pixels::default(); COLS];
407        let column_sizes = column_sizes.unwrap_or(&mut default_column_sizes);
408
409        let mut row_height = px(0.0);
410        for i in 0..COLS {
411            let column_available_width = match self.sizings[i] {
412                Length::Definite(definite_length) => match table_width {
413                    AvailableSpace::Definite(pixels) => AvailableSpace::Definite(
414                        definite_length.to_pixels(pixels.into(), window.rem_size()),
415                    ),
416                    AvailableSpace::MinContent => AvailableSpace::MinContent,
417                    AvailableSpace::MaxContent => AvailableSpace::MaxContent,
418                },
419                Length::Auto => AvailableSpace::MaxContent,
420            };
421
422            let column_available_space = size(column_available_width, AvailableSpace::MinContent);
423
424            // todo!: Adjust row sizing to account for inter-column spacing
425            let cell_size = item_to_measure[i].layout_as_root(column_available_space, window, cx);
426            column_sizes[i] = cell_size.width;
427            row_height = row_height.max(cell_size.height);
428        }
429
430        let mut width = Pixels::ZERO;
431
432        for size in *column_sizes {
433            width += size;
434        }
435
436        Size::new(width + (COLS - 1) * DIVIDER_PADDING_PX, row_height)
437    }
438}
439
440impl<const COLS: usize> UniformTable<COLS> {}
441
442/// A handle for controlling the scroll position of a uniform list.
443/// This should be stored in your view and passed to the uniform_list on each frame.
444#[derive(Clone, Debug, Default)]
445pub struct UniformTableScrollHandle(pub Rc<RefCell<UniformTableScrollState>>);
446
447/// Where to place the element scrolled to.
448#[derive(Clone, Copy, Debug, PartialEq, Eq)]
449pub enum ScrollStrategy {
450    /// Place the element at the top of the list's viewport.
451    Top,
452    /// Attempt to place the element in the middle of the list's viewport.
453    /// May not be possible if there's not enough list items above the item scrolled to:
454    /// in this case, the element will be placed at the closest possible position.
455    Center,
456}
457
458#[derive(Copy, Clone, Debug, Default)]
459/// The size of the item and its contents.
460pub struct RowSize {
461    /// The size of the item.
462    pub row: Size<Pixels>,
463    /// The size of the item's contents, which may be larger than the item itself,
464    /// if the item was bounded by a parent element.
465    pub contents: Size<Pixels>,
466}
467
468#[derive(Clone, Debug, Default)]
469#[allow(missing_docs)]
470pub struct UniformTableScrollState {
471    pub base_handle: ScrollHandle,
472    pub deferred_scroll_to_item: Option<(usize, ScrollStrategy)>,
473    /// Size of the item, captured during last layout.
474    pub last_row_size: Option<RowSize>,
475    /// Whether the list was vertically flipped during last layout.
476    pub y_flipped: bool,
477}
478
479impl UniformTableScrollHandle {
480    /// Create a new scroll handle to bind to a uniform list.
481    pub fn new() -> Self {
482        Self(Rc::new(RefCell::new(UniformTableScrollState {
483            base_handle: ScrollHandle::new(),
484            deferred_scroll_to_item: None,
485            last_row_size: None,
486            y_flipped: false,
487        })))
488    }
489
490    /// Scroll the list to the given item index.
491    pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) {
492        self.0.borrow_mut().deferred_scroll_to_item = Some((ix, strategy));
493    }
494
495    /// Check if the list is flipped vertically.
496    pub fn y_flipped(&self) -> bool {
497        self.0.borrow().y_flipped
498    }
499
500    /// Get the index of the topmost visible child.
501    #[cfg(any(test, feature = "test-support"))]
502    pub fn logical_scroll_top_index(&self) -> usize {
503        let this = self.0.borrow();
504        this.deferred_scroll_to_item
505            .map(|(ix, _)| ix)
506            .unwrap_or_else(|| this.base_handle.logical_scroll_top().0)
507    }
508}