table.rs

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