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