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