1use std::{ops::Range, rc::Rc};
2
3use gpui::{
4 AbsoluteLength, AppContext, Context, DefiniteLength, DragMoveEvent, Entity, EntityId,
5 FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, ListState, Point,
6 Stateful, UniformListScrollHandle, WeakEntity, list, transparent_black, uniform_list,
7};
8
9use crate::{
10 ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
11 ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator,
12 InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce,
13 ScrollAxes, ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled,
14 StyledExt as _, StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex,
15 px, single_example,
16 table_row::{IntoTableRow as _, TableRow},
17 v_flex,
18};
19use itertools::intersperse_with;
20
21pub mod table_row {
22 //! A newtype for a table row that enforces a fixed column count at runtime.
23 //!
24 //! This type ensures that all rows in a table have the same width, preventing accidental creation or mutation of rows with inconsistent lengths.
25 //! It is especially useful for CSV or tabular data where rectangular invariants must be maintained, but the number of columns is only known at runtime.
26 //! By using `TableRow`, we gain stronger guarantees and safer APIs compared to a bare `Vec<T>`, without requiring const generics.
27
28 use std::{
29 any::type_name,
30 ops::{
31 Index, IndexMut, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive,
32 },
33 };
34
35 #[derive(Clone, Debug, PartialEq, Eq)]
36 pub struct TableRow<T>(Vec<T>);
37
38 impl<T> TableRow<T> {
39 pub fn from_element(element: T, length: usize) -> Self
40 where
41 T: Clone,
42 {
43 Self::from_vec(vec![element; length], length)
44 }
45
46 /// Constructs a `TableRow` from a `Vec<T>`, panicking if the length does not match `expected_length`.
47 ///
48 /// Use this when you want to ensure at construction time that the row has the correct number of columns.
49 /// This enforces the rectangular invariant for table data, preventing accidental creation of malformed rows.
50 ///
51 /// # Panics
52 /// Panics if `data.len() != expected_length`.
53 pub fn from_vec(data: Vec<T>, expected_length: usize) -> Self {
54 Self::try_from_vec(data, expected_length).unwrap_or_else(|e| {
55 let name = type_name::<Vec<T>>();
56 panic!("Expected {name} to be created successfully: {e}");
57 })
58 }
59
60 /// Attempts to construct a `TableRow` from a `Vec<T>`, returning an error if the length does not match `expected_len`.
61 ///
62 /// This is a fallible alternative to `from_vec`, allowing you to handle inconsistent row lengths gracefully.
63 /// Returns `Ok(TableRow)` if the length matches, or an `Err` with a descriptive message otherwise.
64 pub fn try_from_vec(data: Vec<T>, expected_len: usize) -> Result<Self, String> {
65 if data.len() != expected_len {
66 Err(format!(
67 "Row length {} does not match expected {}",
68 data.len(),
69 expected_len
70 ))
71 } else {
72 Ok(Self(data))
73 }
74 }
75
76 /// Returns reference to element by column index.
77 ///
78 /// # Panics
79 /// Panics if `col` is out of bounds (i.e., `col >= self.cols()`).
80 pub fn expect_get(&self, col: impl Into<usize>) -> &T {
81 let col = col.into();
82 self.0.get(col).unwrap_or_else(|| {
83 panic!(
84 "Expected table row of `{}` to have {col:?}",
85 type_name::<T>()
86 )
87 })
88 }
89
90 pub fn get(&self, col: impl Into<usize>) -> Option<&T> {
91 self.0.get(col.into())
92 }
93
94 pub fn as_slice(&self) -> &[T] {
95 &self.0
96 }
97
98 pub fn into_vec(self) -> Vec<T> {
99 self.0
100 }
101
102 /// Like [`map`], but borrows the row and clones each element before mapping.
103 ///
104 /// This is useful when you want to map over a borrowed row without consuming it,
105 /// but your mapping function requires ownership of each element.
106 ///
107 /// # Difference
108 /// - `map_cloned` takes `&self`, clones each element, and applies `f(T) -> U`.
109 /// - [`map`] takes `self` by value and applies `f(T) -> U` directly, consuming the row.
110 /// - [`map_ref`] takes `&self` and applies `f(&T) -> U` to references of each element.
111 pub fn map_cloned<F, U>(&self, f: F) -> TableRow<U>
112 where
113 F: FnMut(T) -> U,
114 T: Clone,
115 {
116 self.clone().map(f)
117 }
118
119 /// Consumes the row and transforms all elements within it in a length-safe way.
120 ///
121 /// # Difference
122 /// - `map` takes ownership of the row (`self`) and applies `f(T) -> U` to each element.
123 /// - Use this when you want to transform and consume the row in one step.
124 /// - See also [`map_cloned`] (for mapping over a borrowed row with cloning) and [`map_ref`] (for mapping over references).
125 pub fn map<F, U>(self, f: F) -> TableRow<U>
126 where
127 F: FnMut(T) -> U,
128 {
129 TableRow(self.0.into_iter().map(f).collect())
130 }
131
132 /// Borrows the row and transforms all elements by reference in a length-safe way.
133 ///
134 /// # Difference
135 /// - `map_ref` takes `&self` and applies `f(&T) -> U` to each element by reference.
136 /// - Use this when you want to map over a borrowed row without cloning or consuming it.
137 /// - See also [`map`] (for consuming the row) and [`map_cloned`] (for mapping with cloning).
138 pub fn map_ref<F, U>(&self, f: F) -> TableRow<U>
139 where
140 F: FnMut(&T) -> U,
141 {
142 TableRow(self.0.iter().map(f).collect())
143 }
144
145 /// Number of columns (alias to `len()` with more semantic meaning)
146 pub fn cols(&self) -> usize {
147 self.0.len()
148 }
149 }
150
151 ///// Convenience traits /////
152 pub trait IntoTableRow<T> {
153 fn into_table_row(self, expected_length: usize) -> TableRow<T>;
154 }
155 impl<T> IntoTableRow<T> for Vec<T> {
156 fn into_table_row(self, expected_length: usize) -> TableRow<T> {
157 TableRow::from_vec(self, expected_length)
158 }
159 }
160
161 // Index implementations for convenient access
162 impl<T> Index<usize> for TableRow<T> {
163 type Output = T;
164
165 fn index(&self, index: usize) -> &Self::Output {
166 &self.0[index]
167 }
168 }
169
170 impl<T> IndexMut<usize> for TableRow<T> {
171 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
172 &mut self.0[index]
173 }
174 }
175
176 // Range indexing implementations for slice operations
177 impl<T> Index<Range<usize>> for TableRow<T> {
178 type Output = [T];
179
180 fn index(&self, index: Range<usize>) -> &Self::Output {
181 <Vec<T> as Index<Range<usize>>>::index(&self.0, index)
182 }
183 }
184
185 impl<T> Index<RangeFrom<usize>> for TableRow<T> {
186 type Output = [T];
187
188 fn index(&self, index: RangeFrom<usize>) -> &Self::Output {
189 <Vec<T> as Index<RangeFrom<usize>>>::index(&self.0, index)
190 }
191 }
192
193 impl<T> Index<RangeTo<usize>> for TableRow<T> {
194 type Output = [T];
195
196 fn index(&self, index: RangeTo<usize>) -> &Self::Output {
197 <Vec<T> as Index<RangeTo<usize>>>::index(&self.0, index)
198 }
199 }
200
201 impl<T> Index<RangeToInclusive<usize>> for TableRow<T> {
202 type Output = [T];
203
204 fn index(&self, index: RangeToInclusive<usize>) -> &Self::Output {
205 <Vec<T> as Index<RangeToInclusive<usize>>>::index(&self.0, index)
206 }
207 }
208
209 impl<T> Index<RangeFull> for TableRow<T> {
210 type Output = [T];
211
212 fn index(&self, index: RangeFull) -> &Self::Output {
213 <Vec<T> as Index<RangeFull>>::index(&self.0, index)
214 }
215 }
216
217 impl<T> Index<RangeInclusive<usize>> for TableRow<T> {
218 type Output = [T];
219
220 fn index(&self, index: RangeInclusive<usize>) -> &Self::Output {
221 <Vec<T> as Index<RangeInclusive<usize>>>::index(&self.0, index)
222 }
223 }
224
225 impl<T> IndexMut<RangeInclusive<usize>> for TableRow<T> {
226 fn index_mut(&mut self, index: RangeInclusive<usize>) -> &mut Self::Output {
227 <Vec<T> as IndexMut<RangeInclusive<usize>>>::index_mut(&mut self.0, index)
228 }
229 }
230}
231
232const RESIZE_COLUMN_WIDTH: f32 = 8.0;
233
234/// Represents an unchecked table row, which is a vector of elements.
235/// Will be converted into `TableRow<T>` internally
236pub type UncheckedTableRow<T> = Vec<T>;
237
238#[derive(Debug)]
239struct DraggedColumn(usize);
240
241struct UniformListData {
242 render_list_of_rows_fn:
243 Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<UncheckedTableRow<AnyElement>>>,
244 element_id: ElementId,
245 row_count: usize,
246}
247
248struct VariableRowHeightListData {
249 /// Unlike UniformList, this closure renders only single row, allowing each one to have its own height
250 render_row_fn: Box<dyn Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement>>,
251 list_state: ListState,
252 row_count: usize,
253}
254
255enum TableContents {
256 Vec(Vec<TableRow<AnyElement>>),
257 UniformList(UniformListData),
258 VariableRowHeightList(VariableRowHeightListData),
259}
260
261impl TableContents {
262 fn rows_mut(&mut self) -> Option<&mut Vec<TableRow<AnyElement>>> {
263 match self {
264 TableContents::Vec(rows) => Some(rows),
265 TableContents::UniformList(_) => None,
266 TableContents::VariableRowHeightList(_) => None,
267 }
268 }
269
270 fn len(&self) -> usize {
271 match self {
272 TableContents::Vec(rows) => rows.len(),
273 TableContents::UniformList(data) => data.row_count,
274 TableContents::VariableRowHeightList(data) => data.row_count,
275 }
276 }
277
278 fn is_empty(&self) -> bool {
279 self.len() == 0
280 }
281}
282
283pub struct TableInteractionState {
284 pub focus_handle: FocusHandle,
285 pub scroll_handle: UniformListScrollHandle,
286 pub custom_scrollbar: Option<Scrollbars>,
287}
288
289impl TableInteractionState {
290 pub fn new(cx: &mut App) -> Self {
291 Self {
292 focus_handle: cx.focus_handle(),
293 scroll_handle: UniformListScrollHandle::new(),
294 custom_scrollbar: None,
295 }
296 }
297
298 pub fn with_custom_scrollbar(mut self, custom_scrollbar: Scrollbars) -> Self {
299 self.custom_scrollbar = Some(custom_scrollbar);
300 self
301 }
302
303 pub fn scroll_offset(&self) -> Point<Pixels> {
304 self.scroll_handle.offset()
305 }
306
307 pub fn set_scroll_offset(&self, offset: Point<Pixels>) {
308 self.scroll_handle.set_offset(offset);
309 }
310
311 pub fn listener<E: ?Sized>(
312 this: &Entity<Self>,
313 f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static,
314 ) -> impl Fn(&E, &mut Window, &mut App) + 'static {
315 let view = this.downgrade();
316 move |e: &E, window: &mut Window, cx: &mut App| {
317 view.update(cx, |view, cx| f(view, e, window, cx)).ok();
318 }
319 }
320
321 /// Renders invisible resize handles overlaid on top of table content.
322 ///
323 /// - Spacer: invisible element that matches the width of table column content
324 /// - Divider: contains the actual resize handle that users can drag to resize columns
325 ///
326 /// Structure: [spacer] [divider] [spacer] [divider] [spacer]
327 ///
328 /// Business logic:
329 /// 1. Creates spacers matching each column width
330 /// 2. Intersperses (inserts) resize handles between spacers (interactive only for resizable columns)
331 /// 3. Each handle supports hover highlighting, double-click to reset, and drag to resize
332 /// 4. Returns an absolute-positioned overlay that sits on top of table content
333 fn render_resize_handles(
334 &self,
335 column_widths: &TableRow<Length>,
336 resizable_columns: &TableRow<TableResizeBehavior>,
337 initial_sizes: &TableRow<DefiniteLength>,
338 columns: Option<Entity<TableColumnWidths>>,
339 window: &mut Window,
340 cx: &mut App,
341 ) -> AnyElement {
342 let spacers = column_widths
343 .as_slice()
344 .iter()
345 .map(|width| base_cell_style(Some(*width)).into_any_element());
346
347 let mut column_ix = 0;
348 let resizable_columns_shared = Rc::new(resizable_columns.clone());
349 let initial_sizes_shared = Rc::new(initial_sizes.clone());
350 let mut resizable_columns_iter = resizable_columns.as_slice().iter();
351
352 // Insert dividers between spacers (column content)
353 let dividers = intersperse_with(spacers, || {
354 let resizable_columns = Rc::clone(&resizable_columns_shared);
355 let initial_sizes = Rc::clone(&initial_sizes_shared);
356 window.with_id(column_ix, |window| {
357 let mut resize_divider = div()
358 // This is required because this is evaluated at a different time than the use_state call above
359 .id(column_ix)
360 .relative()
361 .top_0()
362 .w_px()
363 .h_full()
364 .bg(cx.theme().colors().border.opacity(0.8));
365
366 let mut resize_handle = div()
367 .id("column-resize-handle")
368 .absolute()
369 .left_neg_0p5()
370 .w(px(RESIZE_COLUMN_WIDTH))
371 .h_full();
372
373 if resizable_columns_iter
374 .next()
375 .is_some_and(TableResizeBehavior::is_resizable)
376 {
377 let hovered = window.use_state(cx, |_window, _cx| false);
378
379 resize_divider = resize_divider.when(*hovered.read(cx), |div| {
380 div.bg(cx.theme().colors().border_focused)
381 });
382
383 resize_handle = resize_handle
384 .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered))
385 .cursor_col_resize()
386 .when_some(columns.clone(), |this, columns| {
387 this.on_click(move |event, window, cx| {
388 if event.click_count() >= 2 {
389 columns.update(cx, |columns, _| {
390 columns.on_double_click(
391 column_ix,
392 &initial_sizes,
393 &resizable_columns,
394 window,
395 );
396 })
397 }
398
399 cx.stop_propagation();
400 })
401 })
402 .on_drag(DraggedColumn(column_ix), |_, _offset, _window, cx| {
403 cx.new(|_cx| gpui::Empty)
404 })
405 }
406
407 column_ix += 1;
408 resize_divider.child(resize_handle).into_any_element()
409 })
410 });
411
412 h_flex()
413 .id("resize-handles")
414 .absolute()
415 .inset_0()
416 .w_full()
417 .children(dividers)
418 .into_any_element()
419 }
420}
421
422#[derive(Debug, Copy, Clone, PartialEq)]
423pub enum TableResizeBehavior {
424 None,
425 Resizable,
426 MinSize(f32),
427}
428
429impl TableResizeBehavior {
430 pub fn is_resizable(&self) -> bool {
431 *self != TableResizeBehavior::None
432 }
433
434 pub fn min_size(&self) -> Option<f32> {
435 match self {
436 TableResizeBehavior::None => None,
437 TableResizeBehavior::Resizable => Some(0.05),
438 TableResizeBehavior::MinSize(min_size) => Some(*min_size),
439 }
440 }
441}
442
443pub struct TableColumnWidths {
444 widths: TableRow<DefiniteLength>,
445 visible_widths: TableRow<DefiniteLength>,
446 cached_bounds_width: Pixels,
447 initialized: bool,
448}
449
450impl TableColumnWidths {
451 pub fn new(cols: usize, _: &mut App) -> Self {
452 Self {
453 widths: vec![DefiniteLength::default(); cols].into_table_row(cols),
454 visible_widths: vec![DefiniteLength::default(); cols].into_table_row(cols),
455 cached_bounds_width: Default::default(),
456 initialized: false,
457 }
458 }
459
460 pub fn cols(&self) -> usize {
461 self.widths.cols()
462 }
463
464 fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 {
465 match length {
466 DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => *pixels / bounds_width,
467 DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => {
468 rems_width.to_pixels(rem_size) / bounds_width
469 }
470 DefiniteLength::Fraction(fraction) => *fraction,
471 }
472 }
473
474 fn on_double_click(
475 &mut self,
476 double_click_position: usize,
477 initial_sizes: &TableRow<DefiniteLength>,
478 resize_behavior: &TableRow<TableResizeBehavior>,
479 window: &mut Window,
480 ) {
481 let bounds_width = self.cached_bounds_width;
482 let rem_size = window.rem_size();
483 let initial_sizes =
484 initial_sizes.map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
485 let widths = self
486 .widths
487 .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
488
489 let updated_widths = Self::reset_to_initial_size(
490 double_click_position,
491 widths,
492 initial_sizes,
493 resize_behavior,
494 );
495 self.widths = updated_widths.map(DefiniteLength::Fraction);
496 self.visible_widths = self.widths.clone(); // previously was copy
497 }
498
499 fn reset_to_initial_size(
500 col_idx: usize,
501 mut widths: TableRow<f32>,
502 initial_sizes: TableRow<f32>,
503 resize_behavior: &TableRow<TableResizeBehavior>,
504 ) -> TableRow<f32> {
505 // RESET:
506 // Part 1:
507 // Figure out if we should shrink/grow the selected column
508 // Get diff which represents the change in column we want to make initial size delta curr_size = diff
509 //
510 // Part 2: We need to decide which side column we should move and where
511 //
512 // If we want to grow our column we should check the left/right columns diff to see what side
513 // has a greater delta than their initial size. Likewise, if we shrink our column we should check
514 // the left/right column diffs to see what side has the smallest delta.
515 //
516 // Part 3: resize
517 //
518 // col_idx represents the column handle to the right of an active column
519 //
520 // If growing and right has the greater delta {
521 // shift col_idx to the right
522 // } else if growing and left has the greater delta {
523 // shift col_idx - 1 to the left
524 // } else if shrinking and the right has the greater delta {
525 // shift
526 // } {
527 //
528 // }
529 // }
530 //
531 // if we need to shrink, then if the right
532 //
533
534 // DRAGGING
535 // we get diff which represents the change in the _drag handle_ position
536 // -diff => dragging left ->
537 // grow the column to the right of the handle as much as we can shrink columns to the left of the handle
538 // +diff => dragging right -> growing handles column
539 // grow the column to the left of the handle as much as we can shrink columns to the right of the handle
540 //
541
542 let diff = initial_sizes[col_idx] - widths[col_idx];
543
544 let left_diff =
545 initial_sizes[..col_idx].iter().sum::<f32>() - widths[..col_idx].iter().sum::<f32>();
546 let right_diff = initial_sizes[col_idx + 1..].iter().sum::<f32>()
547 - widths[col_idx + 1..].iter().sum::<f32>();
548
549 let go_left_first = if diff < 0.0 {
550 left_diff > right_diff
551 } else {
552 left_diff < right_diff
553 };
554
555 if !go_left_first {
556 let diff_remaining =
557 Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, 1);
558
559 if diff_remaining != 0.0 && col_idx > 0 {
560 Self::propagate_resize_diff(
561 diff_remaining,
562 col_idx,
563 &mut widths,
564 resize_behavior,
565 -1,
566 );
567 }
568 } else {
569 let diff_remaining =
570 Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, -1);
571
572 if diff_remaining != 0.0 {
573 Self::propagate_resize_diff(
574 diff_remaining,
575 col_idx,
576 &mut widths,
577 resize_behavior,
578 1,
579 );
580 }
581 }
582
583 widths
584 }
585
586 fn on_drag_move(
587 &mut self,
588 drag_event: &DragMoveEvent<DraggedColumn>,
589 resize_behavior: &TableRow<TableResizeBehavior>,
590 window: &mut Window,
591 cx: &mut Context<Self>,
592 ) {
593 let drag_position = drag_event.event.position;
594 let bounds = drag_event.bounds;
595
596 let mut col_position = 0.0;
597 let rem_size = window.rem_size();
598 let bounds_width = bounds.right() - bounds.left();
599 let col_idx = drag_event.drag(cx).0;
600
601 let column_handle_width = Self::get_fraction(
602 &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_COLUMN_WIDTH))),
603 bounds_width,
604 rem_size,
605 );
606
607 let mut widths = self
608 .widths
609 .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
610
611 for length in widths[0..=col_idx].iter() {
612 col_position += length + column_handle_width;
613 }
614
615 let mut total_length_ratio = col_position;
616 for length in widths[col_idx + 1..].iter() {
617 total_length_ratio += length;
618 }
619 let cols = resize_behavior.cols();
620 total_length_ratio += (cols - 1 - col_idx) as f32 * column_handle_width;
621
622 let drag_fraction = (drag_position.x - bounds.left()) / bounds_width;
623 let drag_fraction = drag_fraction * total_length_ratio;
624 let diff = drag_fraction - col_position - column_handle_width / 2.0;
625
626 Self::drag_column_handle(diff, col_idx, &mut widths, resize_behavior);
627
628 self.visible_widths = widths.map(DefiniteLength::Fraction);
629 }
630
631 fn drag_column_handle(
632 diff: f32,
633 col_idx: usize,
634 widths: &mut TableRow<f32>,
635 resize_behavior: &TableRow<TableResizeBehavior>,
636 ) {
637 // if diff > 0.0 then go right
638 if diff > 0.0 {
639 Self::propagate_resize_diff(diff, col_idx, widths, resize_behavior, 1);
640 } else {
641 Self::propagate_resize_diff(-diff, col_idx + 1, widths, resize_behavior, -1);
642 }
643 }
644
645 fn propagate_resize_diff(
646 diff: f32,
647 col_idx: usize,
648 widths: &mut TableRow<f32>,
649 resize_behavior: &TableRow<TableResizeBehavior>,
650 direction: i8,
651 ) -> f32 {
652 let mut diff_remaining = diff;
653 if resize_behavior[col_idx].min_size().is_none() {
654 return diff;
655 }
656
657 let step_right;
658 let step_left;
659 if direction < 0 {
660 step_right = 0;
661 step_left = 1;
662 } else {
663 step_right = 1;
664 step_left = 0;
665 }
666 if col_idx == 0 && direction < 0 {
667 return diff;
668 }
669 let mut curr_column = col_idx + step_right - step_left;
670
671 while diff_remaining != 0.0 && curr_column < widths.cols() {
672 let Some(min_size) = resize_behavior[curr_column].min_size() else {
673 if curr_column == 0 {
674 break;
675 }
676 curr_column -= step_left;
677 curr_column += step_right;
678 continue;
679 };
680
681 let curr_width = widths[curr_column] - diff_remaining;
682 widths[curr_column] = curr_width;
683
684 if min_size > curr_width {
685 diff_remaining = min_size - curr_width;
686 widths[curr_column] = min_size;
687 } else {
688 diff_remaining = 0.0;
689 break;
690 }
691 if curr_column == 0 {
692 break;
693 }
694 curr_column -= step_left;
695 curr_column += step_right;
696 }
697 widths[col_idx] = widths[col_idx] + (diff - diff_remaining);
698
699 diff_remaining
700 }
701}
702
703pub struct TableWidths {
704 initial: TableRow<DefiniteLength>,
705 current: Option<Entity<TableColumnWidths>>,
706 resizable: TableRow<TableResizeBehavior>,
707}
708
709impl TableWidths {
710 pub fn new(widths: TableRow<impl Into<DefiniteLength>>) -> Self {
711 let widths = widths.map(Into::into);
712
713 let expected_length = widths.cols();
714 TableWidths {
715 initial: widths,
716 current: None,
717 resizable: vec![TableResizeBehavior::None; expected_length]
718 .into_table_row(expected_length),
719 }
720 }
721
722 fn lengths(&self, cx: &App) -> TableRow<Length> {
723 self.current
724 .as_ref()
725 .map(|entity| entity.read(cx).visible_widths.map_cloned(Length::Definite))
726 .unwrap_or_else(|| self.initial.map_cloned(Length::Definite))
727 }
728}
729
730/// A table component
731#[derive(RegisterComponent, IntoElement)]
732pub struct Table {
733 striped: bool,
734 show_row_borders: bool,
735 show_row_hover: bool,
736 width: Option<Length>,
737 headers: Option<TableRow<AnyElement>>,
738 rows: TableContents,
739 interaction_state: Option<WeakEntity<TableInteractionState>>,
740 col_widths: Option<TableWidths>,
741 map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
742 use_ui_font: bool,
743 empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
744 /// The number of columns in the table. Used to assert column numbers in `TableRow` collections
745 cols: usize,
746 disable_base_cell_style: bool,
747}
748
749impl Table {
750 /// Creates a new table with the specified number of columns.
751 pub fn new(cols: usize) -> Self {
752 Self {
753 cols,
754 striped: false,
755 show_row_borders: true,
756 show_row_hover: true,
757 width: None,
758 headers: None,
759 rows: TableContents::Vec(Vec::new()),
760 interaction_state: None,
761 map_row: None,
762 use_ui_font: true,
763 empty_table_callback: None,
764 col_widths: None,
765 disable_base_cell_style: false,
766 }
767 }
768
769 /// Disables based styling of row cell (paddings, text ellipsis, nowrap, etc), keeping width settings
770 ///
771 /// Doesn't affect base style of header cell.
772 /// Doesn't remove overflow-hidden
773 pub fn disable_base_style(mut self) -> Self {
774 self.disable_base_cell_style = true;
775 self
776 }
777
778 /// Enables uniform list rendering.
779 /// The provided function will be passed directly to the `uniform_list` element.
780 /// Therefore, if this method is called, any calls to [`Table::row`] before or after
781 /// this method is called will be ignored.
782 pub fn uniform_list(
783 mut self,
784 id: impl Into<ElementId>,
785 row_count: usize,
786 render_item_fn: impl Fn(
787 Range<usize>,
788 &mut Window,
789 &mut App,
790 ) -> Vec<UncheckedTableRow<AnyElement>>
791 + 'static,
792 ) -> Self {
793 self.rows = TableContents::UniformList(UniformListData {
794 element_id: id.into(),
795 row_count,
796 render_list_of_rows_fn: Box::new(render_item_fn),
797 });
798 self
799 }
800
801 /// Enables rendering of tables with variable row heights, allowing each row to have its own height.
802 ///
803 /// This mode is useful for displaying content such as CSV data or multiline cells, where rows may not have uniform heights.
804 /// It is generally slower than [`Table::uniform_list`] due to the need to measure each row individually, but it provides correct layout for non-uniform or multiline content.
805 ///
806 /// # Parameters
807 /// - `row_count`: The total number of rows in the table.
808 /// - `list_state`: The [`ListState`] used for managing scroll position and virtualization. This must be initialized and managed by the caller, and should be kept in sync with the number of rows.
809 /// - `render_row_fn`: A closure that renders a single row, given the row index, a mutable reference to [`Window`], and a mutable reference to [`App`]. It should return an array of [`AnyElement`]s, one for each column.
810 pub fn variable_row_height_list(
811 mut self,
812 row_count: usize,
813 list_state: ListState,
814 render_row_fn: impl Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement> + 'static,
815 ) -> Self {
816 self.rows = TableContents::VariableRowHeightList(VariableRowHeightListData {
817 render_row_fn: Box::new(render_row_fn),
818 list_state,
819 row_count,
820 });
821 self
822 }
823
824 /// Enables row striping (alternating row colors)
825 pub fn striped(mut self) -> Self {
826 self.striped = true;
827 self
828 }
829
830 /// Hides the border lines between rows
831 pub fn hide_row_borders(mut self) -> Self {
832 self.show_row_borders = false;
833 self
834 }
835
836 /// Sets the width of the table.
837 /// Will enable horizontal scrolling if [`Self::interactable`] is also called.
838 pub fn width(mut self, width: impl Into<Length>) -> Self {
839 self.width = Some(width.into());
840 self
841 }
842
843 /// Enables interaction (primarily scrolling) with the table.
844 ///
845 /// Vertical scrolling will be enabled by default if the table is taller than its container.
846 ///
847 /// Horizontal scrolling will only be enabled if [`Self::width`] is also called, otherwise
848 /// the list will always shrink the table columns to fit their contents I.e. If [`Self::uniform_list`]
849 /// is used without a width and with [`Self::interactable`], the [`ListHorizontalSizingBehavior`] will
850 /// be set to [`ListHorizontalSizingBehavior::FitList`].
851 pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
852 self.interaction_state = Some(interaction_state.downgrade());
853 self
854 }
855
856 pub fn header(mut self, headers: UncheckedTableRow<impl IntoElement>) -> Self {
857 self.headers = Some(
858 headers
859 .into_table_row(self.cols)
860 .map(IntoElement::into_any_element),
861 );
862 self
863 }
864
865 pub fn row(mut self, items: UncheckedTableRow<impl IntoElement>) -> Self {
866 if let Some(rows) = self.rows.rows_mut() {
867 rows.push(
868 items
869 .into_table_row(self.cols)
870 .map(IntoElement::into_any_element),
871 );
872 }
873 self
874 }
875
876 pub fn column_widths(mut self, widths: UncheckedTableRow<impl Into<DefiniteLength>>) -> Self {
877 if self.col_widths.is_none() {
878 self.col_widths = Some(TableWidths::new(widths.into_table_row(self.cols)));
879 }
880 self
881 }
882
883 pub fn resizable_columns(
884 mut self,
885 resizable: UncheckedTableRow<TableResizeBehavior>,
886 column_widths: &Entity<TableColumnWidths>,
887 cx: &mut App,
888 ) -> Self {
889 if let Some(table_widths) = self.col_widths.as_mut() {
890 table_widths.resizable = resizable.into_table_row(self.cols);
891 let column_widths = table_widths
892 .current
893 .get_or_insert_with(|| column_widths.clone());
894
895 column_widths.update(cx, |widths, _| {
896 if !widths.initialized {
897 widths.initialized = true;
898 widths.widths = table_widths.initial.clone();
899 widths.visible_widths = widths.widths.clone();
900 }
901 })
902 }
903 self
904 }
905
906 pub fn no_ui_font(mut self) -> Self {
907 self.use_ui_font = false;
908 self
909 }
910
911 pub fn map_row(
912 mut self,
913 callback: impl Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement + 'static,
914 ) -> Self {
915 self.map_row = Some(Rc::new(callback));
916 self
917 }
918
919 /// Hides the default hover background on table rows.
920 /// Use this when you want to handle row hover styling manually via `map_row`.
921 pub fn hide_row_hover(mut self) -> Self {
922 self.show_row_hover = false;
923 self
924 }
925
926 /// Provide a callback that is invoked when the table is rendered without any rows
927 pub fn empty_table_callback(
928 mut self,
929 callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
930 ) -> Self {
931 self.empty_table_callback = Some(Rc::new(callback));
932 self
933 }
934}
935
936fn base_cell_style(width: Option<Length>) -> Div {
937 div()
938 .px_1p5()
939 .when_some(width, |this, width| this.w(width))
940 .when(width.is_none(), |this| this.flex_1())
941 .whitespace_nowrap()
942 .text_ellipsis()
943 .overflow_hidden()
944}
945
946fn base_cell_style_text(width: Option<Length>, use_ui_font: bool, cx: &App) -> Div {
947 base_cell_style(width).when(use_ui_font, |el| el.text_ui(cx))
948}
949
950pub fn render_table_row(
951 row_index: usize,
952 items: TableRow<impl IntoElement>,
953 table_context: TableRenderContext,
954 window: &mut Window,
955 cx: &mut App,
956) -> AnyElement {
957 let is_striped = table_context.striped;
958 let is_last = row_index == table_context.total_row_count - 1;
959 let bg = if row_index % 2 == 1 && is_striped {
960 Some(cx.theme().colors().text.opacity(0.05))
961 } else {
962 None
963 };
964 let cols = items.cols();
965 let column_widths = table_context
966 .column_widths
967 .map_or(vec![None; cols].into_table_row(cols), |widths| {
968 widths.map(Some)
969 });
970
971 let mut row = div()
972 // NOTE: `h_flex()` sneakily applies `items_center()` which is not default behavior for div element.
973 // Applying `.flex().flex_row()` manually to overcome that
974 .flex()
975 .flex_row()
976 .id(("table_row", row_index))
977 .size_full()
978 .when_some(bg, |row, bg| row.bg(bg))
979 .when(table_context.show_row_hover, |row| {
980 row.hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.6)))
981 })
982 .when(!is_striped && table_context.show_row_borders, |row| {
983 row.border_b_1()
984 .border_color(transparent_black())
985 .when(!is_last, |row| row.border_color(cx.theme().colors().border))
986 });
987
988 row = row.children(
989 items
990 .map(IntoElement::into_any_element)
991 .into_vec()
992 .into_iter()
993 .zip(column_widths.into_vec())
994 .map(|(cell, width)| {
995 if table_context.disable_base_cell_style {
996 div()
997 .when_some(width, |this, width| this.w(width))
998 .when(width.is_none(), |this| this.flex_1())
999 .overflow_hidden()
1000 .child(cell)
1001 } else {
1002 base_cell_style_text(width, table_context.use_ui_font, cx)
1003 .px_1()
1004 .py_0p5()
1005 .child(cell)
1006 }
1007 }),
1008 );
1009
1010 let row = if let Some(map_row) = table_context.map_row {
1011 map_row((row_index, row), window, cx)
1012 } else {
1013 row.into_any_element()
1014 };
1015
1016 div().size_full().child(row).into_any_element()
1017}
1018
1019pub fn render_table_header(
1020 headers: TableRow<impl IntoElement>,
1021 table_context: TableRenderContext,
1022 columns_widths: Option<(
1023 WeakEntity<TableColumnWidths>,
1024 TableRow<TableResizeBehavior>,
1025 TableRow<DefiniteLength>,
1026 )>,
1027 entity_id: Option<EntityId>,
1028 cx: &mut App,
1029) -> impl IntoElement {
1030 let cols = headers.cols();
1031 let column_widths = table_context
1032 .column_widths
1033 .map_or(vec![None; cols].into_table_row(cols), |widths| {
1034 widths.map(Some)
1035 });
1036
1037 let element_id = entity_id
1038 .map(|entity| entity.to_string())
1039 .unwrap_or_default();
1040
1041 let shared_element_id: SharedString = format!("table-{}", element_id).into();
1042
1043 div()
1044 .flex()
1045 .flex_row()
1046 .items_center()
1047 .justify_between()
1048 .w_full()
1049 .p_2()
1050 .border_b_1()
1051 .border_color(cx.theme().colors().border)
1052 .children(
1053 headers
1054 .into_vec()
1055 .into_iter()
1056 .enumerate()
1057 .zip(column_widths.into_vec())
1058 .map(|((header_idx, h), width)| {
1059 base_cell_style_text(width, table_context.use_ui_font, cx)
1060 .child(h)
1061 .id(ElementId::NamedInteger(
1062 shared_element_id.clone(),
1063 header_idx as u64,
1064 ))
1065 .when_some(
1066 columns_widths.as_ref().cloned(),
1067 |this, (column_widths, resizables, initial_sizes)| {
1068 if resizables[header_idx].is_resizable() {
1069 this.on_click(move |event, window, cx| {
1070 if event.click_count() > 1 {
1071 column_widths
1072 .update(cx, |column, _| {
1073 column.on_double_click(
1074 header_idx,
1075 &initial_sizes,
1076 &resizables,
1077 window,
1078 );
1079 })
1080 .ok();
1081 }
1082 })
1083 } else {
1084 this
1085 }
1086 },
1087 )
1088 }),
1089 )
1090}
1091
1092#[derive(Clone)]
1093pub struct TableRenderContext {
1094 pub striped: bool,
1095 pub show_row_borders: bool,
1096 pub show_row_hover: bool,
1097 pub total_row_count: usize,
1098 pub column_widths: Option<TableRow<Length>>,
1099 pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
1100 pub use_ui_font: bool,
1101 pub disable_base_cell_style: bool,
1102}
1103
1104impl TableRenderContext {
1105 fn new(table: &Table, cx: &App) -> Self {
1106 Self {
1107 striped: table.striped,
1108 show_row_borders: table.show_row_borders,
1109 show_row_hover: table.show_row_hover,
1110 total_row_count: table.rows.len(),
1111 column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)),
1112 map_row: table.map_row.clone(),
1113 use_ui_font: table.use_ui_font,
1114 disable_base_cell_style: table.disable_base_cell_style,
1115 }
1116 }
1117}
1118
1119impl RenderOnce for Table {
1120 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1121 let table_context = TableRenderContext::new(&self, cx);
1122 let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
1123 let current_widths = self
1124 .col_widths
1125 .as_ref()
1126 .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable.clone())))
1127 .map(|(curr, resize_behavior)| (curr.downgrade(), resize_behavior));
1128
1129 let current_widths_with_initial_sizes = self
1130 .col_widths
1131 .as_ref()
1132 .and_then(|widths| {
1133 Some((
1134 widths.current.as_ref()?,
1135 widths.resizable.clone(),
1136 widths.initial.clone(),
1137 ))
1138 })
1139 .map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial));
1140
1141 let width = self.width;
1142 let no_rows_rendered = self.rows.is_empty();
1143
1144 let table = div()
1145 .when_some(width, |this, width| this.w(width))
1146 .h_full()
1147 .v_flex()
1148 .when_some(self.headers.take(), |this, headers| {
1149 this.child(render_table_header(
1150 headers,
1151 table_context.clone(),
1152 current_widths_with_initial_sizes,
1153 interaction_state.as_ref().map(Entity::entity_id),
1154 cx,
1155 ))
1156 })
1157 .when_some(current_widths, {
1158 |this, (widths, resize_behavior)| {
1159 this.on_drag_move::<DraggedColumn>({
1160 let widths = widths.clone();
1161 move |e, window, cx| {
1162 widths
1163 .update(cx, |widths, cx| {
1164 widths.on_drag_move(e, &resize_behavior, window, cx);
1165 })
1166 .ok();
1167 }
1168 })
1169 .on_children_prepainted({
1170 let widths = widths.clone();
1171 move |bounds, _, cx| {
1172 widths
1173 .update(cx, |widths, _| {
1174 // This works because all children x axis bounds are the same
1175 widths.cached_bounds_width =
1176 bounds[0].right() - bounds[0].left();
1177 })
1178 .ok();
1179 }
1180 })
1181 .on_drop::<DraggedColumn>(move |_, _, cx| {
1182 widths
1183 .update(cx, |widths, _| {
1184 widths.widths = widths.visible_widths.clone();
1185 })
1186 .ok();
1187 // Finish the resize operation
1188 })
1189 }
1190 })
1191 .child({
1192 let content = div()
1193 .flex_grow()
1194 .w_full()
1195 .relative()
1196 .overflow_hidden()
1197 .map(|parent| match self.rows {
1198 TableContents::Vec(items) => {
1199 parent.children(items.into_iter().enumerate().map(|(index, row)| {
1200 div().child(render_table_row(
1201 index,
1202 row,
1203 table_context.clone(),
1204 window,
1205 cx,
1206 ))
1207 }))
1208 }
1209 TableContents::UniformList(uniform_list_data) => parent.child(
1210 uniform_list(
1211 uniform_list_data.element_id,
1212 uniform_list_data.row_count,
1213 {
1214 let render_item_fn = uniform_list_data.render_list_of_rows_fn;
1215 move |range: Range<usize>, window, cx| {
1216 let elements = render_item_fn(range.clone(), window, cx)
1217 .into_iter()
1218 .map(|raw_row| raw_row.into_table_row(self.cols))
1219 .collect::<Vec<_>>();
1220 elements
1221 .into_iter()
1222 .zip(range)
1223 .map(|(row, row_index)| {
1224 render_table_row(
1225 row_index,
1226 row,
1227 table_context.clone(),
1228 window,
1229 cx,
1230 )
1231 })
1232 .collect()
1233 }
1234 },
1235 )
1236 .size_full()
1237 .flex_grow()
1238 .with_sizing_behavior(ListSizingBehavior::Auto)
1239 .with_horizontal_sizing_behavior(if width.is_some() {
1240 ListHorizontalSizingBehavior::Unconstrained
1241 } else {
1242 ListHorizontalSizingBehavior::FitList
1243 })
1244 .when_some(
1245 interaction_state.as_ref(),
1246 |this, state| {
1247 this.track_scroll(
1248 &state.read_with(cx, |s, _| s.scroll_handle.clone()),
1249 )
1250 },
1251 ),
1252 ),
1253 TableContents::VariableRowHeightList(variable_list_data) => parent.child(
1254 list(variable_list_data.list_state.clone(), {
1255 let render_item_fn = variable_list_data.render_row_fn;
1256 move |row_index: usize, window: &mut Window, cx: &mut App| {
1257 let row = render_item_fn(row_index, window, cx)
1258 .into_table_row(self.cols);
1259 render_table_row(
1260 row_index,
1261 row,
1262 table_context.clone(),
1263 window,
1264 cx,
1265 )
1266 }
1267 })
1268 .size_full()
1269 .flex_grow()
1270 .with_sizing_behavior(ListSizingBehavior::Auto),
1271 ),
1272 })
1273 .when_some(
1274 self.col_widths.as_ref().zip(interaction_state.as_ref()),
1275 |parent, (table_widths, state)| {
1276 parent.child(state.update(cx, |state, cx| {
1277 let resizable_columns = &table_widths.resizable;
1278 let column_widths = table_widths.lengths(cx);
1279 let columns = table_widths.current.clone();
1280 let initial_sizes = &table_widths.initial;
1281 state.render_resize_handles(
1282 &column_widths,
1283 resizable_columns,
1284 initial_sizes,
1285 columns,
1286 window,
1287 cx,
1288 )
1289 }))
1290 },
1291 );
1292
1293 if let Some(state) = interaction_state.as_ref() {
1294 let scrollbars = state
1295 .read(cx)
1296 .custom_scrollbar
1297 .clone()
1298 .unwrap_or_else(|| Scrollbars::new(ScrollAxes::Both));
1299 content
1300 .custom_scrollbars(
1301 scrollbars.tracked_scroll_handle(&state.read(cx).scroll_handle),
1302 window,
1303 cx,
1304 )
1305 .into_any_element()
1306 } else {
1307 content.into_any_element()
1308 }
1309 })
1310 .when_some(
1311 no_rows_rendered
1312 .then_some(self.empty_table_callback)
1313 .flatten(),
1314 |this, callback| {
1315 this.child(
1316 h_flex()
1317 .size_full()
1318 .p_3()
1319 .items_start()
1320 .justify_center()
1321 .child(callback(window, cx)),
1322 )
1323 },
1324 );
1325
1326 if let Some(interaction_state) = interaction_state.as_ref() {
1327 table
1328 .track_focus(&interaction_state.read(cx).focus_handle)
1329 .id(("table", interaction_state.entity_id()))
1330 .into_any_element()
1331 } else {
1332 table.into_any_element()
1333 }
1334 }
1335}
1336
1337impl Component for Table {
1338 fn scope() -> ComponentScope {
1339 ComponentScope::Layout
1340 }
1341
1342 fn description() -> Option<&'static str> {
1343 Some("A table component for displaying data in rows and columns with optional styling.")
1344 }
1345
1346 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
1347 Some(
1348 v_flex()
1349 .gap_6()
1350 .children(vec![
1351 example_group_with_title(
1352 "Basic Tables",
1353 vec![
1354 single_example(
1355 "Simple Table",
1356 Table::new(3)
1357 .width(px(400.))
1358 .header(vec!["Name", "Age", "City"])
1359 .row(vec!["Alice", "28", "New York"])
1360 .row(vec!["Bob", "32", "San Francisco"])
1361 .row(vec!["Charlie", "25", "London"])
1362 .into_any_element(),
1363 ),
1364 single_example(
1365 "Two Column Table",
1366 Table::new(2)
1367 .header(vec!["Category", "Value"])
1368 .width(px(300.))
1369 .row(vec!["Revenue", "$100,000"])
1370 .row(vec!["Expenses", "$75,000"])
1371 .row(vec!["Profit", "$25,000"])
1372 .into_any_element(),
1373 ),
1374 ],
1375 ),
1376 example_group_with_title(
1377 "Styled Tables",
1378 vec![
1379 single_example(
1380 "Default",
1381 Table::new(3)
1382 .width(px(400.))
1383 .header(vec!["Product", "Price", "Stock"])
1384 .row(vec!["Laptop", "$999", "In Stock"])
1385 .row(vec!["Phone", "$599", "Low Stock"])
1386 .row(vec!["Tablet", "$399", "Out of Stock"])
1387 .into_any_element(),
1388 ),
1389 single_example(
1390 "Striped",
1391 Table::new(3)
1392 .width(px(400.))
1393 .striped()
1394 .header(vec!["Product", "Price", "Stock"])
1395 .row(vec!["Laptop", "$999", "In Stock"])
1396 .row(vec!["Phone", "$599", "Low Stock"])
1397 .row(vec!["Tablet", "$399", "Out of Stock"])
1398 .row(vec!["Headphones", "$199", "In Stock"])
1399 .into_any_element(),
1400 ),
1401 ],
1402 ),
1403 example_group_with_title(
1404 "Mixed Content Table",
1405 vec![single_example(
1406 "Table with Elements",
1407 Table::new(5)
1408 .width(px(840.))
1409 .header(vec!["Status", "Name", "Priority", "Deadline", "Action"])
1410 .row(vec![
1411 Indicator::dot().color(Color::Success).into_any_element(),
1412 "Project A".into_any_element(),
1413 "High".into_any_element(),
1414 "2023-12-31".into_any_element(),
1415 Button::new("view_a", "View")
1416 .style(ButtonStyle::Filled)
1417 .full_width()
1418 .into_any_element(),
1419 ])
1420 .row(vec![
1421 Indicator::dot().color(Color::Warning).into_any_element(),
1422 "Project B".into_any_element(),
1423 "Medium".into_any_element(),
1424 "2024-03-15".into_any_element(),
1425 Button::new("view_b", "View")
1426 .style(ButtonStyle::Filled)
1427 .full_width()
1428 .into_any_element(),
1429 ])
1430 .row(vec![
1431 Indicator::dot().color(Color::Error).into_any_element(),
1432 "Project C".into_any_element(),
1433 "Low".into_any_element(),
1434 "2024-06-30".into_any_element(),
1435 Button::new("view_c", "View")
1436 .style(ButtonStyle::Filled)
1437 .full_width()
1438 .into_any_element(),
1439 ])
1440 .into_any_element(),
1441 )],
1442 ),
1443 ])
1444 .into_any_element(),
1445 )
1446 }
1447}
1448
1449#[cfg(test)]
1450mod test {
1451 use super::*;
1452
1453 fn is_almost_eq(a: &[f32], b: &[f32]) -> bool {
1454 a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6)
1455 }
1456
1457 fn cols_to_str(cols: &[f32], total_size: f32) -> String {
1458 cols.iter()
1459 .map(|f| "*".repeat(f32::round(f * total_size) as usize))
1460 .collect::<Vec<String>>()
1461 .join("|")
1462 }
1463
1464 fn parse_resize_behavior(
1465 input: &str,
1466 total_size: f32,
1467 expected_cols: usize,
1468 ) -> Vec<TableResizeBehavior> {
1469 let mut resize_behavior = Vec::with_capacity(expected_cols);
1470 for col in input.split('|') {
1471 if col.starts_with('X') || col.is_empty() {
1472 resize_behavior.push(TableResizeBehavior::None);
1473 } else if col.starts_with('*') {
1474 resize_behavior.push(TableResizeBehavior::MinSize(col.len() as f32 / total_size));
1475 } else {
1476 panic!("invalid test input: unrecognized resize behavior: {}", col);
1477 }
1478 }
1479
1480 if resize_behavior.len() != expected_cols {
1481 panic!(
1482 "invalid test input: expected {} columns, got {}",
1483 expected_cols,
1484 resize_behavior.len()
1485 );
1486 }
1487 resize_behavior
1488 }
1489
1490 mod reset_column_size {
1491 use super::*;
1492
1493 fn parse(input: &str) -> (Vec<f32>, f32, Option<usize>) {
1494 let mut widths = Vec::new();
1495 let mut column_index = None;
1496 for (index, col) in input.split('|').enumerate() {
1497 widths.push(col.len() as f32);
1498 if col.starts_with('X') {
1499 column_index = Some(index);
1500 }
1501 }
1502
1503 for w in &widths {
1504 assert!(w.is_finite(), "incorrect number of columns");
1505 }
1506 let total = widths.iter().sum::<f32>();
1507 for width in &mut widths {
1508 *width /= total;
1509 }
1510 (widths, total, column_index)
1511 }
1512
1513 #[track_caller]
1514 fn check_reset_size(
1515 initial_sizes: &str,
1516 widths: &str,
1517 expected: &str,
1518 resize_behavior: &str,
1519 ) {
1520 let (initial_sizes, total_1, None) = parse(initial_sizes) else {
1521 panic!("invalid test input: initial sizes should not be marked");
1522 };
1523 let (widths, total_2, Some(column_index)) = parse(widths) else {
1524 panic!("invalid test input: widths should be marked");
1525 };
1526 assert_eq!(
1527 total_1, total_2,
1528 "invalid test input: total width not the same {total_1}, {total_2}"
1529 );
1530 let (expected, total_3, None) = parse(expected) else {
1531 panic!("invalid test input: expected should not be marked: {expected:?}");
1532 };
1533 assert_eq!(
1534 total_2, total_3,
1535 "invalid test input: total width not the same"
1536 );
1537 let cols = initial_sizes.len();
1538 let resize_behavior_vec = parse_resize_behavior(resize_behavior, total_1, cols);
1539 let resize_behavior = TableRow::from_vec(resize_behavior_vec, cols);
1540 let result = TableColumnWidths::reset_to_initial_size(
1541 column_index,
1542 TableRow::from_vec(widths, cols),
1543 TableRow::from_vec(initial_sizes, cols),
1544 &resize_behavior,
1545 );
1546 let result_slice = result.as_slice();
1547 let is_eq = is_almost_eq(result_slice, &expected);
1548 if !is_eq {
1549 let result_str = cols_to_str(result_slice, total_1);
1550 let expected_str = cols_to_str(&expected, total_1);
1551 panic!(
1552 "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result_slice:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}"
1553 );
1554 }
1555 }
1556
1557 macro_rules! check_reset_size {
1558 (columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => {
1559 check_reset_size($initial, $current, $expected, $resizing);
1560 };
1561 ($name:ident, columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => {
1562 #[test]
1563 fn $name() {
1564 check_reset_size($initial, $current, $expected, $resizing);
1565 }
1566 };
1567 }
1568
1569 check_reset_size!(
1570 basic_right,
1571 columns: 5,
1572 starting: "**|**|**|**|**",
1573 snapshot: "**|**|X|***|**",
1574 expected: "**|**|**|**|**",
1575 minimums: "X|*|*|*|*",
1576 );
1577
1578 check_reset_size!(
1579 basic_left,
1580 columns: 5,
1581 starting: "**|**|**|**|**",
1582 snapshot: "**|**|***|X|**",
1583 expected: "**|**|**|**|**",
1584 minimums: "X|*|*|*|**",
1585 );
1586
1587 check_reset_size!(
1588 squashed_left_reset_col2,
1589 columns: 6,
1590 starting: "*|***|**|**|****|*",
1591 snapshot: "*|*|X|*|*|********",
1592 expected: "*|*|**|*|*|*******",
1593 minimums: "X|*|*|*|*|*",
1594 );
1595
1596 check_reset_size!(
1597 grow_cascading_right,
1598 columns: 6,
1599 starting: "*|***|****|**|***|*",
1600 snapshot: "*|***|X|**|**|*****",
1601 expected: "*|***|****|*|*|****",
1602 minimums: "X|*|*|*|*|*",
1603 );
1604
1605 check_reset_size!(
1606 squashed_right_reset_col4,
1607 columns: 6,
1608 starting: "*|***|**|**|****|*",
1609 snapshot: "*|********|*|*|X|*",
1610 expected: "*|*****|*|*|****|*",
1611 minimums: "X|*|*|*|*|*",
1612 );
1613
1614 check_reset_size!(
1615 reset_col6_right,
1616 columns: 6,
1617 starting: "*|***|**|***|***|**",
1618 snapshot: "*|***|**|***|**|XXX",
1619 expected: "*|***|**|***|***|**",
1620 minimums: "X|*|*|*|*|*",
1621 );
1622
1623 check_reset_size!(
1624 reset_col6_left,
1625 columns: 6,
1626 starting: "*|***|**|***|***|**",
1627 snapshot: "*|***|**|***|****|X",
1628 expected: "*|***|**|***|***|**",
1629 minimums: "X|*|*|*|*|*",
1630 );
1631
1632 check_reset_size!(
1633 last_column_grow_cascading,
1634 columns: 6,
1635 starting: "*|***|**|**|**|***",
1636 snapshot: "*|*******|*|**|*|X",
1637 expected: "*|******|*|*|*|***",
1638 minimums: "X|*|*|*|*|*",
1639 );
1640
1641 check_reset_size!(
1642 goes_left_when_left_has_extreme_diff,
1643 columns: 6,
1644 starting: "*|***|****|**|**|***",
1645 snapshot: "*|********|X|*|**|**",
1646 expected: "*|*****|****|*|**|**",
1647 minimums: "X|*|*|*|*|*",
1648 );
1649
1650 check_reset_size!(
1651 basic_shrink_right,
1652 columns: 6,
1653 starting: "**|**|**|**|**|**",
1654 snapshot: "**|**|XXX|*|**|**",
1655 expected: "**|**|**|**|**|**",
1656 minimums: "X|*|*|*|*|*",
1657 );
1658
1659 check_reset_size!(
1660 shrink_should_go_left,
1661 columns: 6,
1662 starting: "*|***|**|*|*|*",
1663 snapshot: "*|*|XXX|**|*|*",
1664 expected: "*|**|**|**|*|*",
1665 minimums: "X|*|*|*|*|*",
1666 );
1667
1668 check_reset_size!(
1669 shrink_should_go_right,
1670 columns: 6,
1671 starting: "*|***|**|**|**|*",
1672 snapshot: "*|****|XXX|*|*|*",
1673 expected: "*|****|**|**|*|*",
1674 minimums: "X|*|*|*|*|*",
1675 );
1676 }
1677
1678 mod drag_handle {
1679 use super::*;
1680
1681 fn parse(input: &str) -> (Vec<f32>, f32, Option<usize>) {
1682 let mut widths = Vec::new();
1683 let column_index = input.replace("*", "").find("I");
1684 for col in input.replace("I", "|").split('|') {
1685 widths.push(col.len() as f32);
1686 }
1687
1688 for w in &widths {
1689 assert!(w.is_finite(), "incorrect number of columns");
1690 }
1691 let total = widths.iter().sum::<f32>();
1692 for width in &mut widths {
1693 *width /= total;
1694 }
1695 (widths, total, column_index)
1696 }
1697
1698 #[track_caller]
1699 fn check(distance: i32, widths: &str, expected: &str, resize_behavior: &str) {
1700 let (widths, total_1, Some(column_index)) = parse(widths) else {
1701 panic!("invalid test input: widths should be marked");
1702 };
1703 let (expected, total_2, None) = parse(expected) else {
1704 panic!("invalid test input: expected should not be marked: {expected:?}");
1705 };
1706 assert_eq!(
1707 total_1, total_2,
1708 "invalid test input: total width not the same"
1709 );
1710 let cols = widths.len();
1711 let resize_behavior_vec = parse_resize_behavior(resize_behavior, total_1, cols);
1712 let resize_behavior = TableRow::from_vec(resize_behavior_vec, cols);
1713
1714 let distance = distance as f32 / total_1;
1715
1716 let mut widths_table_row = TableRow::from_vec(widths, cols);
1717 TableColumnWidths::drag_column_handle(
1718 distance,
1719 column_index,
1720 &mut widths_table_row,
1721 &resize_behavior,
1722 );
1723
1724 let result_widths = widths_table_row.as_slice();
1725 let is_eq = is_almost_eq(result_widths, &expected);
1726 if !is_eq {
1727 let result_str = cols_to_str(result_widths, total_1);
1728 let expected_str = cols_to_str(&expected, total_1);
1729 panic!(
1730 "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result_widths:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}"
1731 );
1732 }
1733 }
1734
1735 macro_rules! check {
1736 (columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => {
1737 check($dist, $current, $expected, $resizing);
1738 };
1739 ($name:ident, columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => {
1740 #[test]
1741 fn $name() {
1742 check($dist, $current, $expected, $resizing);
1743 }
1744 };
1745 }
1746
1747 check!(
1748 basic_right_drag,
1749 columns: 3,
1750 distance: 1,
1751 snapshot: "**|**I**",
1752 expected: "**|***|*",
1753 minimums: "X|*|*",
1754 );
1755
1756 check!(
1757 drag_left_against_mins,
1758 columns: 5,
1759 distance: -1,
1760 snapshot: "*|*|*|*I*******",
1761 expected: "*|*|*|*|*******",
1762 minimums: "X|*|*|*|*",
1763 );
1764
1765 check!(
1766 drag_left,
1767 columns: 5,
1768 distance: -2,
1769 snapshot: "*|*|*|*****I***",
1770 expected: "*|*|*|***|*****",
1771 minimums: "X|*|*|*|*",
1772 );
1773 }
1774}