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
436pub struct ColumnWidths<const COLS: usize> {
437 widths: [Pixels; COLS],
438}
439
440impl<const COLS: usize> ColumnWidths<COLS> {
441 pub fn new(_: &mut App) -> Self {
442 Self {
443 widths: [px(0.0); COLS],
444 }
445 }
446}
447
448/// A table component
449#[derive(RegisterComponent, IntoElement)]
450pub struct Table<const COLS: usize = 3> {
451 striped: bool,
452 width: Option<Length>,
453 headers: Option<[AnyElement; COLS]>,
454 rows: TableContents<COLS>,
455 interaction_state: Option<WeakEntity<TableInteractionState>>,
456 initial_widths: Option<[Length; COLS]>,
457 current_widths: Option<Entity<ColumnWidths<COLS>>>,
458 resizable_columns: Option<[bool; COLS]>,
459 map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
460 empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
461}
462
463impl<const COLS: usize> Table<COLS> {
464 /// number of headers provided.
465 pub fn new() -> Self {
466 Self {
467 striped: false,
468 width: None,
469 headers: None,
470 rows: TableContents::Vec(Vec::new()),
471 interaction_state: None,
472 initial_widths: None,
473 current_widths: None,
474 map_row: None,
475 empty_table_callback: None,
476 resizable_columns: None,
477 }
478 }
479
480 /// Enables uniform list rendering.
481 /// The provided function will be passed directly to the `uniform_list` element.
482 /// Therefore, if this method is called, any calls to [`Table::row`] before or after
483 /// this method is called will be ignored.
484 pub fn uniform_list(
485 mut self,
486 id: impl Into<ElementId>,
487 row_count: usize,
488 render_item_fn: impl Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>
489 + 'static,
490 ) -> Self {
491 self.rows = TableContents::UniformList(UniformListData {
492 element_id: id.into(),
493 row_count: row_count,
494 render_item_fn: Box::new(render_item_fn),
495 });
496 self
497 }
498
499 /// Enables row striping.
500 pub fn striped(mut self) -> Self {
501 self.striped = true;
502 self
503 }
504
505 /// Sets the width of the table.
506 /// Will enable horizontal scrolling if [`Self::interactable`] is also called.
507 pub fn width(mut self, width: impl Into<Length>) -> Self {
508 self.width = Some(width.into());
509 self
510 }
511
512 /// Enables interaction (primarily scrolling) with the table.
513 ///
514 /// Vertical scrolling will be enabled by default if the table is taller than its container.
515 ///
516 /// Horizontal scrolling will only be enabled if [`Self::width`] is also called, otherwise
517 /// the list will always shrink the table columns to fit their contents I.e. If [`Self::uniform_list`]
518 /// is used without a width and with [`Self::interactable`], the [`ListHorizontalSizingBehavior`] will
519 /// be set to [`ListHorizontalSizingBehavior::FitList`].
520 pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
521 self.interaction_state = Some(interaction_state.downgrade());
522 self
523 }
524
525 pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self {
526 self.headers = Some(headers.map(IntoElement::into_any_element));
527 self
528 }
529
530 pub fn row(mut self, items: [impl IntoElement; COLS]) -> Self {
531 if let Some(rows) = self.rows.rows_mut() {
532 rows.push(items.map(IntoElement::into_any_element));
533 }
534 self
535 }
536
537 pub fn column_widths(mut self, widths: [impl Into<Length>; COLS]) -> Self {
538 self.initial_widths = Some(widths.map(Into::into));
539 self
540 }
541
542 pub fn resizable_columns(
543 mut self,
544 resizable: [impl Into<bool>; COLS],
545 current_widths: Entity<ColumnWidths<COLS>>,
546 ) -> Self {
547 self.resizable_columns = Some(resizable.map(Into::into));
548 self.current_widths = Some(current_widths);
549 self
550 }
551
552 pub fn map_row(
553 mut self,
554 callback: impl Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement + 'static,
555 ) -> Self {
556 self.map_row = Some(Rc::new(callback));
557 self
558 }
559
560 /// Provide a callback that is invoked when the table is rendered without any rows
561 pub fn empty_table_callback(
562 mut self,
563 callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
564 ) -> Self {
565 self.empty_table_callback = Some(Rc::new(callback));
566 self
567 }
568}
569
570fn base_cell_style(width: Option<Length>) -> Div {
571 div()
572 .px_1p5()
573 .when_some(width, |this, width| this.w(width))
574 .when(width.is_none(), |this| this.flex_1())
575 .justify_start()
576 .whitespace_nowrap()
577 .text_ellipsis()
578 .overflow_hidden()
579}
580
581fn base_cell_style_text(width: Option<Length>, cx: &App) -> Div {
582 base_cell_style(width).text_ui(cx)
583}
584
585pub fn render_row<const COLS: usize>(
586 row_index: usize,
587 items: [impl IntoElement; COLS],
588 table_context: TableRenderContext<COLS>,
589 window: &mut Window,
590 cx: &mut App,
591) -> AnyElement {
592 let is_striped = table_context.striped;
593 let is_last = row_index == table_context.total_row_count - 1;
594 let bg = if row_index % 2 == 1 && is_striped {
595 Some(cx.theme().colors().text.opacity(0.05))
596 } else {
597 None
598 };
599 let column_widths = table_context
600 .column_widths
601 .map_or([None; COLS], |widths| widths.map(Some));
602
603 let mut row = h_flex()
604 .h_full()
605 .id(("table_row", row_index))
606 .w_full()
607 .justify_between()
608 .when_some(bg, |row, bg| row.bg(bg))
609 .when(!is_striped, |row| {
610 row.border_b_1()
611 .border_color(transparent_black())
612 .when(!is_last, |row| row.border_color(cx.theme().colors().border))
613 });
614
615 row = row.children(
616 items
617 .map(IntoElement::into_any_element)
618 .into_iter()
619 .zip(column_widths)
620 .map(|(cell, width)| base_cell_style_text(width, cx).px_1p5().py_1().child(cell)),
621 );
622
623 let row = if let Some(map_row) = table_context.map_row {
624 map_row((row_index, row), window, cx)
625 } else {
626 row.into_any_element()
627 };
628
629 div().h_full().w_full().child(row).into_any_element()
630}
631
632pub fn render_header<const COLS: usize>(
633 headers: [impl IntoElement; COLS],
634 table_context: TableRenderContext<COLS>,
635 cx: &mut App,
636) -> impl IntoElement {
637 let column_widths = table_context
638 .column_widths
639 .map_or([None; COLS], |widths| widths.map(Some));
640 div()
641 .flex()
642 .flex_row()
643 .items_center()
644 .justify_between()
645 .w_full()
646 .p_2()
647 .border_b_1()
648 .border_color(cx.theme().colors().border)
649 .children(
650 headers
651 .into_iter()
652 .zip(column_widths)
653 .map(|(h, width)| base_cell_style_text(width, cx).child(h)),
654 )
655}
656
657#[derive(Clone)]
658pub struct TableRenderContext<const COLS: usize> {
659 pub striped: bool,
660 pub total_row_count: usize,
661 pub column_widths: Option<[Length; COLS]>,
662 pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
663}
664
665impl<const COLS: usize> TableRenderContext<COLS> {
666 fn new(table: &Table<COLS>) -> Self {
667 Self {
668 striped: table.striped,
669 total_row_count: table.rows.len(),
670 column_widths: table.initial_widths,
671 map_row: table.map_row.clone(),
672 }
673 }
674}
675
676impl<const COLS: usize> RenderOnce for Table<COLS> {
677 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
678 let table_context = TableRenderContext::new(&self);
679 let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
680
681 let scroll_track_size = px(16.);
682 let h_scroll_offset = if interaction_state
683 .as_ref()
684 .is_some_and(|state| state.read(cx).vertical_scrollbar.show_scrollbar)
685 {
686 // magic number
687 px(3.)
688 } else {
689 px(0.)
690 };
691
692 let width = self.width;
693 let no_rows_rendered = self.rows.is_empty();
694
695 let table = div()
696 .when_some(width, |this, width| this.w(width))
697 .h_full()
698 .v_flex()
699 .when_some(self.headers.take(), |this, headers| {
700 this.child(render_header(headers, table_context.clone(), cx))
701 })
702 .child(
703 div()
704 .flex_grow()
705 .w_full()
706 .relative()
707 .overflow_hidden()
708 .map(|parent| match self.rows {
709 TableContents::Vec(items) => {
710 parent.children(items.into_iter().enumerate().map(|(index, row)| {
711 render_row(index, row, table_context.clone(), window, cx)
712 }))
713 }
714 TableContents::UniformList(uniform_list_data) => parent.child(
715 uniform_list(
716 uniform_list_data.element_id,
717 uniform_list_data.row_count,
718 {
719 let render_item_fn = uniform_list_data.render_item_fn;
720 move |range: Range<usize>, window, cx| {
721 let elements = render_item_fn(range.clone(), window, cx);
722 elements
723 .into_iter()
724 .zip(range)
725 .map(|(row, row_index)| {
726 render_row(
727 row_index,
728 row,
729 table_context.clone(),
730 window,
731 cx,
732 )
733 })
734 .collect()
735 }
736 },
737 )
738 .size_full()
739 .flex_grow()
740 .with_sizing_behavior(ListSizingBehavior::Auto)
741 .with_horizontal_sizing_behavior(if width.is_some() {
742 ListHorizontalSizingBehavior::Unconstrained
743 } else {
744 ListHorizontalSizingBehavior::FitList
745 })
746 .when_some(
747 interaction_state.as_ref(),
748 |this, state| {
749 this.track_scroll(
750 state.read_with(cx, |s, _| s.scroll_handle.clone()),
751 )
752 },
753 ),
754 ),
755 })
756 .when_some(
757 self.initial_widths
758 .as_ref()
759 .zip(interaction_state.as_ref())
760 .zip(self.resizable_columns.as_ref()),
761 |parent, ((column_widths, state), resizable_columns)| {
762 parent.child(state.update(cx, |state, cx| {
763 state.render_resize_handles(
764 column_widths,
765 resizable_columns,
766 window,
767 cx,
768 )
769 }))
770 },
771 )
772 .when_some(interaction_state.as_ref(), |this, interaction_state| {
773 this.map(|this| {
774 TableInteractionState::render_vertical_scrollbar_track(
775 interaction_state,
776 this,
777 scroll_track_size,
778 cx,
779 )
780 })
781 .map(|this| {
782 TableInteractionState::render_vertical_scrollbar(
783 interaction_state,
784 this,
785 cx,
786 )
787 })
788 }),
789 )
790 .when_some(
791 no_rows_rendered
792 .then_some(self.empty_table_callback)
793 .flatten(),
794 |this, callback| {
795 this.child(
796 h_flex()
797 .size_full()
798 .p_3()
799 .items_start()
800 .justify_center()
801 .child(callback(window, cx)),
802 )
803 },
804 )
805 .when_some(
806 width.and(interaction_state.as_ref()),
807 |this, interaction_state| {
808 this.map(|this| {
809 TableInteractionState::render_horizontal_scrollbar_track(
810 interaction_state,
811 this,
812 scroll_track_size,
813 cx,
814 )
815 })
816 .map(|this| {
817 TableInteractionState::render_horizontal_scrollbar(
818 interaction_state,
819 this,
820 h_scroll_offset,
821 cx,
822 )
823 })
824 },
825 );
826
827 if let Some(interaction_state) = interaction_state.as_ref() {
828 table
829 .track_focus(&interaction_state.read(cx).focus_handle)
830 .id(("table", interaction_state.entity_id()))
831 .on_hover({
832 let interaction_state = interaction_state.downgrade();
833 move |hovered, window, cx| {
834 interaction_state
835 .update(cx, |interaction_state, cx| {
836 if *hovered {
837 interaction_state.horizontal_scrollbar.show(cx);
838 interaction_state.vertical_scrollbar.show(cx);
839 cx.notify();
840 } else if !interaction_state
841 .focus_handle
842 .contains_focused(window, cx)
843 {
844 interaction_state.hide_scrollbars(window, cx);
845 }
846 })
847 .ok();
848 }
849 })
850 .into_any_element()
851 } else {
852 table.into_any_element()
853 }
854 }
855}
856
857// computed state related to how to render scrollbars
858// one per axis
859// on render we just read this off the keymap editor
860// we update it when
861// - settings change
862// - on focus in, on focus out, on hover, etc.
863#[derive(Debug)]
864pub struct ScrollbarProperties {
865 axis: Axis,
866 show_scrollbar: bool,
867 show_track: bool,
868 auto_hide: bool,
869 hide_task: Option<Task<()>>,
870 state: ScrollbarState,
871}
872
873impl ScrollbarProperties {
874 // Shows the scrollbar and cancels any pending hide task
875 fn show(&mut self, cx: &mut Context<TableInteractionState>) {
876 if !self.auto_hide {
877 return;
878 }
879 self.show_scrollbar = true;
880 self.hide_task.take();
881 cx.notify();
882 }
883
884 fn hide(&mut self, window: &mut Window, cx: &mut Context<TableInteractionState>) {
885 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
886
887 if !self.auto_hide {
888 return;
889 }
890
891 let axis = self.axis;
892 self.hide_task = Some(cx.spawn_in(window, async move |keymap_editor, cx| {
893 cx.background_executor()
894 .timer(SCROLLBAR_SHOW_INTERVAL)
895 .await;
896
897 if let Some(keymap_editor) = keymap_editor.upgrade() {
898 keymap_editor
899 .update(cx, |keymap_editor, cx| {
900 match axis {
901 Axis::Vertical => {
902 keymap_editor.vertical_scrollbar.show_scrollbar = false
903 }
904 Axis::Horizontal => {
905 keymap_editor.horizontal_scrollbar.show_scrollbar = false
906 }
907 }
908 cx.notify();
909 })
910 .ok();
911 }
912 }));
913 }
914}
915
916impl Component for Table<3> {
917 fn scope() -> ComponentScope {
918 ComponentScope::Layout
919 }
920
921 fn description() -> Option<&'static str> {
922 Some("A table component for displaying data in rows and columns with optional styling.")
923 }
924
925 fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
926 Some(
927 v_flex()
928 .gap_6()
929 .children(vec![
930 example_group_with_title(
931 "Basic Tables",
932 vec![
933 single_example(
934 "Simple Table",
935 Table::new()
936 .width(px(400.))
937 .header(["Name", "Age", "City"])
938 .row(["Alice", "28", "New York"])
939 .row(["Bob", "32", "San Francisco"])
940 .row(["Charlie", "25", "London"])
941 .into_any_element(),
942 ),
943 single_example(
944 "Two Column Table",
945 Table::new()
946 .header(["Category", "Value"])
947 .width(px(300.))
948 .row(["Revenue", "$100,000"])
949 .row(["Expenses", "$75,000"])
950 .row(["Profit", "$25,000"])
951 .into_any_element(),
952 ),
953 ],
954 ),
955 example_group_with_title(
956 "Styled Tables",
957 vec![
958 single_example(
959 "Default",
960 Table::new()
961 .width(px(400.))
962 .header(["Product", "Price", "Stock"])
963 .row(["Laptop", "$999", "In Stock"])
964 .row(["Phone", "$599", "Low Stock"])
965 .row(["Tablet", "$399", "Out of Stock"])
966 .into_any_element(),
967 ),
968 single_example(
969 "Striped",
970 Table::new()
971 .width(px(400.))
972 .striped()
973 .header(["Product", "Price", "Stock"])
974 .row(["Laptop", "$999", "In Stock"])
975 .row(["Phone", "$599", "Low Stock"])
976 .row(["Tablet", "$399", "Out of Stock"])
977 .row(["Headphones", "$199", "In Stock"])
978 .into_any_element(),
979 ),
980 ],
981 ),
982 example_group_with_title(
983 "Mixed Content Table",
984 vec![single_example(
985 "Table with Elements",
986 Table::new()
987 .width(px(840.))
988 .header(["Status", "Name", "Priority", "Deadline", "Action"])
989 .row([
990 Indicator::dot().color(Color::Success).into_any_element(),
991 "Project A".into_any_element(),
992 "High".into_any_element(),
993 "2023-12-31".into_any_element(),
994 Button::new("view_a", "View")
995 .style(ButtonStyle::Filled)
996 .full_width()
997 .into_any_element(),
998 ])
999 .row([
1000 Indicator::dot().color(Color::Warning).into_any_element(),
1001 "Project B".into_any_element(),
1002 "Medium".into_any_element(),
1003 "2024-03-15".into_any_element(),
1004 Button::new("view_b", "View")
1005 .style(ButtonStyle::Filled)
1006 .full_width()
1007 .into_any_element(),
1008 ])
1009 .row([
1010 Indicator::dot().color(Color::Error).into_any_element(),
1011 "Project C".into_any_element(),
1012 "Low".into_any_element(),
1013 "2024-06-30".into_any_element(),
1014 Button::new("view_c", "View")
1015 .style(ButtonStyle::Filled)
1016 .full_width()
1017 .into_any_element(),
1018 ])
1019 .into_any_element(),
1020 )],
1021 ),
1022 ])
1023 .into_any_element(),
1024 )
1025 }
1026}