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