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