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, FocusHandle,
6 Length, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Point, Stateful, Task,
7 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, StatefulInteractiveElement, Styled, StyledExt as _,
17 StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex,
18};
19
20#[derive(Debug)]
21struct DraggedColumn(usize);
22
23struct UniformListData<const COLS: usize> {
24 render_item_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>,
25 element_id: ElementId,
26 row_count: usize,
27}
28
29enum TableContents<const COLS: usize> {
30 Vec(Vec<[AnyElement; COLS]>),
31 UniformList(UniformListData<COLS>),
32}
33
34impl<const COLS: usize> TableContents<COLS> {
35 fn rows_mut(&mut self) -> Option<&mut Vec<[AnyElement; COLS]>> {
36 match self {
37 TableContents::Vec(rows) => Some(rows),
38 TableContents::UniformList(_) => None,
39 }
40 }
41
42 fn len(&self) -> usize {
43 match self {
44 TableContents::Vec(rows) => rows.len(),
45 TableContents::UniformList(data) => data.row_count,
46 }
47 }
48
49 fn is_empty(&self) -> bool {
50 self.len() == 0
51 }
52}
53
54pub struct TableInteractionState {
55 pub focus_handle: FocusHandle,
56 pub scroll_handle: UniformListScrollHandle,
57 pub horizontal_scrollbar: ScrollbarProperties,
58 pub vertical_scrollbar: ScrollbarProperties,
59}
60
61impl TableInteractionState {
62 pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
63 cx.new(|cx| {
64 let focus_handle = cx.focus_handle();
65
66 cx.on_focus_out(&focus_handle, window, |this: &mut Self, _, window, cx| {
67 this.hide_scrollbars(window, cx);
68 })
69 .detach();
70
71 let scroll_handle = UniformListScrollHandle::new();
72 let vertical_scrollbar = ScrollbarProperties {
73 axis: Axis::Vertical,
74 state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
75 show_scrollbar: false,
76 show_track: false,
77 auto_hide: false,
78 hide_task: None,
79 };
80
81 let horizontal_scrollbar = ScrollbarProperties {
82 axis: Axis::Horizontal,
83 state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
84 show_scrollbar: false,
85 show_track: false,
86 auto_hide: false,
87 hide_task: None,
88 };
89
90 let mut this = Self {
91 focus_handle,
92 scroll_handle,
93 horizontal_scrollbar,
94 vertical_scrollbar,
95 };
96
97 this.update_scrollbar_visibility(cx);
98 this
99 })
100 }
101
102 pub fn get_scrollbar_offset(&self, axis: Axis) -> Point<Pixels> {
103 match axis {
104 Axis::Vertical => self.vertical_scrollbar.state.scroll_handle().offset(),
105 Axis::Horizontal => self.horizontal_scrollbar.state.scroll_handle().offset(),
106 }
107 }
108
109 pub fn set_scrollbar_offset(&self, axis: Axis, offset: Point<Pixels>) {
110 match axis {
111 Axis::Vertical => self
112 .vertical_scrollbar
113 .state
114 .scroll_handle()
115 .set_offset(offset),
116 Axis::Horizontal => self
117 .horizontal_scrollbar
118 .state
119 .scroll_handle()
120 .set_offset(offset),
121 }
122 }
123
124 fn update_scrollbar_visibility(&mut self, cx: &mut Context<Self>) {
125 let show_setting = EditorSettings::get_global(cx).scrollbar.show;
126
127 let scroll_handle = self.scroll_handle.0.borrow();
128
129 let autohide = |show: ShowScrollbar, cx: &mut Context<Self>| match show {
130 ShowScrollbar::Auto => true,
131 ShowScrollbar::System => cx
132 .try_global::<ScrollbarAutoHide>()
133 .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
134 ShowScrollbar::Always => false,
135 ShowScrollbar::Never => false,
136 };
137
138 let longest_item_width = scroll_handle.last_item_size.and_then(|size| {
139 (size.contents.width > size.item.width).then_some(size.contents.width)
140 });
141
142 // is there an item long enough that we should show a horizontal scrollbar?
143 let item_wider_than_container = if let Some(longest_item_width) = longest_item_width {
144 longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0)
145 } else {
146 true
147 };
148
149 let show_scrollbar = match show_setting {
150 ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true,
151 ShowScrollbar::Never => false,
152 };
153 let show_vertical = show_scrollbar;
154
155 let show_horizontal = item_wider_than_container && show_scrollbar;
156
157 let show_horizontal_track =
158 show_horizontal && matches!(show_setting, ShowScrollbar::Always);
159
160 // TODO: we probably should hide the scroll track when the list doesn't need to scroll
161 let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always);
162
163 self.vertical_scrollbar = ScrollbarProperties {
164 axis: self.vertical_scrollbar.axis,
165 state: self.vertical_scrollbar.state.clone(),
166 show_scrollbar: show_vertical,
167 show_track: show_vertical_track,
168 auto_hide: autohide(show_setting, cx),
169 hide_task: None,
170 };
171
172 self.horizontal_scrollbar = ScrollbarProperties {
173 axis: self.horizontal_scrollbar.axis,
174 state: self.horizontal_scrollbar.state.clone(),
175 show_scrollbar: show_horizontal,
176 show_track: show_horizontal_track,
177 auto_hide: autohide(show_setting, cx),
178 hide_task: None,
179 };
180
181 cx.notify();
182 }
183
184 fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
185 self.horizontal_scrollbar.hide(window, cx);
186 self.vertical_scrollbar.hide(window, cx);
187 }
188
189 pub fn listener<E: ?Sized>(
190 this: &Entity<Self>,
191 f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static,
192 ) -> impl Fn(&E, &mut Window, &mut App) + 'static {
193 let view = this.downgrade();
194 move |e: &E, window: &mut Window, cx: &mut App| {
195 view.update(cx, |view, cx| f(view, e, window, cx)).ok();
196 }
197 }
198
199 fn render_resize_handles<const COLS: usize>(
200 &self,
201 column_widths: &[Length; COLS],
202 resizable_columns: &[ResizeBehavior; COLS],
203 initial_sizes: [DefiniteLength; COLS],
204 columns: Option<Entity<ColumnWidths<COLS>>>,
205 window: &mut Window,
206 cx: &mut App,
207 ) -> AnyElement {
208 let spacers = column_widths
209 .iter()
210 .map(|width| base_cell_style(Some(*width)).into_any_element());
211
212 let mut column_ix = 0;
213 let resizable_columns_slice = *resizable_columns;
214 let mut resizable_columns = resizable_columns.into_iter();
215 let dividers = intersperse_with(spacers, || {
216 window.with_id(column_ix, |window| {
217 let mut resize_divider = div()
218 // This is required because this is evaluated at a different time than the use_state call above
219 .id(column_ix)
220 .relative()
221 .top_0()
222 .w_0p5()
223 .h_full()
224 .bg(cx.theme().colors().border.opacity(0.5));
225
226 let mut resize_handle = div()
227 .id("column-resize-handle")
228 .absolute()
229 .left_neg_0p5()
230 .w(px(5.0))
231 .h_full();
232
233 if resizable_columns
234 .next()
235 .is_some_and(ResizeBehavior::is_resizable)
236 {
237 let hovered = window.use_state(cx, |_window, _cx| false);
238 resize_divider = resize_divider.when(*hovered.read(cx), |div| {
239 div.bg(cx.theme().colors().border_focused)
240 });
241 resize_handle = resize_handle
242 .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered))
243 .cursor_col_resize()
244 .when_some(columns.clone(), |this, columns| {
245 this.on_click(move |event, window, cx| {
246 if event.down.click_count >= 2 {
247 columns.update(cx, |columns, _| {
248 columns.on_double_click(
249 column_ix,
250 &initial_sizes,
251 &resizable_columns_slice,
252 window,
253 );
254 })
255 }
256
257 cx.stop_propagation();
258 })
259 })
260 .on_drag(DraggedColumn(column_ix), |_, _offset, _window, cx| {
261 cx.new(|_cx| gpui::Empty)
262 })
263 }
264
265 column_ix += 1;
266 resize_divider.child(resize_handle).into_any_element()
267 })
268 });
269
270 div()
271 .id("resize-handles")
272 .h_flex()
273 .absolute()
274 .w_full()
275 .inset_0()
276 .children(dividers)
277 .into_any_element()
278 }
279
280 fn render_vertical_scrollbar_track(
281 this: &Entity<Self>,
282 parent: Div,
283 scroll_track_size: Pixels,
284 cx: &mut App,
285 ) -> Div {
286 if !this.read(cx).vertical_scrollbar.show_track {
287 return parent;
288 }
289 let child = v_flex()
290 .h_full()
291 .flex_none()
292 .w(scroll_track_size)
293 .bg(cx.theme().colors().background)
294 .child(
295 div()
296 .size_full()
297 .flex_1()
298 .border_l_1()
299 .border_color(cx.theme().colors().border),
300 );
301 parent.child(child)
302 }
303
304 fn render_vertical_scrollbar(this: &Entity<Self>, parent: Div, cx: &mut App) -> Div {
305 if !this.read(cx).vertical_scrollbar.show_scrollbar {
306 return parent;
307 }
308 let child = div()
309 .id(("table-vertical-scrollbar", this.entity_id()))
310 .occlude()
311 .flex_none()
312 .h_full()
313 .cursor_default()
314 .absolute()
315 .right_0()
316 .top_0()
317 .bottom_0()
318 .w(px(12.))
319 .on_mouse_move(Self::listener(this, |_, _, _, cx| {
320 cx.notify();
321 cx.stop_propagation()
322 }))
323 .on_hover(|_, _, cx| {
324 cx.stop_propagation();
325 })
326 .on_mouse_up(
327 MouseButton::Left,
328 Self::listener(this, |this, _, window, cx| {
329 if !this.vertical_scrollbar.state.is_dragging()
330 && !this.focus_handle.contains_focused(window, cx)
331 {
332 this.vertical_scrollbar.hide(window, cx);
333 cx.notify();
334 }
335
336 cx.stop_propagation();
337 }),
338 )
339 .on_any_mouse_down(|_, _, cx| {
340 cx.stop_propagation();
341 })
342 .on_scroll_wheel(Self::listener(&this, |_, _, _, cx| {
343 cx.notify();
344 }))
345 .children(Scrollbar::vertical(
346 this.read(cx).vertical_scrollbar.state.clone(),
347 ));
348 parent.child(child)
349 }
350
351 /// Renders the horizontal scrollbar.
352 ///
353 /// The right offset is used to determine how far to the right the
354 /// scrollbar should extend to, useful for ensuring it doesn't collide
355 /// with the vertical scrollbar when visible.
356 fn render_horizontal_scrollbar(
357 this: &Entity<Self>,
358 parent: Div,
359 right_offset: Pixels,
360 cx: &mut App,
361 ) -> Div {
362 if !this.read(cx).horizontal_scrollbar.show_scrollbar {
363 return parent;
364 }
365 let child = div()
366 .id(("table-horizontal-scrollbar", this.entity_id()))
367 .occlude()
368 .flex_none()
369 .w_full()
370 .cursor_default()
371 .absolute()
372 .bottom_neg_px()
373 .left_0()
374 .right_0()
375 .pr(right_offset)
376 .on_mouse_move(Self::listener(this, |_, _, _, cx| {
377 cx.notify();
378 cx.stop_propagation()
379 }))
380 .on_hover(|_, _, cx| {
381 cx.stop_propagation();
382 })
383 .on_any_mouse_down(|_, _, cx| {
384 cx.stop_propagation();
385 })
386 .on_mouse_up(
387 MouseButton::Left,
388 Self::listener(this, |this, _, window, cx| {
389 if !this.horizontal_scrollbar.state.is_dragging()
390 && !this.focus_handle.contains_focused(window, cx)
391 {
392 this.horizontal_scrollbar.hide(window, cx);
393 cx.notify();
394 }
395
396 cx.stop_propagation();
397 }),
398 )
399 .on_scroll_wheel(Self::listener(this, |_, _, _, cx| {
400 cx.notify();
401 }))
402 .children(Scrollbar::horizontal(
403 // percentage as f32..end_offset as f32,
404 this.read(cx).horizontal_scrollbar.state.clone(),
405 ));
406 parent.child(child)
407 }
408
409 fn render_horizontal_scrollbar_track(
410 this: &Entity<Self>,
411 parent: Div,
412 scroll_track_size: Pixels,
413 cx: &mut App,
414 ) -> Div {
415 if !this.read(cx).horizontal_scrollbar.show_track {
416 return parent;
417 }
418 let child = h_flex()
419 .w_full()
420 .h(scroll_track_size)
421 .flex_none()
422 .relative()
423 .child(
424 div()
425 .w_full()
426 .flex_1()
427 // for some reason the horizontal scrollbar is 1px
428 // taller than the vertical scrollbar??
429 .h(scroll_track_size - px(1.))
430 .bg(cx.theme().colors().background)
431 .border_t_1()
432 .border_color(cx.theme().colors().border),
433 )
434 .when(this.read(cx).vertical_scrollbar.show_track, |parent| {
435 parent
436 .child(
437 div()
438 .flex_none()
439 // -1px prevents a missing pixel between the two container borders
440 .w(scroll_track_size - px(1.))
441 .h_full(),
442 )
443 .child(
444 // HACK: Fill the missing 1px 🥲
445 div()
446 .absolute()
447 .right(scroll_track_size - px(1.))
448 .bottom(scroll_track_size - px(1.))
449 .size_px()
450 .bg(cx.theme().colors().border),
451 )
452 });
453
454 parent.child(child)
455 }
456}
457
458#[derive(Debug, Copy, Clone, PartialEq)]
459pub enum ResizeBehavior {
460 None,
461 Resizable,
462 MinSize(f32),
463}
464
465impl ResizeBehavior {
466 pub fn is_resizable(&self) -> bool {
467 *self != ResizeBehavior::None
468 }
469
470 pub fn min_size(&self) -> Option<f32> {
471 match self {
472 ResizeBehavior::None => None,
473 ResizeBehavior::Resizable => Some(0.05),
474 ResizeBehavior::MinSize(min_size) => Some(*min_size),
475 }
476 }
477}
478
479pub struct ColumnWidths<const COLS: usize> {
480 widths: [DefiniteLength; COLS],
481 cached_bounds_width: Pixels,
482 initialized: bool,
483}
484
485impl<const COLS: usize> ColumnWidths<COLS> {
486 pub fn new(_: &mut App) -> Self {
487 Self {
488 widths: [DefiniteLength::default(); COLS],
489 cached_bounds_width: Default::default(),
490 initialized: false,
491 }
492 }
493
494 fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 {
495 match length {
496 DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => *pixels / bounds_width,
497 DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => {
498 rems_width.to_pixels(rem_size) / bounds_width
499 }
500 DefiniteLength::Fraction(fraction) => *fraction,
501 }
502 }
503
504 fn on_double_click(
505 &mut self,
506 double_click_position: usize,
507 initial_sizes: &[DefiniteLength; COLS],
508 resize_behavior: &[ResizeBehavior; COLS],
509 window: &mut Window,
510 ) {
511 let bounds_width = self.cached_bounds_width;
512 let rem_size = window.rem_size();
513 let initial_sizes =
514 initial_sizes.map(|length| Self::get_fraction(&length, bounds_width, rem_size));
515 let mut widths = self
516 .widths
517 .map(|length| Self::get_fraction(&length, bounds_width, rem_size));
518
519 let diff = initial_sizes[double_click_position] - widths[double_click_position];
520
521 if diff > 0.0 {
522 let diff_remaining = self.propagate_resize_diff_right(
523 diff,
524 double_click_position,
525 &mut widths,
526 resize_behavior,
527 );
528
529 if diff_remaining > 0.0 && double_click_position > 0 {
530 self.propagate_resize_diff_left(
531 -diff_remaining,
532 double_click_position - 1,
533 &mut widths,
534 resize_behavior,
535 );
536 }
537 } else if double_click_position > 0 {
538 let diff_remaining = self.propagate_resize_diff_left(
539 diff,
540 double_click_position,
541 &mut widths,
542 resize_behavior,
543 );
544
545 if diff_remaining < 0.0 {
546 self.propagate_resize_diff_right(
547 -diff_remaining,
548 double_click_position,
549 &mut widths,
550 resize_behavior,
551 );
552 }
553 }
554 self.widths = widths.map(DefiniteLength::Fraction);
555 }
556
557 fn on_drag_move(
558 &mut self,
559 drag_event: &DragMoveEvent<DraggedColumn>,
560 resize_behavior: &[ResizeBehavior; COLS],
561 window: &mut Window,
562 cx: &mut Context<Self>,
563 ) {
564 let drag_position = drag_event.event.position;
565 let bounds = drag_event.bounds;
566
567 let mut col_position = 0.0;
568 let rem_size = window.rem_size();
569 let bounds_width = bounds.right() - bounds.left();
570 let col_idx = drag_event.drag(cx).0;
571
572 let mut widths = self
573 .widths
574 .map(|length| Self::get_fraction(&length, bounds_width, rem_size));
575
576 for length in widths[0..=col_idx].iter() {
577 col_position += length;
578 }
579
580 let mut total_length_ratio = col_position;
581 for length in widths[col_idx + 1..].iter() {
582 total_length_ratio += length;
583 }
584
585 let drag_fraction = (drag_position.x - bounds.left()) / bounds_width;
586 let drag_fraction = drag_fraction * total_length_ratio;
587 let diff = drag_fraction - col_position;
588
589 let is_dragging_right = diff > 0.0;
590
591 if is_dragging_right {
592 self.propagate_resize_diff_right(diff, col_idx, &mut widths, resize_behavior);
593 } else {
594 // Resize behavior should be improved in the future by also seeking to the right column when there's not enough space
595 self.propagate_resize_diff_left(diff, col_idx, &mut widths, resize_behavior);
596 }
597 self.widths = widths.map(DefiniteLength::Fraction);
598 }
599
600 fn propagate_resize_diff_right(
601 &self,
602 diff: f32,
603 col_idx: usize,
604 widths: &mut [f32; COLS],
605 resize_behavior: &[ResizeBehavior; COLS],
606 ) -> f32 {
607 let mut diff_remaining = diff;
608 let mut curr_column = col_idx + 1;
609
610 while diff_remaining > 0.0 && curr_column < COLS {
611 let Some(min_size) = resize_behavior[curr_column - 1].min_size() else {
612 curr_column += 1;
613 continue;
614 };
615
616 let mut curr_width = widths[curr_column] - diff_remaining;
617
618 diff_remaining = 0.0;
619 if min_size > curr_width {
620 diff_remaining += min_size - curr_width;
621 curr_width = min_size;
622 }
623 widths[curr_column] = curr_width;
624 curr_column += 1;
625 }
626
627 widths[col_idx] = widths[col_idx] + (diff - diff_remaining);
628 return diff_remaining;
629 }
630
631 fn propagate_resize_diff_left(
632 &mut self,
633 diff: f32,
634 mut curr_column: usize,
635 widths: &mut [f32; COLS],
636 resize_behavior: &[ResizeBehavior; COLS],
637 ) -> f32 {
638 let mut diff_remaining = diff;
639 let col_idx = curr_column;
640 while diff_remaining < 0.0 {
641 let Some(min_size) = resize_behavior[curr_column].min_size() else {
642 if curr_column == 0 {
643 break;
644 }
645 curr_column -= 1;
646 continue;
647 };
648
649 let mut curr_width = widths[curr_column] + diff_remaining;
650
651 diff_remaining = 0.0;
652 if curr_width < min_size {
653 diff_remaining = curr_width - min_size;
654 curr_width = min_size
655 }
656
657 widths[curr_column] = curr_width;
658 if curr_column == 0 {
659 break;
660 }
661 curr_column -= 1;
662 }
663 widths[col_idx + 1] = widths[col_idx + 1] - (diff - diff_remaining);
664
665 return diff_remaining;
666 }
667}
668
669pub struct TableWidths<const COLS: usize> {
670 initial: [DefiniteLength; COLS],
671 current: Option<Entity<ColumnWidths<COLS>>>,
672 resizable: [ResizeBehavior; COLS],
673}
674
675impl<const COLS: usize> TableWidths<COLS> {
676 pub fn new(widths: [impl Into<DefiniteLength>; COLS]) -> Self {
677 let widths = widths.map(Into::into);
678
679 TableWidths {
680 initial: widths,
681 current: None,
682 resizable: [ResizeBehavior::None; COLS],
683 }
684 }
685
686 fn lengths(&self, cx: &App) -> [Length; COLS] {
687 self.current
688 .as_ref()
689 .map(|entity| entity.read(cx).widths.map(Length::Definite))
690 .unwrap_or(self.initial.map(Length::Definite))
691 }
692}
693
694/// A table component
695#[derive(RegisterComponent, IntoElement)]
696pub struct Table<const COLS: usize = 3> {
697 striped: bool,
698 width: Option<Length>,
699 headers: Option<[AnyElement; COLS]>,
700 rows: TableContents<COLS>,
701 interaction_state: Option<WeakEntity<TableInteractionState>>,
702 col_widths: Option<TableWidths<COLS>>,
703 map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
704 empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
705}
706
707impl<const COLS: usize> Table<COLS> {
708 /// number of headers provided.
709 pub fn new() -> Self {
710 Self {
711 striped: false,
712 width: None,
713 headers: None,
714 rows: TableContents::Vec(Vec::new()),
715 interaction_state: None,
716 map_row: None,
717 empty_table_callback: None,
718 col_widths: None,
719 }
720 }
721
722 /// Enables uniform list rendering.
723 /// The provided function will be passed directly to the `uniform_list` element.
724 /// Therefore, if this method is called, any calls to [`Table::row`] before or after
725 /// this method is called will be ignored.
726 pub fn uniform_list(
727 mut self,
728 id: impl Into<ElementId>,
729 row_count: usize,
730 render_item_fn: impl Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>
731 + 'static,
732 ) -> Self {
733 self.rows = TableContents::UniformList(UniformListData {
734 element_id: id.into(),
735 row_count: row_count,
736 render_item_fn: Box::new(render_item_fn),
737 });
738 self
739 }
740
741 /// Enables row striping.
742 pub fn striped(mut self) -> Self {
743 self.striped = true;
744 self
745 }
746
747 /// Sets the width of the table.
748 /// Will enable horizontal scrolling if [`Self::interactable`] is also called.
749 pub fn width(mut self, width: impl Into<Length>) -> Self {
750 self.width = Some(width.into());
751 self
752 }
753
754 /// Enables interaction (primarily scrolling) with the table.
755 ///
756 /// Vertical scrolling will be enabled by default if the table is taller than its container.
757 ///
758 /// Horizontal scrolling will only be enabled if [`Self::width`] is also called, otherwise
759 /// the list will always shrink the table columns to fit their contents I.e. If [`Self::uniform_list`]
760 /// is used without a width and with [`Self::interactable`], the [`ListHorizontalSizingBehavior`] will
761 /// be set to [`ListHorizontalSizingBehavior::FitList`].
762 pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
763 self.interaction_state = Some(interaction_state.downgrade());
764 self
765 }
766
767 pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self {
768 self.headers = Some(headers.map(IntoElement::into_any_element));
769 self
770 }
771
772 pub fn row(mut self, items: [impl IntoElement; COLS]) -> Self {
773 if let Some(rows) = self.rows.rows_mut() {
774 rows.push(items.map(IntoElement::into_any_element));
775 }
776 self
777 }
778
779 pub fn column_widths(mut self, widths: [impl Into<DefiniteLength>; COLS]) -> Self {
780 if self.col_widths.is_none() {
781 self.col_widths = Some(TableWidths::new(widths));
782 }
783 self
784 }
785
786 pub fn resizable_columns(
787 mut self,
788 resizable: [ResizeBehavior; COLS],
789 column_widths: &Entity<ColumnWidths<COLS>>,
790 cx: &mut App,
791 ) -> Self {
792 if let Some(table_widths) = self.col_widths.as_mut() {
793 table_widths.resizable = resizable;
794 let column_widths = table_widths
795 .current
796 .get_or_insert_with(|| column_widths.clone());
797
798 column_widths.update(cx, |widths, _| {
799 if !widths.initialized {
800 widths.initialized = true;
801 widths.widths = table_widths.initial;
802 }
803 })
804 }
805 self
806 }
807
808 pub fn map_row(
809 mut self,
810 callback: impl Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement + 'static,
811 ) -> Self {
812 self.map_row = Some(Rc::new(callback));
813 self
814 }
815
816 /// Provide a callback that is invoked when the table is rendered without any rows
817 pub fn empty_table_callback(
818 mut self,
819 callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
820 ) -> Self {
821 self.empty_table_callback = Some(Rc::new(callback));
822 self
823 }
824}
825
826fn base_cell_style(width: Option<Length>) -> Div {
827 div()
828 .px_1p5()
829 .when_some(width, |this, width| this.w(width))
830 .when(width.is_none(), |this| this.flex_1())
831 .justify_start()
832 .whitespace_nowrap()
833 .text_ellipsis()
834 .overflow_hidden()
835}
836
837fn base_cell_style_text(width: Option<Length>, cx: &App) -> Div {
838 base_cell_style(width).text_ui(cx)
839}
840
841pub fn render_row<const COLS: usize>(
842 row_index: usize,
843 items: [impl IntoElement; COLS],
844 table_context: TableRenderContext<COLS>,
845 window: &mut Window,
846 cx: &mut App,
847) -> AnyElement {
848 let is_striped = table_context.striped;
849 let is_last = row_index == table_context.total_row_count - 1;
850 let bg = if row_index % 2 == 1 && is_striped {
851 Some(cx.theme().colors().text.opacity(0.05))
852 } else {
853 None
854 };
855 let column_widths = table_context
856 .column_widths
857 .map_or([None; COLS], |widths| widths.map(Some));
858
859 let mut row = h_flex()
860 .h_full()
861 .id(("table_row", row_index))
862 .w_full()
863 .justify_between()
864 .when_some(bg, |row, bg| row.bg(bg))
865 .when(!is_striped, |row| {
866 row.border_b_1()
867 .border_color(transparent_black())
868 .when(!is_last, |row| row.border_color(cx.theme().colors().border))
869 });
870
871 row = row.children(
872 items
873 .map(IntoElement::into_any_element)
874 .into_iter()
875 .zip(column_widths)
876 .map(|(cell, width)| base_cell_style_text(width, cx).px_1p5().py_1().child(cell)),
877 );
878
879 let row = if let Some(map_row) = table_context.map_row {
880 map_row((row_index, row), window, cx)
881 } else {
882 row.into_any_element()
883 };
884
885 div().h_full().w_full().child(row).into_any_element()
886}
887
888pub fn render_header<const COLS: usize>(
889 headers: [impl IntoElement; COLS],
890 table_context: TableRenderContext<COLS>,
891 cx: &mut App,
892) -> impl IntoElement {
893 let column_widths = table_context
894 .column_widths
895 .map_or([None; COLS], |widths| widths.map(Some));
896 div()
897 .flex()
898 .flex_row()
899 .items_center()
900 .justify_between()
901 .w_full()
902 .p_2()
903 .border_b_1()
904 .border_color(cx.theme().colors().border)
905 .children(
906 headers
907 .into_iter()
908 .zip(column_widths)
909 .map(|(h, width)| base_cell_style_text(width, cx).child(h)),
910 )
911}
912
913#[derive(Clone)]
914pub struct TableRenderContext<const COLS: usize> {
915 pub striped: bool,
916 pub total_row_count: usize,
917 pub column_widths: Option<[Length; COLS]>,
918 pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
919}
920
921impl<const COLS: usize> TableRenderContext<COLS> {
922 fn new(table: &Table<COLS>, cx: &App) -> Self {
923 Self {
924 striped: table.striped,
925 total_row_count: table.rows.len(),
926 column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)),
927 map_row: table.map_row.clone(),
928 }
929 }
930}
931
932impl<const COLS: usize> RenderOnce for Table<COLS> {
933 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
934 let table_context = TableRenderContext::new(&self, cx);
935 let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
936 let current_widths = self
937 .col_widths
938 .as_ref()
939 .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable)))
940 .map(|(curr, resize_behavior)| (curr.downgrade(), resize_behavior));
941
942 let scroll_track_size = px(16.);
943 let h_scroll_offset = if interaction_state
944 .as_ref()
945 .is_some_and(|state| state.read(cx).vertical_scrollbar.show_scrollbar)
946 {
947 // magic number
948 px(3.)
949 } else {
950 px(0.)
951 };
952
953 let width = self.width;
954 let no_rows_rendered = self.rows.is_empty();
955
956 let table = div()
957 .when_some(width, |this, width| this.w(width))
958 .h_full()
959 .v_flex()
960 .when_some(self.headers.take(), |this, headers| {
961 this.child(render_header(headers, table_context.clone(), cx))
962 })
963 .when_some(current_widths, {
964 |this, (widths, resize_behavior)| {
965 this.on_drag_move::<DraggedColumn>({
966 let widths = widths.clone();
967 move |e, window, cx| {
968 widths
969 .update(cx, |widths, cx| {
970 widths.on_drag_move(e, &resize_behavior, window, cx);
971 })
972 .ok();
973 }
974 })
975 .on_children_prepainted(move |bounds, _, cx| {
976 widths
977 .update(cx, |widths, _| {
978 // This works because all children x axis bounds are the same
979 widths.cached_bounds_width = bounds[0].right() - bounds[0].left();
980 })
981 .ok();
982 })
983 }
984 })
985 .on_drop::<DraggedColumn>(|_, _, _| {
986 // Finish the resize operation
987 })
988 .child(
989 div()
990 .flex_grow()
991 .w_full()
992 .relative()
993 .overflow_hidden()
994 .map(|parent| match self.rows {
995 TableContents::Vec(items) => {
996 parent.children(items.into_iter().enumerate().map(|(index, row)| {
997 render_row(index, row, table_context.clone(), window, cx)
998 }))
999 }
1000 TableContents::UniformList(uniform_list_data) => parent.child(
1001 uniform_list(
1002 uniform_list_data.element_id,
1003 uniform_list_data.row_count,
1004 {
1005 let render_item_fn = uniform_list_data.render_item_fn;
1006 move |range: Range<usize>, window, cx| {
1007 let elements = render_item_fn(range.clone(), window, cx);
1008 elements
1009 .into_iter()
1010 .zip(range)
1011 .map(|(row, row_index)| {
1012 render_row(
1013 row_index,
1014 row,
1015 table_context.clone(),
1016 window,
1017 cx,
1018 )
1019 })
1020 .collect()
1021 }
1022 },
1023 )
1024 .size_full()
1025 .flex_grow()
1026 .with_sizing_behavior(ListSizingBehavior::Auto)
1027 .with_horizontal_sizing_behavior(if width.is_some() {
1028 ListHorizontalSizingBehavior::Unconstrained
1029 } else {
1030 ListHorizontalSizingBehavior::FitList
1031 })
1032 .when_some(
1033 interaction_state.as_ref(),
1034 |this, state| {
1035 this.track_scroll(
1036 state.read_with(cx, |s, _| s.scroll_handle.clone()),
1037 )
1038 },
1039 ),
1040 ),
1041 })
1042 .when_some(
1043 self.col_widths.as_ref().zip(interaction_state.as_ref()),
1044 |parent, (table_widths, state)| {
1045 parent.child(state.update(cx, |state, cx| {
1046 let resizable_columns = table_widths.resizable;
1047 let column_widths = table_widths.lengths(cx);
1048 let columns = table_widths.current.clone();
1049 let initial_sizes = table_widths.initial;
1050 state.render_resize_handles(
1051 &column_widths,
1052 &resizable_columns,
1053 initial_sizes,
1054 columns,
1055 window,
1056 cx,
1057 )
1058 }))
1059 },
1060 )
1061 .when_some(interaction_state.as_ref(), |this, interaction_state| {
1062 this.map(|this| {
1063 TableInteractionState::render_vertical_scrollbar_track(
1064 interaction_state,
1065 this,
1066 scroll_track_size,
1067 cx,
1068 )
1069 })
1070 .map(|this| {
1071 TableInteractionState::render_vertical_scrollbar(
1072 interaction_state,
1073 this,
1074 cx,
1075 )
1076 })
1077 }),
1078 )
1079 .when_some(
1080 no_rows_rendered
1081 .then_some(self.empty_table_callback)
1082 .flatten(),
1083 |this, callback| {
1084 this.child(
1085 h_flex()
1086 .size_full()
1087 .p_3()
1088 .items_start()
1089 .justify_center()
1090 .child(callback(window, cx)),
1091 )
1092 },
1093 )
1094 .when_some(
1095 width.and(interaction_state.as_ref()),
1096 |this, interaction_state| {
1097 this.map(|this| {
1098 TableInteractionState::render_horizontal_scrollbar_track(
1099 interaction_state,
1100 this,
1101 scroll_track_size,
1102 cx,
1103 )
1104 })
1105 .map(|this| {
1106 TableInteractionState::render_horizontal_scrollbar(
1107 interaction_state,
1108 this,
1109 h_scroll_offset,
1110 cx,
1111 )
1112 })
1113 },
1114 );
1115
1116 if let Some(interaction_state) = interaction_state.as_ref() {
1117 table
1118 .track_focus(&interaction_state.read(cx).focus_handle)
1119 .id(("table", interaction_state.entity_id()))
1120 .on_hover({
1121 let interaction_state = interaction_state.downgrade();
1122 move |hovered, window, cx| {
1123 interaction_state
1124 .update(cx, |interaction_state, cx| {
1125 if *hovered {
1126 interaction_state.horizontal_scrollbar.show(cx);
1127 interaction_state.vertical_scrollbar.show(cx);
1128 cx.notify();
1129 } else if !interaction_state
1130 .focus_handle
1131 .contains_focused(window, cx)
1132 {
1133 interaction_state.hide_scrollbars(window, cx);
1134 }
1135 })
1136 .ok();
1137 }
1138 })
1139 .into_any_element()
1140 } else {
1141 table.into_any_element()
1142 }
1143 }
1144}
1145
1146// computed state related to how to render scrollbars
1147// one per axis
1148// on render we just read this off the keymap editor
1149// we update it when
1150// - settings change
1151// - on focus in, on focus out, on hover, etc.
1152#[derive(Debug)]
1153pub struct ScrollbarProperties {
1154 axis: Axis,
1155 show_scrollbar: bool,
1156 show_track: bool,
1157 auto_hide: bool,
1158 hide_task: Option<Task<()>>,
1159 state: ScrollbarState,
1160}
1161
1162impl ScrollbarProperties {
1163 // Shows the scrollbar and cancels any pending hide task
1164 fn show(&mut self, cx: &mut Context<TableInteractionState>) {
1165 if !self.auto_hide {
1166 return;
1167 }
1168 self.show_scrollbar = true;
1169 self.hide_task.take();
1170 cx.notify();
1171 }
1172
1173 fn hide(&mut self, window: &mut Window, cx: &mut Context<TableInteractionState>) {
1174 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
1175
1176 if !self.auto_hide {
1177 return;
1178 }
1179
1180 let axis = self.axis;
1181 self.hide_task = Some(cx.spawn_in(window, async move |keymap_editor, cx| {
1182 cx.background_executor()
1183 .timer(SCROLLBAR_SHOW_INTERVAL)
1184 .await;
1185
1186 if let Some(keymap_editor) = keymap_editor.upgrade() {
1187 keymap_editor
1188 .update(cx, |keymap_editor, cx| {
1189 match axis {
1190 Axis::Vertical => {
1191 keymap_editor.vertical_scrollbar.show_scrollbar = false
1192 }
1193 Axis::Horizontal => {
1194 keymap_editor.horizontal_scrollbar.show_scrollbar = false
1195 }
1196 }
1197 cx.notify();
1198 })
1199 .ok();
1200 }
1201 }));
1202 }
1203}
1204
1205impl Component for Table<3> {
1206 fn scope() -> ComponentScope {
1207 ComponentScope::Layout
1208 }
1209
1210 fn description() -> Option<&'static str> {
1211 Some("A table component for displaying data in rows and columns with optional styling.")
1212 }
1213
1214 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
1215 Some(
1216 v_flex()
1217 .gap_6()
1218 .children(vec![
1219 example_group_with_title(
1220 "Basic Tables",
1221 vec![
1222 single_example(
1223 "Simple Table",
1224 Table::new()
1225 .width(px(400.))
1226 .header(["Name", "Age", "City"])
1227 .row(["Alice", "28", "New York"])
1228 .row(["Bob", "32", "San Francisco"])
1229 .row(["Charlie", "25", "London"])
1230 .into_any_element(),
1231 ),
1232 single_example(
1233 "Two Column Table",
1234 Table::new()
1235 .header(["Category", "Value"])
1236 .width(px(300.))
1237 .row(["Revenue", "$100,000"])
1238 .row(["Expenses", "$75,000"])
1239 .row(["Profit", "$25,000"])
1240 .into_any_element(),
1241 ),
1242 ],
1243 ),
1244 example_group_with_title(
1245 "Styled Tables",
1246 vec![
1247 single_example(
1248 "Default",
1249 Table::new()
1250 .width(px(400.))
1251 .header(["Product", "Price", "Stock"])
1252 .row(["Laptop", "$999", "In Stock"])
1253 .row(["Phone", "$599", "Low Stock"])
1254 .row(["Tablet", "$399", "Out of Stock"])
1255 .into_any_element(),
1256 ),
1257 single_example(
1258 "Striped",
1259 Table::new()
1260 .width(px(400.))
1261 .striped()
1262 .header(["Product", "Price", "Stock"])
1263 .row(["Laptop", "$999", "In Stock"])
1264 .row(["Phone", "$599", "Low Stock"])
1265 .row(["Tablet", "$399", "Out of Stock"])
1266 .row(["Headphones", "$199", "In Stock"])
1267 .into_any_element(),
1268 ),
1269 ],
1270 ),
1271 example_group_with_title(
1272 "Mixed Content Table",
1273 vec![single_example(
1274 "Table with Elements",
1275 Table::new()
1276 .width(px(840.))
1277 .header(["Status", "Name", "Priority", "Deadline", "Action"])
1278 .row([
1279 Indicator::dot().color(Color::Success).into_any_element(),
1280 "Project A".into_any_element(),
1281 "High".into_any_element(),
1282 "2023-12-31".into_any_element(),
1283 Button::new("view_a", "View")
1284 .style(ButtonStyle::Filled)
1285 .full_width()
1286 .into_any_element(),
1287 ])
1288 .row([
1289 Indicator::dot().color(Color::Warning).into_any_element(),
1290 "Project B".into_any_element(),
1291 "Medium".into_any_element(),
1292 "2024-03-15".into_any_element(),
1293 Button::new("view_b", "View")
1294 .style(ButtonStyle::Filled)
1295 .full_width()
1296 .into_any_element(),
1297 ])
1298 .row([
1299 Indicator::dot().color(Color::Error).into_any_element(),
1300 "Project C".into_any_element(),
1301 "Low".into_any_element(),
1302 "2024-06-30".into_any_element(),
1303 Button::new("view_c", "View")
1304 .style(ButtonStyle::Filled)
1305 .full_width()
1306 .into_any_element(),
1307 ])
1308 .into_any_element(),
1309 )],
1310 ),
1311 ])
1312 .into_any_element(),
1313 )
1314 }
1315}