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