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