table.rs

   1use std::{ops::Range, rc::Rc, time::Duration};
   2
   3use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide};
   4use gpui::{
   5    AbsoluteLength, AppContext, Axis, Context, DefiniteLength, DragMoveEvent, Entity, FocusHandle,
   6    Length, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Point, Stateful, Task,
   7    UniformListScrollHandle, WeakEntity, 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: &[ResizeBehavior; 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 resizable_columns
 231                    .next()
 232                    .is_some_and(ResizeBehavior::is_resizable)
 233                {
 234                    let hovered = window.use_state(cx, |_window, _cx| false);
 235                    resize_divider = resize_divider.when(*hovered.read(cx), |div| {
 236                        div.bg(cx.theme().colors().border_focused)
 237                    });
 238                    resize_handle = resize_handle
 239                        .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered))
 240                        .cursor_col_resize()
 241                        .on_drag(DraggedColumn(column_ix), |ix, _offset, _window, cx| {
 242                            eprintln!("Start resizing column {:?}", ix);
 243                            cx.new(|_cx| gpui::Empty)
 244                        })
 245                }
 246
 247                column_ix += 1;
 248                resize_divider.child(resize_handle).into_any_element()
 249            })
 250        });
 251
 252        div()
 253            .id("resize-handles")
 254            .h_flex()
 255            .absolute()
 256            .w_full()
 257            .inset_0()
 258            .children(dividers)
 259            .into_any_element()
 260    }
 261
 262    fn render_vertical_scrollbar_track(
 263        this: &Entity<Self>,
 264        parent: Div,
 265        scroll_track_size: Pixels,
 266        cx: &mut App,
 267    ) -> Div {
 268        if !this.read(cx).vertical_scrollbar.show_track {
 269            return parent;
 270        }
 271        let child = v_flex()
 272            .h_full()
 273            .flex_none()
 274            .w(scroll_track_size)
 275            .bg(cx.theme().colors().background)
 276            .child(
 277                div()
 278                    .size_full()
 279                    .flex_1()
 280                    .border_l_1()
 281                    .border_color(cx.theme().colors().border),
 282            );
 283        parent.child(child)
 284    }
 285
 286    fn render_vertical_scrollbar(this: &Entity<Self>, parent: Div, cx: &mut App) -> Div {
 287        if !this.read(cx).vertical_scrollbar.show_scrollbar {
 288            return parent;
 289        }
 290        let child = div()
 291            .id(("table-vertical-scrollbar", this.entity_id()))
 292            .occlude()
 293            .flex_none()
 294            .h_full()
 295            .cursor_default()
 296            .absolute()
 297            .right_0()
 298            .top_0()
 299            .bottom_0()
 300            .w(px(12.))
 301            .on_mouse_move(Self::listener(this, |_, _, _, cx| {
 302                cx.notify();
 303                cx.stop_propagation()
 304            }))
 305            .on_hover(|_, _, cx| {
 306                cx.stop_propagation();
 307            })
 308            .on_mouse_up(
 309                MouseButton::Left,
 310                Self::listener(this, |this, _, window, cx| {
 311                    if !this.vertical_scrollbar.state.is_dragging()
 312                        && !this.focus_handle.contains_focused(window, cx)
 313                    {
 314                        this.vertical_scrollbar.hide(window, cx);
 315                        cx.notify();
 316                    }
 317
 318                    cx.stop_propagation();
 319                }),
 320            )
 321            .on_any_mouse_down(|_, _, cx| {
 322                cx.stop_propagation();
 323            })
 324            .on_scroll_wheel(Self::listener(&this, |_, _, _, cx| {
 325                cx.notify();
 326            }))
 327            .children(Scrollbar::vertical(
 328                this.read(cx).vertical_scrollbar.state.clone(),
 329            ));
 330        parent.child(child)
 331    }
 332
 333    /// Renders the horizontal scrollbar.
 334    ///
 335    /// The right offset is used to determine how far to the right the
 336    /// scrollbar should extend to, useful for ensuring it doesn't collide
 337    /// with the vertical scrollbar when visible.
 338    fn render_horizontal_scrollbar(
 339        this: &Entity<Self>,
 340        parent: Div,
 341        right_offset: Pixels,
 342        cx: &mut App,
 343    ) -> Div {
 344        if !this.read(cx).horizontal_scrollbar.show_scrollbar {
 345            return parent;
 346        }
 347        let child = div()
 348            .id(("table-horizontal-scrollbar", this.entity_id()))
 349            .occlude()
 350            .flex_none()
 351            .w_full()
 352            .cursor_default()
 353            .absolute()
 354            .bottom_neg_px()
 355            .left_0()
 356            .right_0()
 357            .pr(right_offset)
 358            .on_mouse_move(Self::listener(this, |_, _, _, cx| {
 359                cx.notify();
 360                cx.stop_propagation()
 361            }))
 362            .on_hover(|_, _, cx| {
 363                cx.stop_propagation();
 364            })
 365            .on_any_mouse_down(|_, _, cx| {
 366                cx.stop_propagation();
 367            })
 368            .on_mouse_up(
 369                MouseButton::Left,
 370                Self::listener(this, |this, _, window, cx| {
 371                    if !this.horizontal_scrollbar.state.is_dragging()
 372                        && !this.focus_handle.contains_focused(window, cx)
 373                    {
 374                        this.horizontal_scrollbar.hide(window, cx);
 375                        cx.notify();
 376                    }
 377
 378                    cx.stop_propagation();
 379                }),
 380            )
 381            .on_scroll_wheel(Self::listener(this, |_, _, _, cx| {
 382                cx.notify();
 383            }))
 384            .children(Scrollbar::horizontal(
 385                // percentage as f32..end_offset as f32,
 386                this.read(cx).horizontal_scrollbar.state.clone(),
 387            ));
 388        parent.child(child)
 389    }
 390
 391    fn render_horizontal_scrollbar_track(
 392        this: &Entity<Self>,
 393        parent: Div,
 394        scroll_track_size: Pixels,
 395        cx: &mut App,
 396    ) -> Div {
 397        if !this.read(cx).horizontal_scrollbar.show_track {
 398            return parent;
 399        }
 400        let child = h_flex()
 401            .w_full()
 402            .h(scroll_track_size)
 403            .flex_none()
 404            .relative()
 405            .child(
 406                div()
 407                    .w_full()
 408                    .flex_1()
 409                    // for some reason the horizontal scrollbar is 1px
 410                    // taller than the vertical scrollbar??
 411                    .h(scroll_track_size - px(1.))
 412                    .bg(cx.theme().colors().background)
 413                    .border_t_1()
 414                    .border_color(cx.theme().colors().border),
 415            )
 416            .when(this.read(cx).vertical_scrollbar.show_track, |parent| {
 417                parent
 418                    .child(
 419                        div()
 420                            .flex_none()
 421                            // -1px prevents a missing pixel between the two container borders
 422                            .w(scroll_track_size - px(1.))
 423                            .h_full(),
 424                    )
 425                    .child(
 426                        // HACK: Fill the missing 1px 🥲
 427                        div()
 428                            .absolute()
 429                            .right(scroll_track_size - px(1.))
 430                            .bottom(scroll_track_size - px(1.))
 431                            .size_px()
 432                            .bg(cx.theme().colors().border),
 433                    )
 434            });
 435
 436        parent.child(child)
 437    }
 438}
 439
 440#[derive(Debug, Copy, Clone, PartialEq)]
 441pub enum ResizeBehavior {
 442    None,
 443    Resizable,
 444    MinSize(f32),
 445}
 446
 447impl ResizeBehavior {
 448    pub fn is_resizable(&self) -> bool {
 449        *self != ResizeBehavior::None
 450    }
 451
 452    pub fn min_size(&self) -> Option<f32> {
 453        match self {
 454            ResizeBehavior::None => None,
 455            ResizeBehavior::Resizable => Some(0.05),
 456            ResizeBehavior::MinSize(min_size) => Some(*min_size),
 457        }
 458    }
 459}
 460
 461pub struct ColumnWidths<const COLS: usize> {
 462    widths: [DefiniteLength; COLS],
 463    initialized: bool,
 464}
 465
 466impl<const COLS: usize> ColumnWidths<COLS> {
 467    pub fn new(_: &mut App) -> Self {
 468        Self {
 469            widths: [DefiniteLength::default(); COLS],
 470            initialized: false,
 471        }
 472    }
 473
 474    fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 {
 475        match length {
 476            DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => *pixels / bounds_width,
 477            DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => {
 478                rems_width.to_pixels(rem_size) / bounds_width
 479            }
 480            DefiniteLength::Fraction(fraction) => *fraction,
 481        }
 482    }
 483
 484    fn on_drag_move(
 485        &mut self,
 486        drag_event: &DragMoveEvent<DraggedColumn>,
 487        resize_behavior: &[ResizeBehavior; COLS],
 488        window: &mut Window,
 489        cx: &mut Context<Self>,
 490    ) {
 491        // - [ ] Fix bugs in resize
 492        // - [ ] Create and respect a minimum size
 493        // - [ ] Cascade resize columns to next column if at minimum width
 494        // - [ ] Double click to reset column widths
 495        let drag_position = drag_event.event.position;
 496        let bounds = drag_event.bounds;
 497
 498        let mut col_position = 0.0;
 499        let rem_size = window.rem_size();
 500        let bounds_width = bounds.right() - bounds.left();
 501        let col_idx = drag_event.drag(cx).0;
 502
 503        for length in self.widths[0..=col_idx].iter() {
 504            col_position += Self::get_fraction(length, bounds_width, rem_size);
 505        }
 506
 507        let mut total_length_ratio = col_position;
 508        for length in self.widths[col_idx + 1..].iter() {
 509            total_length_ratio += Self::get_fraction(length, bounds_width, rem_size);
 510        }
 511
 512        let drag_fraction = (drag_position.x - bounds.left()) / bounds_width;
 513        let drag_fraction = drag_fraction * total_length_ratio;
 514        let diff = drag_fraction - col_position;
 515
 516        let is_dragging_right = diff > 0.0;
 517
 518        let mut diff_left = diff;
 519        let mut curr_column = col_idx + 1;
 520
 521        if is_dragging_right {
 522            while diff_left > 0.0 && curr_column < COLS {
 523                let Some(min_size) = resize_behavior[curr_column - 1].min_size() else {
 524                    curr_column += 1;
 525                    continue;
 526                };
 527
 528                let mut curr_width =
 529                    Self::get_fraction(&self.widths[curr_column], bounds_width, rem_size)
 530                        - diff_left;
 531
 532                diff_left = 0.0;
 533                if min_size > curr_width {
 534                    diff_left += min_size - curr_width;
 535                    curr_width = min_size;
 536                }
 537                self.widths[curr_column] = DefiniteLength::Fraction(curr_width);
 538                curr_column += 1;
 539            }
 540
 541            self.widths[col_idx] = DefiniteLength::Fraction(
 542                Self::get_fraction(&self.widths[col_idx], bounds_width, rem_size)
 543                    + (diff - diff_left),
 544            );
 545        } else {
 546            curr_column = col_idx;
 547            while diff_left < 0.0 && curr_column > 0 {
 548                // todo! When resize is none and dragging to the left this doesn't work correctly
 549                let Some(min_size) = resize_behavior[curr_column].min_size() else {
 550                    curr_column -= 1;
 551                    continue;
 552                };
 553
 554                let mut curr_width =
 555                    Self::get_fraction(&self.widths[curr_column], bounds_width, rem_size)
 556                        + diff_left;
 557
 558                diff_left = 0.0;
 559                if curr_width < min_size {
 560                    diff_left = curr_width - min_size;
 561                    curr_width = min_size
 562                }
 563
 564                self.widths[curr_column] = DefiniteLength::Fraction(curr_width);
 565                curr_column -= 1;
 566            }
 567
 568            self.widths[col_idx + 1] = DefiniteLength::Fraction(
 569                Self::get_fraction(&self.widths[col_idx + 1], bounds_width, rem_size)
 570                    - (diff - diff_left),
 571            );
 572        }
 573    }
 574}
 575
 576pub struct TableWidths<const COLS: usize> {
 577    initial: [DefiniteLength; COLS],
 578    current: Option<Entity<ColumnWidths<COLS>>>,
 579    resizable: [ResizeBehavior; COLS],
 580}
 581
 582impl<const COLS: usize> TableWidths<COLS> {
 583    pub fn new(widths: [impl Into<DefiniteLength>; COLS]) -> Self {
 584        let widths = widths.map(Into::into);
 585
 586        TableWidths {
 587            initial: widths.clone(),
 588            current: None,
 589            resizable: [ResizeBehavior::None; COLS],
 590        }
 591    }
 592
 593    fn lengths(&self, cx: &App) -> [Length; COLS] {
 594        self.current
 595            .as_ref()
 596            .map(|entity| entity.read(cx).widths.map(|w| Length::Definite(w)))
 597            .unwrap_or(self.initial.map(|w| Length::Definite(w)))
 598    }
 599}
 600
 601/// A table component
 602#[derive(RegisterComponent, IntoElement)]
 603pub struct Table<const COLS: usize = 3> {
 604    striped: bool,
 605    width: Option<Length>,
 606    headers: Option<[AnyElement; COLS]>,
 607    rows: TableContents<COLS>,
 608    interaction_state: Option<WeakEntity<TableInteractionState>>,
 609    col_widths: Option<TableWidths<COLS>>,
 610    map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
 611    empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
 612}
 613
 614impl<const COLS: usize> Table<COLS> {
 615    /// number of headers provided.
 616    pub fn new() -> Self {
 617        Self {
 618            striped: false,
 619            width: None,
 620            headers: None,
 621            rows: TableContents::Vec(Vec::new()),
 622            interaction_state: None,
 623            map_row: None,
 624            empty_table_callback: None,
 625            col_widths: None,
 626        }
 627    }
 628
 629    /// Enables uniform list rendering.
 630    /// The provided function will be passed directly to the `uniform_list` element.
 631    /// Therefore, if this method is called, any calls to [`Table::row`] before or after
 632    /// this method is called will be ignored.
 633    pub fn uniform_list(
 634        mut self,
 635        id: impl Into<ElementId>,
 636        row_count: usize,
 637        render_item_fn: impl Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>
 638        + 'static,
 639    ) -> Self {
 640        self.rows = TableContents::UniformList(UniformListData {
 641            element_id: id.into(),
 642            row_count: row_count,
 643            render_item_fn: Box::new(render_item_fn),
 644        });
 645        self
 646    }
 647
 648    /// Enables row striping.
 649    pub fn striped(mut self) -> Self {
 650        self.striped = true;
 651        self
 652    }
 653
 654    /// Sets the width of the table.
 655    /// Will enable horizontal scrolling if [`Self::interactable`] is also called.
 656    pub fn width(mut self, width: impl Into<Length>) -> Self {
 657        self.width = Some(width.into());
 658        self
 659    }
 660
 661    /// Enables interaction (primarily scrolling) with the table.
 662    ///
 663    /// Vertical scrolling will be enabled by default if the table is taller than its container.
 664    ///
 665    /// Horizontal scrolling will only be enabled if [`Self::width`] is also called, otherwise
 666    /// the list will always shrink the table columns to fit their contents I.e. If [`Self::uniform_list`]
 667    /// is used without a width and with [`Self::interactable`], the [`ListHorizontalSizingBehavior`] will
 668    /// be set to [`ListHorizontalSizingBehavior::FitList`].
 669    pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
 670        self.interaction_state = Some(interaction_state.downgrade());
 671        self
 672    }
 673
 674    pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self {
 675        self.headers = Some(headers.map(IntoElement::into_any_element));
 676        self
 677    }
 678
 679    pub fn row(mut self, items: [impl IntoElement; COLS]) -> Self {
 680        if let Some(rows) = self.rows.rows_mut() {
 681            rows.push(items.map(IntoElement::into_any_element));
 682        }
 683        self
 684    }
 685
 686    pub fn column_widths(mut self, widths: [impl Into<DefiniteLength>; COLS]) -> Self {
 687        if self.col_widths.is_none() {
 688            self.col_widths = Some(TableWidths::new(widths));
 689        }
 690        self
 691    }
 692
 693    pub fn resizable_columns(
 694        mut self,
 695        resizable: [ResizeBehavior; COLS],
 696        column_widths: &Entity<ColumnWidths<COLS>>,
 697        cx: &mut App,
 698    ) -> Self {
 699        if let Some(table_widths) = self.col_widths.as_mut() {
 700            table_widths.resizable = resizable;
 701            let column_widths = table_widths
 702                .current
 703                .get_or_insert_with(|| column_widths.clone());
 704
 705            column_widths.update(cx, |widths, _| {
 706                if !widths.initialized {
 707                    widths.initialized = true;
 708                    widths.widths = table_widths.initial.clone();
 709                }
 710            })
 711        }
 712        self
 713    }
 714
 715    pub fn map_row(
 716        mut self,
 717        callback: impl Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement + 'static,
 718    ) -> Self {
 719        self.map_row = Some(Rc::new(callback));
 720        self
 721    }
 722
 723    /// Provide a callback that is invoked when the table is rendered without any rows
 724    pub fn empty_table_callback(
 725        mut self,
 726        callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
 727    ) -> Self {
 728        self.empty_table_callback = Some(Rc::new(callback));
 729        self
 730    }
 731}
 732
 733fn base_cell_style(width: Option<Length>) -> Div {
 734    div()
 735        .px_1p5()
 736        .when_some(width, |this, width| this.w(width))
 737        .when(width.is_none(), |this| this.flex_1())
 738        .justify_start()
 739        .whitespace_nowrap()
 740        .text_ellipsis()
 741        .overflow_hidden()
 742}
 743
 744fn base_cell_style_text(width: Option<Length>, cx: &App) -> Div {
 745    base_cell_style(width).text_ui(cx)
 746}
 747
 748pub fn render_row<const COLS: usize>(
 749    row_index: usize,
 750    items: [impl IntoElement; COLS],
 751    table_context: TableRenderContext<COLS>,
 752    window: &mut Window,
 753    cx: &mut App,
 754) -> AnyElement {
 755    let is_striped = table_context.striped;
 756    let is_last = row_index == table_context.total_row_count - 1;
 757    let bg = if row_index % 2 == 1 && is_striped {
 758        Some(cx.theme().colors().text.opacity(0.05))
 759    } else {
 760        None
 761    };
 762    let column_widths = table_context
 763        .column_widths
 764        .map_or([None; COLS], |widths| widths.map(Some));
 765
 766    let mut row = h_flex()
 767        .h_full()
 768        .id(("table_row", row_index))
 769        .w_full()
 770        .justify_between()
 771        .when_some(bg, |row, bg| row.bg(bg))
 772        .when(!is_striped, |row| {
 773            row.border_b_1()
 774                .border_color(transparent_black())
 775                .when(!is_last, |row| row.border_color(cx.theme().colors().border))
 776        });
 777
 778    row = row.children(
 779        items
 780            .map(IntoElement::into_any_element)
 781            .into_iter()
 782            .zip(column_widths)
 783            .map(|(cell, width)| base_cell_style_text(width, cx).px_1p5().py_1().child(cell)),
 784    );
 785
 786    let row = if let Some(map_row) = table_context.map_row {
 787        map_row((row_index, row), window, cx)
 788    } else {
 789        row.into_any_element()
 790    };
 791
 792    div().h_full().w_full().child(row).into_any_element()
 793}
 794
 795pub fn render_header<const COLS: usize>(
 796    headers: [impl IntoElement; COLS],
 797    table_context: TableRenderContext<COLS>,
 798    cx: &mut App,
 799) -> impl IntoElement {
 800    let column_widths = table_context
 801        .column_widths
 802        .map_or([None; COLS], |widths| widths.map(Some));
 803    div()
 804        .flex()
 805        .flex_row()
 806        .items_center()
 807        .justify_between()
 808        .w_full()
 809        .p_2()
 810        .border_b_1()
 811        .border_color(cx.theme().colors().border)
 812        .children(
 813            headers
 814                .into_iter()
 815                .zip(column_widths)
 816                .map(|(h, width)| base_cell_style_text(width, cx).child(h)),
 817        )
 818}
 819
 820#[derive(Clone)]
 821pub struct TableRenderContext<const COLS: usize> {
 822    pub striped: bool,
 823    pub total_row_count: usize,
 824    pub column_widths: Option<[Length; COLS]>,
 825    pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
 826}
 827
 828impl<const COLS: usize> TableRenderContext<COLS> {
 829    fn new(table: &Table<COLS>, cx: &App) -> Self {
 830        Self {
 831            striped: table.striped,
 832            total_row_count: table.rows.len(),
 833            column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)),
 834            map_row: table.map_row.clone(),
 835        }
 836    }
 837}
 838
 839impl<const COLS: usize> RenderOnce for Table<COLS> {
 840    fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
 841        let table_context = TableRenderContext::new(&self, cx);
 842        let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
 843        let current_widths = self
 844            .col_widths
 845            .as_ref()
 846            .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable)))
 847            .map(|(curr, resize_behavior)| (curr.downgrade(), resize_behavior));
 848
 849        let scroll_track_size = px(16.);
 850        let h_scroll_offset = if interaction_state
 851            .as_ref()
 852            .is_some_and(|state| state.read(cx).vertical_scrollbar.show_scrollbar)
 853        {
 854            // magic number
 855            px(3.)
 856        } else {
 857            px(0.)
 858        };
 859
 860        let width = self.width;
 861        let no_rows_rendered = self.rows.is_empty();
 862
 863        let table = div()
 864            .when_some(width, |this, width| this.w(width))
 865            .h_full()
 866            .v_flex()
 867            .when_some(self.headers.take(), |this, headers| {
 868                this.child(render_header(headers, table_context.clone(), cx))
 869            })
 870            .when_some(current_widths, |this, (widths, resize_behavior)| {
 871                this.on_drag_move::<DraggedColumn>(move |e, window, cx| {
 872                    widths
 873                        .update(cx, |widths, cx| {
 874                            widths.on_drag_move(e, &resize_behavior, window, cx);
 875                        })
 876                        .ok();
 877                })
 878            })
 879            .on_drop::<DraggedColumn>(|_, _, _| {
 880                // Finish the resize operation
 881            })
 882            .child(
 883                div()
 884                    .flex_grow()
 885                    .w_full()
 886                    .relative()
 887                    .overflow_hidden()
 888                    .map(|parent| match self.rows {
 889                        TableContents::Vec(items) => {
 890                            parent.children(items.into_iter().enumerate().map(|(index, row)| {
 891                                render_row(index, row, table_context.clone(), window, cx)
 892                            }))
 893                        }
 894                        TableContents::UniformList(uniform_list_data) => parent.child(
 895                            uniform_list(
 896                                uniform_list_data.element_id,
 897                                uniform_list_data.row_count,
 898                                {
 899                                    let render_item_fn = uniform_list_data.render_item_fn;
 900                                    move |range: Range<usize>, window, cx| {
 901                                        let elements = render_item_fn(range.clone(), window, cx);
 902                                        elements
 903                                            .into_iter()
 904                                            .zip(range)
 905                                            .map(|(row, row_index)| {
 906                                                render_row(
 907                                                    row_index,
 908                                                    row,
 909                                                    table_context.clone(),
 910                                                    window,
 911                                                    cx,
 912                                                )
 913                                            })
 914                                            .collect()
 915                                    }
 916                                },
 917                            )
 918                            .size_full()
 919                            .flex_grow()
 920                            .with_sizing_behavior(ListSizingBehavior::Auto)
 921                            .with_horizontal_sizing_behavior(if width.is_some() {
 922                                ListHorizontalSizingBehavior::Unconstrained
 923                            } else {
 924                                ListHorizontalSizingBehavior::FitList
 925                            })
 926                            .when_some(
 927                                interaction_state.as_ref(),
 928                                |this, state| {
 929                                    this.track_scroll(
 930                                        state.read_with(cx, |s, _| s.scroll_handle.clone()),
 931                                    )
 932                                },
 933                            ),
 934                        ),
 935                    })
 936                    .when_some(
 937                        self.col_widths.as_ref().zip(interaction_state.as_ref()),
 938                        |parent, (table_widths, state)| {
 939                            parent.child(state.update(cx, |state, cx| {
 940                                let resizable_columns = table_widths.resizable;
 941                                let column_widths = table_widths.lengths(cx);
 942                                state.render_resize_handles(
 943                                    &column_widths,
 944                                    &resizable_columns,
 945                                    window,
 946                                    cx,
 947                                )
 948                            }))
 949                        },
 950                    )
 951                    .when_some(interaction_state.as_ref(), |this, interaction_state| {
 952                        this.map(|this| {
 953                            TableInteractionState::render_vertical_scrollbar_track(
 954                                interaction_state,
 955                                this,
 956                                scroll_track_size,
 957                                cx,
 958                            )
 959                        })
 960                        .map(|this| {
 961                            TableInteractionState::render_vertical_scrollbar(
 962                                interaction_state,
 963                                this,
 964                                cx,
 965                            )
 966                        })
 967                    }),
 968            )
 969            .when_some(
 970                no_rows_rendered
 971                    .then_some(self.empty_table_callback)
 972                    .flatten(),
 973                |this, callback| {
 974                    this.child(
 975                        h_flex()
 976                            .size_full()
 977                            .p_3()
 978                            .items_start()
 979                            .justify_center()
 980                            .child(callback(window, cx)),
 981                    )
 982                },
 983            )
 984            .when_some(
 985                width.and(interaction_state.as_ref()),
 986                |this, interaction_state| {
 987                    this.map(|this| {
 988                        TableInteractionState::render_horizontal_scrollbar_track(
 989                            interaction_state,
 990                            this,
 991                            scroll_track_size,
 992                            cx,
 993                        )
 994                    })
 995                    .map(|this| {
 996                        TableInteractionState::render_horizontal_scrollbar(
 997                            interaction_state,
 998                            this,
 999                            h_scroll_offset,
1000                            cx,
1001                        )
1002                    })
1003                },
1004            );
1005
1006        if let Some(interaction_state) = interaction_state.as_ref() {
1007            table
1008                .track_focus(&interaction_state.read(cx).focus_handle)
1009                .id(("table", interaction_state.entity_id()))
1010                .on_hover({
1011                    let interaction_state = interaction_state.downgrade();
1012                    move |hovered, window, cx| {
1013                        interaction_state
1014                            .update(cx, |interaction_state, cx| {
1015                                if *hovered {
1016                                    interaction_state.horizontal_scrollbar.show(cx);
1017                                    interaction_state.vertical_scrollbar.show(cx);
1018                                    cx.notify();
1019                                } else if !interaction_state
1020                                    .focus_handle
1021                                    .contains_focused(window, cx)
1022                                {
1023                                    interaction_state.hide_scrollbars(window, cx);
1024                                }
1025                            })
1026                            .ok();
1027                    }
1028                })
1029                .into_any_element()
1030        } else {
1031            table.into_any_element()
1032        }
1033    }
1034}
1035
1036// computed state related to how to render scrollbars
1037// one per axis
1038// on render we just read this off the keymap editor
1039// we update it when
1040// - settings change
1041// - on focus in, on focus out, on hover, etc.
1042#[derive(Debug)]
1043pub struct ScrollbarProperties {
1044    axis: Axis,
1045    show_scrollbar: bool,
1046    show_track: bool,
1047    auto_hide: bool,
1048    hide_task: Option<Task<()>>,
1049    state: ScrollbarState,
1050}
1051
1052impl ScrollbarProperties {
1053    // Shows the scrollbar and cancels any pending hide task
1054    fn show(&mut self, cx: &mut Context<TableInteractionState>) {
1055        if !self.auto_hide {
1056            return;
1057        }
1058        self.show_scrollbar = true;
1059        self.hide_task.take();
1060        cx.notify();
1061    }
1062
1063    fn hide(&mut self, window: &mut Window, cx: &mut Context<TableInteractionState>) {
1064        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
1065
1066        if !self.auto_hide {
1067            return;
1068        }
1069
1070        let axis = self.axis;
1071        self.hide_task = Some(cx.spawn_in(window, async move |keymap_editor, cx| {
1072            cx.background_executor()
1073                .timer(SCROLLBAR_SHOW_INTERVAL)
1074                .await;
1075
1076            if let Some(keymap_editor) = keymap_editor.upgrade() {
1077                keymap_editor
1078                    .update(cx, |keymap_editor, cx| {
1079                        match axis {
1080                            Axis::Vertical => {
1081                                keymap_editor.vertical_scrollbar.show_scrollbar = false
1082                            }
1083                            Axis::Horizontal => {
1084                                keymap_editor.horizontal_scrollbar.show_scrollbar = false
1085                            }
1086                        }
1087                        cx.notify();
1088                    })
1089                    .ok();
1090            }
1091        }));
1092    }
1093}
1094
1095impl Component for Table<3> {
1096    fn scope() -> ComponentScope {
1097        ComponentScope::Layout
1098    }
1099
1100    fn description() -> Option<&'static str> {
1101        Some("A table component for displaying data in rows and columns with optional styling.")
1102    }
1103
1104    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
1105        Some(
1106            v_flex()
1107                .gap_6()
1108                .children(vec![
1109                    example_group_with_title(
1110                        "Basic Tables",
1111                        vec![
1112                            single_example(
1113                                "Simple Table",
1114                                Table::new()
1115                                    .width(px(400.))
1116                                    .header(["Name", "Age", "City"])
1117                                    .row(["Alice", "28", "New York"])
1118                                    .row(["Bob", "32", "San Francisco"])
1119                                    .row(["Charlie", "25", "London"])
1120                                    .into_any_element(),
1121                            ),
1122                            single_example(
1123                                "Two Column Table",
1124                                Table::new()
1125                                    .header(["Category", "Value"])
1126                                    .width(px(300.))
1127                                    .row(["Revenue", "$100,000"])
1128                                    .row(["Expenses", "$75,000"])
1129                                    .row(["Profit", "$25,000"])
1130                                    .into_any_element(),
1131                            ),
1132                        ],
1133                    ),
1134                    example_group_with_title(
1135                        "Styled Tables",
1136                        vec![
1137                            single_example(
1138                                "Default",
1139                                Table::new()
1140                                    .width(px(400.))
1141                                    .header(["Product", "Price", "Stock"])
1142                                    .row(["Laptop", "$999", "In Stock"])
1143                                    .row(["Phone", "$599", "Low Stock"])
1144                                    .row(["Tablet", "$399", "Out of Stock"])
1145                                    .into_any_element(),
1146                            ),
1147                            single_example(
1148                                "Striped",
1149                                Table::new()
1150                                    .width(px(400.))
1151                                    .striped()
1152                                    .header(["Product", "Price", "Stock"])
1153                                    .row(["Laptop", "$999", "In Stock"])
1154                                    .row(["Phone", "$599", "Low Stock"])
1155                                    .row(["Tablet", "$399", "Out of Stock"])
1156                                    .row(["Headphones", "$199", "In Stock"])
1157                                    .into_any_element(),
1158                            ),
1159                        ],
1160                    ),
1161                    example_group_with_title(
1162                        "Mixed Content Table",
1163                        vec![single_example(
1164                            "Table with Elements",
1165                            Table::new()
1166                                .width(px(840.))
1167                                .header(["Status", "Name", "Priority", "Deadline", "Action"])
1168                                .row([
1169                                    Indicator::dot().color(Color::Success).into_any_element(),
1170                                    "Project A".into_any_element(),
1171                                    "High".into_any_element(),
1172                                    "2023-12-31".into_any_element(),
1173                                    Button::new("view_a", "View")
1174                                        .style(ButtonStyle::Filled)
1175                                        .full_width()
1176                                        .into_any_element(),
1177                                ])
1178                                .row([
1179                                    Indicator::dot().color(Color::Warning).into_any_element(),
1180                                    "Project B".into_any_element(),
1181                                    "Medium".into_any_element(),
1182                                    "2024-03-15".into_any_element(),
1183                                    Button::new("view_b", "View")
1184                                        .style(ButtonStyle::Filled)
1185                                        .full_width()
1186                                        .into_any_element(),
1187                                ])
1188                                .row([
1189                                    Indicator::dot().color(Color::Error).into_any_element(),
1190                                    "Project C".into_any_element(),
1191                                    "Low".into_any_element(),
1192                                    "2024-06-30".into_any_element(),
1193                                    Button::new("view_c", "View")
1194                                        .style(ButtonStyle::Filled)
1195                                        .full_width()
1196                                        .into_any_element(),
1197                                ])
1198                                .into_any_element(),
1199                        )],
1200                    ),
1201                ])
1202                .into_any_element(),
1203        )
1204    }
1205}