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