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