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