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