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