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