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}