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