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 show_row_borders: bool,
727 width: Option<Length>,
728 headers: Option<TableRow<AnyElement>>,
729 rows: TableContents,
730 interaction_state: Option<WeakEntity<TableInteractionState>>,
731 col_widths: Option<TableWidths>,
732 map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
733 use_ui_font: bool,
734 empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
735 /// The number of columns in the table. Used to assert column numbers in `TableRow` collections
736 cols: usize,
737}
738
739impl Table {
740 /// Creates a new table with the specified number of columns.
741 pub fn new(cols: usize) -> Self {
742 Self {
743 cols,
744 striped: false,
745 show_row_borders: true,
746 width: None,
747 headers: None,
748 rows: TableContents::Vec(Vec::new()),
749 interaction_state: None,
750 map_row: None,
751 use_ui_font: true,
752 empty_table_callback: None,
753 col_widths: None,
754 }
755 }
756
757 /// Enables uniform list rendering.
758 /// The provided function will be passed directly to the `uniform_list` element.
759 /// Therefore, if this method is called, any calls to [`Table::row`] before or after
760 /// this method is called will be ignored.
761 pub fn uniform_list(
762 mut self,
763 id: impl Into<ElementId>,
764 row_count: usize,
765 render_item_fn: impl Fn(
766 Range<usize>,
767 &mut Window,
768 &mut App,
769 ) -> Vec<UncheckedTableRow<AnyElement>>
770 + 'static,
771 ) -> Self {
772 self.rows = TableContents::UniformList(UniformListData {
773 element_id: id.into(),
774 row_count,
775 render_list_of_rows_fn: Box::new(render_item_fn),
776 });
777 self
778 }
779
780 /// Enables rendering of tables with variable row heights, allowing each row to have its own height.
781 ///
782 /// This mode is useful for displaying content such as CSV data or multiline cells, where rows may not have uniform heights.
783 /// 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.
784 ///
785 /// # Parameters
786 /// - `row_count`: The total number of rows in the table.
787 /// - `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.
788 /// - `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.
789 pub fn variable_row_height_list(
790 mut self,
791 row_count: usize,
792 list_state: ListState,
793 render_row_fn: impl Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement> + 'static,
794 ) -> Self {
795 self.rows = TableContents::VariableRowHeightList(VariableRowHeightListData {
796 render_row_fn: Box::new(render_row_fn),
797 list_state,
798 row_count,
799 });
800 self
801 }
802
803 /// Enables row striping (alternating row colors)
804 pub fn striped(mut self) -> Self {
805 self.striped = true;
806 self
807 }
808
809 /// Hides the border lines between rows
810 pub fn hide_row_borders(mut self) -> Self {
811 self.show_row_borders = false;
812 self
813 }
814
815 /// Sets the width of the table.
816 /// Will enable horizontal scrolling if [`Self::interactable`] is also called.
817 pub fn width(mut self, width: impl Into<Length>) -> Self {
818 self.width = Some(width.into());
819 self
820 }
821
822 /// Enables interaction (primarily scrolling) with the table.
823 ///
824 /// Vertical scrolling will be enabled by default if the table is taller than its container.
825 ///
826 /// Horizontal scrolling will only be enabled if [`Self::width`] is also called, otherwise
827 /// the list will always shrink the table columns to fit their contents I.e. If [`Self::uniform_list`]
828 /// is used without a width and with [`Self::interactable`], the [`ListHorizontalSizingBehavior`] will
829 /// be set to [`ListHorizontalSizingBehavior::FitList`].
830 pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
831 self.interaction_state = Some(interaction_state.downgrade());
832 self
833 }
834
835 pub fn header(mut self, headers: UncheckedTableRow<impl IntoElement>) -> Self {
836 self.headers = Some(
837 headers
838 .into_table_row(self.cols)
839 .map(IntoElement::into_any_element),
840 );
841 self
842 }
843
844 pub fn row(mut self, items: UncheckedTableRow<impl IntoElement>) -> Self {
845 if let Some(rows) = self.rows.rows_mut() {
846 rows.push(
847 items
848 .into_table_row(self.cols)
849 .map(IntoElement::into_any_element),
850 );
851 }
852 self
853 }
854
855 pub fn column_widths(mut self, widths: UncheckedTableRow<impl Into<DefiniteLength>>) -> Self {
856 if self.col_widths.is_none() {
857 self.col_widths = Some(TableWidths::new(widths.into_table_row(self.cols)));
858 }
859 self
860 }
861
862 pub fn resizable_columns(
863 mut self,
864 resizable: UncheckedTableRow<TableResizeBehavior>,
865 column_widths: &Entity<TableColumnWidths>,
866 cx: &mut App,
867 ) -> Self {
868 if let Some(table_widths) = self.col_widths.as_mut() {
869 table_widths.resizable = resizable.into_table_row(self.cols);
870 let column_widths = table_widths
871 .current
872 .get_or_insert_with(|| column_widths.clone());
873
874 column_widths.update(cx, |widths, _| {
875 if !widths.initialized {
876 widths.initialized = true;
877 widths.widths = table_widths.initial.clone();
878 widths.visible_widths = widths.widths.clone();
879 }
880 })
881 }
882 self
883 }
884
885 pub fn no_ui_font(mut self) -> Self {
886 self.use_ui_font = false;
887 self
888 }
889
890 pub fn map_row(
891 mut self,
892 callback: impl Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement + 'static,
893 ) -> Self {
894 self.map_row = Some(Rc::new(callback));
895 self
896 }
897
898 /// Provide a callback that is invoked when the table is rendered without any rows
899 pub fn empty_table_callback(
900 mut self,
901 callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
902 ) -> Self {
903 self.empty_table_callback = Some(Rc::new(callback));
904 self
905 }
906}
907
908fn base_cell_style(width: Option<Length>) -> Div {
909 div()
910 .px_1p5()
911 .when_some(width, |this, width| this.w(width))
912 .when(width.is_none(), |this| this.flex_1())
913 .whitespace_nowrap()
914 .text_ellipsis()
915 .overflow_hidden()
916}
917
918fn base_cell_style_text(width: Option<Length>, use_ui_font: bool, cx: &App) -> Div {
919 base_cell_style(width).when(use_ui_font, |el| el.text_ui(cx))
920}
921
922pub fn render_table_row(
923 row_index: usize,
924 items: TableRow<impl IntoElement>,
925 table_context: TableRenderContext,
926 window: &mut Window,
927 cx: &mut App,
928) -> AnyElement {
929 let is_striped = table_context.striped;
930 let is_last = row_index == table_context.total_row_count - 1;
931 let bg = if row_index % 2 == 1 && is_striped {
932 Some(cx.theme().colors().text.opacity(0.05))
933 } else {
934 None
935 };
936 let cols = items.cols();
937 let column_widths = table_context
938 .column_widths
939 .map_or(vec![None; cols].into_table_row(cols), |widths| {
940 widths.map(Some)
941 });
942
943 let mut row = div()
944 // NOTE: `h_flex()` sneakily applies `items_center()` which is not default behavior for div element.
945 // Applying `.flex().flex_row()` manually to overcome that
946 .flex()
947 .flex_row()
948 .id(("table_row", row_index))
949 .size_full()
950 .when_some(bg, |row, bg| row.bg(bg))
951 .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.6)))
952 .when(!is_striped && table_context.show_row_borders, |row| {
953 row.border_b_1()
954 .border_color(transparent_black())
955 .when(!is_last, |row| row.border_color(cx.theme().colors().border))
956 });
957
958 row = row.children(
959 items
960 .map(IntoElement::into_any_element)
961 .into_vec()
962 .into_iter()
963 .zip(column_widths.into_vec())
964 .map(|(cell, width)| {
965 base_cell_style_text(width, table_context.use_ui_font, cx)
966 .px_1()
967 .py_0p5()
968 .child(cell)
969 }),
970 );
971
972 let row = if let Some(map_row) = table_context.map_row {
973 map_row((row_index, row), window, cx)
974 } else {
975 row.into_any_element()
976 };
977
978 div().size_full().child(row).into_any_element()
979}
980
981pub fn render_table_header(
982 headers: TableRow<impl IntoElement>,
983 table_context: TableRenderContext,
984 columns_widths: Option<(
985 WeakEntity<TableColumnWidths>,
986 TableRow<TableResizeBehavior>,
987 TableRow<DefiniteLength>,
988 )>,
989 entity_id: Option<EntityId>,
990 cx: &mut App,
991) -> impl IntoElement {
992 let cols = headers.cols();
993 let column_widths = table_context
994 .column_widths
995 .map_or(vec![None; cols].into_table_row(cols), |widths| {
996 widths.map(Some)
997 });
998
999 let element_id = entity_id
1000 .map(|entity| entity.to_string())
1001 .unwrap_or_default();
1002
1003 let shared_element_id: SharedString = format!("table-{}", element_id).into();
1004
1005 div()
1006 .flex()
1007 .flex_row()
1008 .items_center()
1009 .justify_between()
1010 .w_full()
1011 .p_2()
1012 .border_b_1()
1013 .border_color(cx.theme().colors().border)
1014 .children(
1015 headers
1016 .into_vec()
1017 .into_iter()
1018 .enumerate()
1019 .zip(column_widths.into_vec())
1020 .map(|((header_idx, h), width)| {
1021 base_cell_style_text(width, table_context.use_ui_font, cx)
1022 .child(h)
1023 .id(ElementId::NamedInteger(
1024 shared_element_id.clone(),
1025 header_idx as u64,
1026 ))
1027 .when_some(
1028 columns_widths.as_ref().cloned(),
1029 |this, (column_widths, resizables, initial_sizes)| {
1030 if resizables[header_idx].is_resizable() {
1031 this.on_click(move |event, window, cx| {
1032 if event.click_count() > 1 {
1033 column_widths
1034 .update(cx, |column, _| {
1035 column.on_double_click(
1036 header_idx,
1037 &initial_sizes,
1038 &resizables,
1039 window,
1040 );
1041 })
1042 .ok();
1043 }
1044 })
1045 } else {
1046 this
1047 }
1048 },
1049 )
1050 }),
1051 )
1052}
1053
1054#[derive(Clone)]
1055pub struct TableRenderContext {
1056 pub striped: bool,
1057 pub show_row_borders: bool,
1058 pub total_row_count: usize,
1059 pub column_widths: Option<TableRow<Length>>,
1060 pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
1061 pub use_ui_font: bool,
1062}
1063
1064impl TableRenderContext {
1065 fn new(table: &Table, cx: &App) -> Self {
1066 Self {
1067 striped: table.striped,
1068 show_row_borders: table.show_row_borders,
1069 total_row_count: table.rows.len(),
1070 column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)),
1071 map_row: table.map_row.clone(),
1072 use_ui_font: table.use_ui_font,
1073 }
1074 }
1075}
1076
1077impl RenderOnce for Table {
1078 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1079 let table_context = TableRenderContext::new(&self, cx);
1080 let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
1081 let current_widths = self
1082 .col_widths
1083 .as_ref()
1084 .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable.clone())))
1085 .map(|(curr, resize_behavior)| (curr.downgrade(), resize_behavior));
1086
1087 let current_widths_with_initial_sizes = self
1088 .col_widths
1089 .as_ref()
1090 .and_then(|widths| {
1091 Some((
1092 widths.current.as_ref()?,
1093 widths.resizable.clone(),
1094 widths.initial.clone(),
1095 ))
1096 })
1097 .map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial));
1098
1099 let width = self.width;
1100 let no_rows_rendered = self.rows.is_empty();
1101
1102 let table = div()
1103 .when_some(width, |this, width| this.w(width))
1104 .h_full()
1105 .v_flex()
1106 .when_some(self.headers.take(), |this, headers| {
1107 this.child(render_table_header(
1108 headers,
1109 table_context.clone(),
1110 current_widths_with_initial_sizes,
1111 interaction_state.as_ref().map(Entity::entity_id),
1112 cx,
1113 ))
1114 })
1115 .when_some(current_widths, {
1116 |this, (widths, resize_behavior)| {
1117 this.on_drag_move::<DraggedColumn>({
1118 let widths = widths.clone();
1119 move |e, window, cx| {
1120 widths
1121 .update(cx, |widths, cx| {
1122 widths.on_drag_move(e, &resize_behavior, window, cx);
1123 })
1124 .ok();
1125 }
1126 })
1127 .on_children_prepainted({
1128 let widths = widths.clone();
1129 move |bounds, _, cx| {
1130 widths
1131 .update(cx, |widths, _| {
1132 // This works because all children x axis bounds are the same
1133 widths.cached_bounds_width =
1134 bounds[0].right() - bounds[0].left();
1135 })
1136 .ok();
1137 }
1138 })
1139 .on_drop::<DraggedColumn>(move |_, _, cx| {
1140 widths
1141 .update(cx, |widths, _| {
1142 widths.widths = widths.visible_widths.clone();
1143 })
1144 .ok();
1145 // Finish the resize operation
1146 })
1147 }
1148 })
1149 .child({
1150 let content = div()
1151 .flex_grow()
1152 .w_full()
1153 .relative()
1154 .overflow_hidden()
1155 .map(|parent| match self.rows {
1156 TableContents::Vec(items) => {
1157 parent.children(items.into_iter().enumerate().map(|(index, row)| {
1158 div().child(render_table_row(
1159 index,
1160 row,
1161 table_context.clone(),
1162 window,
1163 cx,
1164 ))
1165 }))
1166 }
1167 TableContents::UniformList(uniform_list_data) => parent.child(
1168 uniform_list(
1169 uniform_list_data.element_id,
1170 uniform_list_data.row_count,
1171 {
1172 let render_item_fn = uniform_list_data.render_list_of_rows_fn;
1173 move |range: Range<usize>, window, cx| {
1174 let elements = render_item_fn(range.clone(), window, cx)
1175 .into_iter()
1176 .map(|raw_row| raw_row.into_table_row(self.cols))
1177 .collect::<Vec<_>>();
1178 elements
1179 .into_iter()
1180 .zip(range)
1181 .map(|(row, row_index)| {
1182 render_table_row(
1183 row_index,
1184 row,
1185 table_context.clone(),
1186 window,
1187 cx,
1188 )
1189 })
1190 .collect()
1191 }
1192 },
1193 )
1194 .size_full()
1195 .flex_grow()
1196 .with_sizing_behavior(ListSizingBehavior::Auto)
1197 .with_horizontal_sizing_behavior(if width.is_some() {
1198 ListHorizontalSizingBehavior::Unconstrained
1199 } else {
1200 ListHorizontalSizingBehavior::FitList
1201 })
1202 .when_some(
1203 interaction_state.as_ref(),
1204 |this, state| {
1205 this.track_scroll(
1206 &state.read_with(cx, |s, _| s.scroll_handle.clone()),
1207 )
1208 },
1209 ),
1210 ),
1211 TableContents::VariableRowHeightList(variable_list_data) => parent.child(
1212 list(variable_list_data.list_state.clone(), {
1213 let render_item_fn = variable_list_data.render_row_fn;
1214 move |row_index: usize, window: &mut Window, cx: &mut App| {
1215 let row = render_item_fn(row_index, window, cx)
1216 .into_table_row(self.cols);
1217 render_table_row(
1218 row_index,
1219 row,
1220 table_context.clone(),
1221 window,
1222 cx,
1223 )
1224 }
1225 })
1226 .size_full()
1227 .flex_grow()
1228 .with_sizing_behavior(ListSizingBehavior::Auto),
1229 ),
1230 })
1231 .when_some(
1232 self.col_widths.as_ref().zip(interaction_state.as_ref()),
1233 |parent, (table_widths, state)| {
1234 parent.child(state.update(cx, |state, cx| {
1235 let resizable_columns = &table_widths.resizable;
1236 let column_widths = table_widths.lengths(cx);
1237 let columns = table_widths.current.clone();
1238 let initial_sizes = &table_widths.initial;
1239 state.render_resize_handles(
1240 &column_widths,
1241 resizable_columns,
1242 initial_sizes,
1243 columns,
1244 window,
1245 cx,
1246 )
1247 }))
1248 },
1249 );
1250
1251 if let Some(state) = interaction_state.as_ref() {
1252 let scrollbars = state
1253 .read(cx)
1254 .custom_scrollbar
1255 .clone()
1256 .unwrap_or_else(|| Scrollbars::new(ScrollAxes::Both));
1257 content
1258 .custom_scrollbars(
1259 scrollbars.tracked_scroll_handle(&state.read(cx).scroll_handle),
1260 window,
1261 cx,
1262 )
1263 .into_any_element()
1264 } else {
1265 content.into_any_element()
1266 }
1267 })
1268 .when_some(
1269 no_rows_rendered
1270 .then_some(self.empty_table_callback)
1271 .flatten(),
1272 |this, callback| {
1273 this.child(
1274 h_flex()
1275 .size_full()
1276 .p_3()
1277 .items_start()
1278 .justify_center()
1279 .child(callback(window, cx)),
1280 )
1281 },
1282 );
1283
1284 if let Some(interaction_state) = interaction_state.as_ref() {
1285 table
1286 .track_focus(&interaction_state.read(cx).focus_handle)
1287 .id(("table", interaction_state.entity_id()))
1288 .into_any_element()
1289 } else {
1290 table.into_any_element()
1291 }
1292 }
1293}
1294
1295impl Component for Table {
1296 fn scope() -> ComponentScope {
1297 ComponentScope::Layout
1298 }
1299
1300 fn description() -> Option<&'static str> {
1301 Some("A table component for displaying data in rows and columns with optional styling.")
1302 }
1303
1304 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
1305 Some(
1306 v_flex()
1307 .gap_6()
1308 .children(vec![
1309 example_group_with_title(
1310 "Basic Tables",
1311 vec![
1312 single_example(
1313 "Simple Table",
1314 Table::new(3)
1315 .width(px(400.))
1316 .header(vec!["Name", "Age", "City"])
1317 .row(vec!["Alice", "28", "New York"])
1318 .row(vec!["Bob", "32", "San Francisco"])
1319 .row(vec!["Charlie", "25", "London"])
1320 .into_any_element(),
1321 ),
1322 single_example(
1323 "Two Column Table",
1324 Table::new(2)
1325 .header(vec!["Category", "Value"])
1326 .width(px(300.))
1327 .row(vec!["Revenue", "$100,000"])
1328 .row(vec!["Expenses", "$75,000"])
1329 .row(vec!["Profit", "$25,000"])
1330 .into_any_element(),
1331 ),
1332 ],
1333 ),
1334 example_group_with_title(
1335 "Styled Tables",
1336 vec![
1337 single_example(
1338 "Default",
1339 Table::new(3)
1340 .width(px(400.))
1341 .header(vec!["Product", "Price", "Stock"])
1342 .row(vec!["Laptop", "$999", "In Stock"])
1343 .row(vec!["Phone", "$599", "Low Stock"])
1344 .row(vec!["Tablet", "$399", "Out of Stock"])
1345 .into_any_element(),
1346 ),
1347 single_example(
1348 "Striped",
1349 Table::new(3)
1350 .width(px(400.))
1351 .striped()
1352 .header(vec!["Product", "Price", "Stock"])
1353 .row(vec!["Laptop", "$999", "In Stock"])
1354 .row(vec!["Phone", "$599", "Low Stock"])
1355 .row(vec!["Tablet", "$399", "Out of Stock"])
1356 .row(vec!["Headphones", "$199", "In Stock"])
1357 .into_any_element(),
1358 ),
1359 ],
1360 ),
1361 example_group_with_title(
1362 "Mixed Content Table",
1363 vec![single_example(
1364 "Table with Elements",
1365 Table::new(5)
1366 .width(px(840.))
1367 .header(vec!["Status", "Name", "Priority", "Deadline", "Action"])
1368 .row(vec![
1369 Indicator::dot().color(Color::Success).into_any_element(),
1370 "Project A".into_any_element(),
1371 "High".into_any_element(),
1372 "2023-12-31".into_any_element(),
1373 Button::new("view_a", "View")
1374 .style(ButtonStyle::Filled)
1375 .full_width()
1376 .into_any_element(),
1377 ])
1378 .row(vec![
1379 Indicator::dot().color(Color::Warning).into_any_element(),
1380 "Project B".into_any_element(),
1381 "Medium".into_any_element(),
1382 "2024-03-15".into_any_element(),
1383 Button::new("view_b", "View")
1384 .style(ButtonStyle::Filled)
1385 .full_width()
1386 .into_any_element(),
1387 ])
1388 .row(vec![
1389 Indicator::dot().color(Color::Error).into_any_element(),
1390 "Project C".into_any_element(),
1391 "Low".into_any_element(),
1392 "2024-06-30".into_any_element(),
1393 Button::new("view_c", "View")
1394 .style(ButtonStyle::Filled)
1395 .full_width()
1396 .into_any_element(),
1397 ])
1398 .into_any_element(),
1399 )],
1400 ),
1401 ])
1402 .into_any_element(),
1403 )
1404 }
1405}
1406
1407#[cfg(test)]
1408mod test {
1409 use super::*;
1410
1411 fn is_almost_eq(a: &[f32], b: &[f32]) -> bool {
1412 a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6)
1413 }
1414
1415 fn cols_to_str(cols: &[f32], total_size: f32) -> String {
1416 cols.iter()
1417 .map(|f| "*".repeat(f32::round(f * total_size) as usize))
1418 .collect::<Vec<String>>()
1419 .join("|")
1420 }
1421
1422 fn parse_resize_behavior(
1423 input: &str,
1424 total_size: f32,
1425 expected_cols: usize,
1426 ) -> Vec<TableResizeBehavior> {
1427 let mut resize_behavior = Vec::with_capacity(expected_cols);
1428 for col in input.split('|') {
1429 if col.starts_with('X') || col.is_empty() {
1430 resize_behavior.push(TableResizeBehavior::None);
1431 } else if col.starts_with('*') {
1432 resize_behavior.push(TableResizeBehavior::MinSize(col.len() as f32 / total_size));
1433 } else {
1434 panic!("invalid test input: unrecognized resize behavior: {}", col);
1435 }
1436 }
1437
1438 if resize_behavior.len() != expected_cols {
1439 panic!(
1440 "invalid test input: expected {} columns, got {}",
1441 expected_cols,
1442 resize_behavior.len()
1443 );
1444 }
1445 resize_behavior
1446 }
1447
1448 mod reset_column_size {
1449 use super::*;
1450
1451 fn parse(input: &str) -> (Vec<f32>, f32, Option<usize>) {
1452 let mut widths = Vec::new();
1453 let mut column_index = None;
1454 for (index, col) in input.split('|').enumerate() {
1455 widths.push(col.len() as f32);
1456 if col.starts_with('X') {
1457 column_index = Some(index);
1458 }
1459 }
1460
1461 for w in &widths {
1462 assert!(w.is_finite(), "incorrect number of columns");
1463 }
1464 let total = widths.iter().sum::<f32>();
1465 for width in &mut widths {
1466 *width /= total;
1467 }
1468 (widths, total, column_index)
1469 }
1470
1471 #[track_caller]
1472 fn check_reset_size(
1473 initial_sizes: &str,
1474 widths: &str,
1475 expected: &str,
1476 resize_behavior: &str,
1477 ) {
1478 let (initial_sizes, total_1, None) = parse(initial_sizes) else {
1479 panic!("invalid test input: initial sizes should not be marked");
1480 };
1481 let (widths, total_2, Some(column_index)) = parse(widths) else {
1482 panic!("invalid test input: widths should be marked");
1483 };
1484 assert_eq!(
1485 total_1, total_2,
1486 "invalid test input: total width not the same {total_1}, {total_2}"
1487 );
1488 let (expected, total_3, None) = parse(expected) else {
1489 panic!("invalid test input: expected should not be marked: {expected:?}");
1490 };
1491 assert_eq!(
1492 total_2, total_3,
1493 "invalid test input: total width not the same"
1494 );
1495 let cols = initial_sizes.len();
1496 let resize_behavior_vec = parse_resize_behavior(resize_behavior, total_1, cols);
1497 let resize_behavior = TableRow::from_vec(resize_behavior_vec, cols);
1498 let result = TableColumnWidths::reset_to_initial_size(
1499 column_index,
1500 TableRow::from_vec(widths, cols),
1501 TableRow::from_vec(initial_sizes, cols),
1502 &resize_behavior,
1503 );
1504 let result_slice = result.as_slice();
1505 let is_eq = is_almost_eq(result_slice, &expected);
1506 if !is_eq {
1507 let result_str = cols_to_str(result_slice, total_1);
1508 let expected_str = cols_to_str(&expected, total_1);
1509 panic!(
1510 "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result_slice:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}"
1511 );
1512 }
1513 }
1514
1515 macro_rules! check_reset_size {
1516 (columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => {
1517 check_reset_size($initial, $current, $expected, $resizing);
1518 };
1519 ($name:ident, columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => {
1520 #[test]
1521 fn $name() {
1522 check_reset_size($initial, $current, $expected, $resizing);
1523 }
1524 };
1525 }
1526
1527 check_reset_size!(
1528 basic_right,
1529 columns: 5,
1530 starting: "**|**|**|**|**",
1531 snapshot: "**|**|X|***|**",
1532 expected: "**|**|**|**|**",
1533 minimums: "X|*|*|*|*",
1534 );
1535
1536 check_reset_size!(
1537 basic_left,
1538 columns: 5,
1539 starting: "**|**|**|**|**",
1540 snapshot: "**|**|***|X|**",
1541 expected: "**|**|**|**|**",
1542 minimums: "X|*|*|*|**",
1543 );
1544
1545 check_reset_size!(
1546 squashed_left_reset_col2,
1547 columns: 6,
1548 starting: "*|***|**|**|****|*",
1549 snapshot: "*|*|X|*|*|********",
1550 expected: "*|*|**|*|*|*******",
1551 minimums: "X|*|*|*|*|*",
1552 );
1553
1554 check_reset_size!(
1555 grow_cascading_right,
1556 columns: 6,
1557 starting: "*|***|****|**|***|*",
1558 snapshot: "*|***|X|**|**|*****",
1559 expected: "*|***|****|*|*|****",
1560 minimums: "X|*|*|*|*|*",
1561 );
1562
1563 check_reset_size!(
1564 squashed_right_reset_col4,
1565 columns: 6,
1566 starting: "*|***|**|**|****|*",
1567 snapshot: "*|********|*|*|X|*",
1568 expected: "*|*****|*|*|****|*",
1569 minimums: "X|*|*|*|*|*",
1570 );
1571
1572 check_reset_size!(
1573 reset_col6_right,
1574 columns: 6,
1575 starting: "*|***|**|***|***|**",
1576 snapshot: "*|***|**|***|**|XXX",
1577 expected: "*|***|**|***|***|**",
1578 minimums: "X|*|*|*|*|*",
1579 );
1580
1581 check_reset_size!(
1582 reset_col6_left,
1583 columns: 6,
1584 starting: "*|***|**|***|***|**",
1585 snapshot: "*|***|**|***|****|X",
1586 expected: "*|***|**|***|***|**",
1587 minimums: "X|*|*|*|*|*",
1588 );
1589
1590 check_reset_size!(
1591 last_column_grow_cascading,
1592 columns: 6,
1593 starting: "*|***|**|**|**|***",
1594 snapshot: "*|*******|*|**|*|X",
1595 expected: "*|******|*|*|*|***",
1596 minimums: "X|*|*|*|*|*",
1597 );
1598
1599 check_reset_size!(
1600 goes_left_when_left_has_extreme_diff,
1601 columns: 6,
1602 starting: "*|***|****|**|**|***",
1603 snapshot: "*|********|X|*|**|**",
1604 expected: "*|*****|****|*|**|**",
1605 minimums: "X|*|*|*|*|*",
1606 );
1607
1608 check_reset_size!(
1609 basic_shrink_right,
1610 columns: 6,
1611 starting: "**|**|**|**|**|**",
1612 snapshot: "**|**|XXX|*|**|**",
1613 expected: "**|**|**|**|**|**",
1614 minimums: "X|*|*|*|*|*",
1615 );
1616
1617 check_reset_size!(
1618 shrink_should_go_left,
1619 columns: 6,
1620 starting: "*|***|**|*|*|*",
1621 snapshot: "*|*|XXX|**|*|*",
1622 expected: "*|**|**|**|*|*",
1623 minimums: "X|*|*|*|*|*",
1624 );
1625
1626 check_reset_size!(
1627 shrink_should_go_right,
1628 columns: 6,
1629 starting: "*|***|**|**|**|*",
1630 snapshot: "*|****|XXX|*|*|*",
1631 expected: "*|****|**|**|*|*",
1632 minimums: "X|*|*|*|*|*",
1633 );
1634 }
1635
1636 mod drag_handle {
1637 use super::*;
1638
1639 fn parse(input: &str) -> (Vec<f32>, f32, Option<usize>) {
1640 let mut widths = Vec::new();
1641 let column_index = input.replace("*", "").find("I");
1642 for col in input.replace("I", "|").split('|') {
1643 widths.push(col.len() as f32);
1644 }
1645
1646 for w in &widths {
1647 assert!(w.is_finite(), "incorrect number of columns");
1648 }
1649 let total = widths.iter().sum::<f32>();
1650 for width in &mut widths {
1651 *width /= total;
1652 }
1653 (widths, total, column_index)
1654 }
1655
1656 #[track_caller]
1657 fn check(distance: i32, widths: &str, expected: &str, resize_behavior: &str) {
1658 let (widths, total_1, Some(column_index)) = parse(widths) else {
1659 panic!("invalid test input: widths should be marked");
1660 };
1661 let (expected, total_2, None) = parse(expected) else {
1662 panic!("invalid test input: expected should not be marked: {expected:?}");
1663 };
1664 assert_eq!(
1665 total_1, total_2,
1666 "invalid test input: total width not the same"
1667 );
1668 let cols = widths.len();
1669 let resize_behavior_vec = parse_resize_behavior(resize_behavior, total_1, cols);
1670 let resize_behavior = TableRow::from_vec(resize_behavior_vec, cols);
1671
1672 let distance = distance as f32 / total_1;
1673
1674 let mut widths_table_row = TableRow::from_vec(widths, cols);
1675 TableColumnWidths::drag_column_handle(
1676 distance,
1677 column_index,
1678 &mut widths_table_row,
1679 &resize_behavior,
1680 );
1681
1682 let result_widths = widths_table_row.as_slice();
1683 let is_eq = is_almost_eq(result_widths, &expected);
1684 if !is_eq {
1685 let result_str = cols_to_str(result_widths, total_1);
1686 let expected_str = cols_to_str(&expected, total_1);
1687 panic!(
1688 "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result_widths:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}"
1689 );
1690 }
1691 }
1692
1693 macro_rules! check {
1694 (columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => {
1695 check($dist, $current, $expected, $resizing);
1696 };
1697 ($name:ident, columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => {
1698 #[test]
1699 fn $name() {
1700 check($dist, $current, $expected, $resizing);
1701 }
1702 };
1703 }
1704
1705 check!(
1706 basic_right_drag,
1707 columns: 3,
1708 distance: 1,
1709 snapshot: "**|**I**",
1710 expected: "**|***|*",
1711 minimums: "X|*|*",
1712 );
1713
1714 check!(
1715 drag_left_against_mins,
1716 columns: 5,
1717 distance: -1,
1718 snapshot: "*|*|*|*I*******",
1719 expected: "*|*|*|*|*******",
1720 minimums: "X|*|*|*|*",
1721 );
1722
1723 check!(
1724 drag_left,
1725 columns: 5,
1726 distance: -2,
1727 snapshot: "*|*|*|*****I***",
1728 expected: "*|*|*|***|*****",
1729 minimums: "X|*|*|*|*",
1730 );
1731 }
1732}