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}