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