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