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