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