data_table.rs

   1use std::{ops::Range, rc::Rc};
   2
   3use gpui::{
   4    AbsoluteLength, AppContext, Context, DefiniteLength, DragMoveEvent, Entity, EntityId,
   5    FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, Point, Stateful,
   6    UniformListScrollHandle, WeakEntity, transparent_black, uniform_list,
   7};
   8
   9use crate::{
  10    ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
  11    ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator,
  12    InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce,
  13    ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled, StyledExt as _,
  14    StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex, px,
  15    single_example, v_flex,
  16};
  17use itertools::intersperse_with;
  18
  19const RESIZE_COLUMN_WIDTH: f32 = 8.0;
  20
  21#[derive(Debug)]
  22struct DraggedColumn(usize);
  23
  24struct UniformListData<const COLS: usize> {
  25    render_item_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>,
  26    element_id: ElementId,
  27    row_count: usize,
  28}
  29
  30enum TableContents<const COLS: usize> {
  31    Vec(Vec<[AnyElement; COLS]>),
  32    UniformList(UniformListData<COLS>),
  33}
  34
  35impl<const COLS: usize> TableContents<COLS> {
  36    fn rows_mut(&mut self) -> Option<&mut Vec<[AnyElement; COLS]>> {
  37        match self {
  38            TableContents::Vec(rows) => Some(rows),
  39            TableContents::UniformList(_) => None,
  40        }
  41    }
  42
  43    fn len(&self) -> usize {
  44        match self {
  45            TableContents::Vec(rows) => rows.len(),
  46            TableContents::UniformList(data) => data.row_count,
  47        }
  48    }
  49
  50    fn is_empty(&self) -> bool {
  51        self.len() == 0
  52    }
  53}
  54
  55pub struct TableInteractionState {
  56    pub focus_handle: FocusHandle,
  57    pub scroll_handle: UniformListScrollHandle,
  58    pub custom_scrollbar: Option<Scrollbars>,
  59}
  60
  61impl TableInteractionState {
  62    pub fn new(cx: &mut App) -> Self {
  63        Self {
  64            focus_handle: cx.focus_handle(),
  65            scroll_handle: UniformListScrollHandle::new(),
  66            custom_scrollbar: None,
  67        }
  68    }
  69
  70    pub fn with_custom_scrollbar(mut self, custom_scrollbar: Scrollbars) -> Self {
  71        self.custom_scrollbar = Some(custom_scrollbar);
  72        self
  73    }
  74
  75    pub fn scroll_offset(&self) -> Point<Pixels> {
  76        self.scroll_handle.offset()
  77    }
  78
  79    pub fn set_scroll_offset(&self, offset: Point<Pixels>) {
  80        self.scroll_handle.set_offset(offset);
  81    }
  82
  83    pub fn listener<E: ?Sized>(
  84        this: &Entity<Self>,
  85        f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static,
  86    ) -> impl Fn(&E, &mut Window, &mut App) + 'static {
  87        let view = this.downgrade();
  88        move |e: &E, window: &mut Window, cx: &mut App| {
  89            view.update(cx, |view, cx| f(view, e, window, cx)).ok();
  90        }
  91    }
  92
  93    fn render_resize_handles<const COLS: usize>(
  94        &self,
  95        column_widths: &[Length; COLS],
  96        resizable_columns: &[TableResizeBehavior; COLS],
  97        initial_sizes: [DefiniteLength; COLS],
  98        columns: Option<Entity<TableColumnWidths<COLS>>>,
  99        window: &mut Window,
 100        cx: &mut App,
 101    ) -> AnyElement {
 102        let spacers = column_widths
 103            .iter()
 104            .map(|width| base_cell_style(Some(*width)).into_any_element());
 105
 106        let mut column_ix = 0;
 107        let resizable_columns_slice = *resizable_columns;
 108        let mut resizable_columns = resizable_columns.iter();
 109
 110        let dividers = intersperse_with(spacers, || {
 111            window.with_id(column_ix, |window| {
 112                let mut resize_divider = div()
 113                    // This is required because this is evaluated at a different time than the use_state call above
 114                    .id(column_ix)
 115                    .relative()
 116                    .top_0()
 117                    .w_px()
 118                    .h_full()
 119                    .bg(cx.theme().colors().border.opacity(0.8));
 120
 121                let mut resize_handle = div()
 122                    .id("column-resize-handle")
 123                    .absolute()
 124                    .left_neg_0p5()
 125                    .w(px(RESIZE_COLUMN_WIDTH))
 126                    .h_full();
 127
 128                if resizable_columns
 129                    .next()
 130                    .is_some_and(TableResizeBehavior::is_resizable)
 131                {
 132                    let hovered = window.use_state(cx, |_window, _cx| false);
 133
 134                    resize_divider = resize_divider.when(*hovered.read(cx), |div| {
 135                        div.bg(cx.theme().colors().border_focused)
 136                    });
 137
 138                    resize_handle = resize_handle
 139                        .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered))
 140                        .cursor_col_resize()
 141                        .when_some(columns.clone(), |this, columns| {
 142                            this.on_click(move |event, window, cx| {
 143                                if event.click_count() >= 2 {
 144                                    columns.update(cx, |columns, _| {
 145                                        columns.on_double_click(
 146                                            column_ix,
 147                                            &initial_sizes,
 148                                            &resizable_columns_slice,
 149                                            window,
 150                                        );
 151                                    })
 152                                }
 153
 154                                cx.stop_propagation();
 155                            })
 156                        })
 157                        .on_drag(DraggedColumn(column_ix), |_, _offset, _window, cx| {
 158                            cx.new(|_cx| gpui::Empty)
 159                        })
 160                }
 161
 162                column_ix += 1;
 163                resize_divider.child(resize_handle).into_any_element()
 164            })
 165        });
 166
 167        h_flex()
 168            .id("resize-handles")
 169            .absolute()
 170            .inset_0()
 171            .w_full()
 172            .children(dividers)
 173            .into_any_element()
 174    }
 175}
 176
 177#[derive(Debug, Copy, Clone, PartialEq)]
 178pub enum TableResizeBehavior {
 179    None,
 180    Resizable,
 181    MinSize(f32),
 182}
 183
 184impl TableResizeBehavior {
 185    pub fn is_resizable(&self) -> bool {
 186        *self != TableResizeBehavior::None
 187    }
 188
 189    pub fn min_size(&self) -> Option<f32> {
 190        match self {
 191            TableResizeBehavior::None => None,
 192            TableResizeBehavior::Resizable => Some(0.05),
 193            TableResizeBehavior::MinSize(min_size) => Some(*min_size),
 194        }
 195    }
 196}
 197
 198pub struct TableColumnWidths<const COLS: usize> {
 199    widths: [DefiniteLength; COLS],
 200    visible_widths: [DefiniteLength; COLS],
 201    cached_bounds_width: Pixels,
 202    initialized: bool,
 203}
 204
 205impl<const COLS: usize> TableColumnWidths<COLS> {
 206    pub fn new(_: &mut App) -> Self {
 207        Self {
 208            widths: [DefiniteLength::default(); COLS],
 209            visible_widths: [DefiniteLength::default(); COLS],
 210            cached_bounds_width: Default::default(),
 211            initialized: false,
 212        }
 213    }
 214
 215    fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 {
 216        match length {
 217            DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => *pixels / bounds_width,
 218            DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => {
 219                rems_width.to_pixels(rem_size) / bounds_width
 220            }
 221            DefiniteLength::Fraction(fraction) => *fraction,
 222        }
 223    }
 224
 225    fn on_double_click(
 226        &mut self,
 227        double_click_position: usize,
 228        initial_sizes: &[DefiniteLength; COLS],
 229        resize_behavior: &[TableResizeBehavior; COLS],
 230        window: &mut Window,
 231    ) {
 232        let bounds_width = self.cached_bounds_width;
 233        let rem_size = window.rem_size();
 234        let initial_sizes =
 235            initial_sizes.map(|length| Self::get_fraction(&length, bounds_width, rem_size));
 236        let widths = self
 237            .widths
 238            .map(|length| Self::get_fraction(&length, bounds_width, rem_size));
 239
 240        let updated_widths = Self::reset_to_initial_size(
 241            double_click_position,
 242            widths,
 243            initial_sizes,
 244            resize_behavior,
 245        );
 246        self.widths = updated_widths.map(DefiniteLength::Fraction);
 247        self.visible_widths = self.widths;
 248    }
 249
 250    fn reset_to_initial_size(
 251        col_idx: usize,
 252        mut widths: [f32; COLS],
 253        initial_sizes: [f32; COLS],
 254        resize_behavior: &[TableResizeBehavior; COLS],
 255    ) -> [f32; COLS] {
 256        // RESET:
 257        // Part 1:
 258        // Figure out if we should shrink/grow the selected column
 259        // Get diff which represents the change in column we want to make initial size delta curr_size = diff
 260        //
 261        // Part 2: We need to decide which side column we should move and where
 262        //
 263        // If we want to grow our column we should check the left/right columns diff to see what side
 264        // has a greater delta than their initial size. Likewise, if we shrink our column we should check
 265        // the left/right column diffs to see what side has the smallest delta.
 266        //
 267        // Part 3: resize
 268        //
 269        // col_idx represents the column handle to the right of an active column
 270        //
 271        // If growing and right has the greater delta {
 272        //    shift col_idx to the right
 273        // } else if growing and left has the greater delta {
 274        //  shift col_idx - 1 to the left
 275        // } else if shrinking and the right has the greater delta {
 276        //  shift
 277        // } {
 278        //
 279        // }
 280        // }
 281        //
 282        // if we need to shrink, then if the right
 283        //
 284
 285        // DRAGGING
 286        // we get diff which represents the change in the _drag handle_ position
 287        // -diff => dragging left ->
 288        //      grow the column to the right of the handle as much as we can shrink columns to the left of the handle
 289        // +diff => dragging right -> growing handles column
 290        //      grow the column to the left of the handle as much as we can shrink columns to the right of the handle
 291        //
 292
 293        let diff = initial_sizes[col_idx] - widths[col_idx];
 294
 295        let left_diff =
 296            initial_sizes[..col_idx].iter().sum::<f32>() - widths[..col_idx].iter().sum::<f32>();
 297        let right_diff = initial_sizes[col_idx + 1..].iter().sum::<f32>()
 298            - widths[col_idx + 1..].iter().sum::<f32>();
 299
 300        let go_left_first = if diff < 0.0 {
 301            left_diff > right_diff
 302        } else {
 303            left_diff < right_diff
 304        };
 305
 306        if !go_left_first {
 307            let diff_remaining =
 308                Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, 1);
 309
 310            if diff_remaining != 0.0 && col_idx > 0 {
 311                Self::propagate_resize_diff(
 312                    diff_remaining,
 313                    col_idx,
 314                    &mut widths,
 315                    resize_behavior,
 316                    -1,
 317                );
 318            }
 319        } else {
 320            let diff_remaining =
 321                Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, -1);
 322
 323            if diff_remaining != 0.0 {
 324                Self::propagate_resize_diff(
 325                    diff_remaining,
 326                    col_idx,
 327                    &mut widths,
 328                    resize_behavior,
 329                    1,
 330                );
 331            }
 332        }
 333
 334        widths
 335    }
 336
 337    fn on_drag_move(
 338        &mut self,
 339        drag_event: &DragMoveEvent<DraggedColumn>,
 340        resize_behavior: &[TableResizeBehavior; COLS],
 341        window: &mut Window,
 342        cx: &mut Context<Self>,
 343    ) {
 344        let drag_position = drag_event.event.position;
 345        let bounds = drag_event.bounds;
 346
 347        let mut col_position = 0.0;
 348        let rem_size = window.rem_size();
 349        let bounds_width = bounds.right() - bounds.left();
 350        let col_idx = drag_event.drag(cx).0;
 351
 352        let column_handle_width = Self::get_fraction(
 353            &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_COLUMN_WIDTH))),
 354            bounds_width,
 355            rem_size,
 356        );
 357
 358        let mut widths = self
 359            .widths
 360            .map(|length| Self::get_fraction(&length, bounds_width, rem_size));
 361
 362        for length in widths[0..=col_idx].iter() {
 363            col_position += length + column_handle_width;
 364        }
 365
 366        let mut total_length_ratio = col_position;
 367        for length in widths[col_idx + 1..].iter() {
 368            total_length_ratio += length;
 369        }
 370        total_length_ratio += (COLS - 1 - col_idx) as f32 * column_handle_width;
 371
 372        let drag_fraction = (drag_position.x - bounds.left()) / bounds_width;
 373        let drag_fraction = drag_fraction * total_length_ratio;
 374        let diff = drag_fraction - col_position - column_handle_width / 2.0;
 375
 376        Self::drag_column_handle(diff, col_idx, &mut widths, resize_behavior);
 377
 378        self.visible_widths = widths.map(DefiniteLength::Fraction);
 379    }
 380
 381    fn drag_column_handle(
 382        diff: f32,
 383        col_idx: usize,
 384        widths: &mut [f32; COLS],
 385        resize_behavior: &[TableResizeBehavior; COLS],
 386    ) {
 387        // if diff > 0.0 then go right
 388        if diff > 0.0 {
 389            Self::propagate_resize_diff(diff, col_idx, widths, resize_behavior, 1);
 390        } else {
 391            Self::propagate_resize_diff(-diff, col_idx + 1, widths, resize_behavior, -1);
 392        }
 393    }
 394
 395    fn propagate_resize_diff(
 396        diff: f32,
 397        col_idx: usize,
 398        widths: &mut [f32; COLS],
 399        resize_behavior: &[TableResizeBehavior; COLS],
 400        direction: i8,
 401    ) -> f32 {
 402        let mut diff_remaining = diff;
 403        if resize_behavior[col_idx].min_size().is_none() {
 404            return diff;
 405        }
 406
 407        let step_right;
 408        let step_left;
 409        if direction < 0 {
 410            step_right = 0;
 411            step_left = 1;
 412        } else {
 413            step_right = 1;
 414            step_left = 0;
 415        }
 416        if col_idx == 0 && direction < 0 {
 417            return diff;
 418        }
 419        let mut curr_column = col_idx + step_right - step_left;
 420
 421        while diff_remaining != 0.0 && curr_column < COLS {
 422            let Some(min_size) = resize_behavior[curr_column].min_size() else {
 423                if curr_column == 0 {
 424                    break;
 425                }
 426                curr_column -= step_left;
 427                curr_column += step_right;
 428                continue;
 429            };
 430
 431            let curr_width = widths[curr_column] - diff_remaining;
 432            widths[curr_column] = curr_width;
 433
 434            if min_size > curr_width {
 435                diff_remaining = min_size - curr_width;
 436                widths[curr_column] = min_size;
 437            } else {
 438                diff_remaining = 0.0;
 439                break;
 440            }
 441            if curr_column == 0 {
 442                break;
 443            }
 444            curr_column -= step_left;
 445            curr_column += step_right;
 446        }
 447        widths[col_idx] = widths[col_idx] + (diff - diff_remaining);
 448
 449        diff_remaining
 450    }
 451}
 452
 453pub struct TableWidths<const COLS: usize> {
 454    initial: [DefiniteLength; COLS],
 455    current: Option<Entity<TableColumnWidths<COLS>>>,
 456    resizable: [TableResizeBehavior; COLS],
 457}
 458
 459impl<const COLS: usize> TableWidths<COLS> {
 460    pub fn new(widths: [impl Into<DefiniteLength>; COLS]) -> Self {
 461        let widths = widths.map(Into::into);
 462
 463        TableWidths {
 464            initial: widths,
 465            current: None,
 466            resizable: [TableResizeBehavior::None; COLS],
 467        }
 468    }
 469
 470    fn lengths(&self, cx: &App) -> [Length; COLS] {
 471        self.current
 472            .as_ref()
 473            .map(|entity| entity.read(cx).visible_widths.map(Length::Definite))
 474            .unwrap_or(self.initial.map(Length::Definite))
 475    }
 476}
 477
 478/// A table component
 479#[derive(RegisterComponent, IntoElement)]
 480pub struct Table<const COLS: usize = 3> {
 481    striped: bool,
 482    width: Option<Length>,
 483    headers: Option<[AnyElement; COLS]>,
 484    rows: TableContents<COLS>,
 485    interaction_state: Option<WeakEntity<TableInteractionState>>,
 486    col_widths: Option<TableWidths<COLS>>,
 487    map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
 488    use_ui_font: bool,
 489    empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
 490}
 491
 492impl<const COLS: usize> Table<COLS> {
 493    /// number of headers provided.
 494    pub fn new() -> Self {
 495        Self {
 496            striped: false,
 497            width: None,
 498            headers: None,
 499            rows: TableContents::Vec(Vec::new()),
 500            interaction_state: None,
 501            map_row: None,
 502            use_ui_font: true,
 503            empty_table_callback: None,
 504            col_widths: None,
 505        }
 506    }
 507
 508    /// Enables uniform list rendering.
 509    /// The provided function will be passed directly to the `uniform_list` element.
 510    /// Therefore, if this method is called, any calls to [`Table::row`] before or after
 511    /// this method is called will be ignored.
 512    pub fn uniform_list(
 513        mut self,
 514        id: impl Into<ElementId>,
 515        row_count: usize,
 516        render_item_fn: impl Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>
 517        + 'static,
 518    ) -> Self {
 519        self.rows = TableContents::UniformList(UniformListData {
 520            element_id: id.into(),
 521            row_count,
 522            render_item_fn: Box::new(render_item_fn),
 523        });
 524        self
 525    }
 526
 527    /// Enables row striping.
 528    pub fn striped(mut self) -> Self {
 529        self.striped = true;
 530        self
 531    }
 532
 533    /// Sets the width of the table.
 534    /// Will enable horizontal scrolling if [`Self::interactable`] is also called.
 535    pub fn width(mut self, width: impl Into<Length>) -> Self {
 536        self.width = Some(width.into());
 537        self
 538    }
 539
 540    /// Enables interaction (primarily scrolling) with the table.
 541    ///
 542    /// Vertical scrolling will be enabled by default if the table is taller than its container.
 543    ///
 544    /// Horizontal scrolling will only be enabled if [`Self::width`] is also called, otherwise
 545    /// the list will always shrink the table columns to fit their contents I.e. If [`Self::uniform_list`]
 546    /// is used without a width and with [`Self::interactable`], the [`ListHorizontalSizingBehavior`] will
 547    /// be set to [`ListHorizontalSizingBehavior::FitList`].
 548    pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
 549        self.interaction_state = Some(interaction_state.downgrade());
 550        self
 551    }
 552
 553    pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self {
 554        self.headers = Some(headers.map(IntoElement::into_any_element));
 555        self
 556    }
 557
 558    pub fn row(mut self, items: [impl IntoElement; COLS]) -> Self {
 559        if let Some(rows) = self.rows.rows_mut() {
 560            rows.push(items.map(IntoElement::into_any_element));
 561        }
 562        self
 563    }
 564
 565    pub fn column_widths(mut self, widths: [impl Into<DefiniteLength>; COLS]) -> Self {
 566        if self.col_widths.is_none() {
 567            self.col_widths = Some(TableWidths::new(widths));
 568        }
 569        self
 570    }
 571
 572    pub fn resizable_columns(
 573        mut self,
 574        resizable: [TableResizeBehavior; COLS],
 575        column_widths: &Entity<TableColumnWidths<COLS>>,
 576        cx: &mut App,
 577    ) -> Self {
 578        if let Some(table_widths) = self.col_widths.as_mut() {
 579            table_widths.resizable = resizable;
 580            let column_widths = table_widths
 581                .current
 582                .get_or_insert_with(|| column_widths.clone());
 583
 584            column_widths.update(cx, |widths, _| {
 585                if !widths.initialized {
 586                    widths.initialized = true;
 587                    widths.widths = table_widths.initial;
 588                    widths.visible_widths = widths.widths;
 589                }
 590            })
 591        }
 592        self
 593    }
 594
 595    pub fn no_ui_font(mut self) -> Self {
 596        self.use_ui_font = false;
 597        self
 598    }
 599
 600    pub fn map_row(
 601        mut self,
 602        callback: impl Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement + 'static,
 603    ) -> Self {
 604        self.map_row = Some(Rc::new(callback));
 605        self
 606    }
 607
 608    /// Provide a callback that is invoked when the table is rendered without any rows
 609    pub fn empty_table_callback(
 610        mut self,
 611        callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
 612    ) -> Self {
 613        self.empty_table_callback = Some(Rc::new(callback));
 614        self
 615    }
 616}
 617
 618fn base_cell_style(width: Option<Length>) -> Div {
 619    div()
 620        .px_1p5()
 621        .when_some(width, |this, width| this.w(width))
 622        .when(width.is_none(), |this| this.flex_1())
 623        .whitespace_nowrap()
 624        .text_ellipsis()
 625        .overflow_hidden()
 626}
 627
 628fn base_cell_style_text(width: Option<Length>, use_ui_font: bool, cx: &App) -> Div {
 629    base_cell_style(width).when(use_ui_font, |el| el.text_ui(cx))
 630}
 631
 632pub fn render_table_row<const COLS: usize>(
 633    row_index: usize,
 634    items: [impl IntoElement; COLS],
 635    table_context: TableRenderContext<COLS>,
 636    window: &mut Window,
 637    cx: &mut App,
 638) -> AnyElement {
 639    let is_striped = table_context.striped;
 640    let is_last = row_index == table_context.total_row_count - 1;
 641    let bg = if row_index % 2 == 1 && is_striped {
 642        Some(cx.theme().colors().text.opacity(0.05))
 643    } else {
 644        None
 645    };
 646    let column_widths = table_context
 647        .column_widths
 648        .map_or([None; COLS], |widths| widths.map(Some));
 649
 650    let mut row = h_flex()
 651        .id(("table_row", row_index))
 652        .size_full()
 653        .when_some(bg, |row, bg| row.bg(bg))
 654        .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.6)))
 655        .when(!is_striped, |row| {
 656            row.border_b_1()
 657                .border_color(transparent_black())
 658                .when(!is_last, |row| row.border_color(cx.theme().colors().border))
 659        });
 660
 661    row = row.children(
 662        items
 663            .map(IntoElement::into_any_element)
 664            .into_iter()
 665            .zip(column_widths)
 666            .map(|(cell, width)| {
 667                base_cell_style_text(width, table_context.use_ui_font, cx)
 668                    .px_1()
 669                    .py_0p5()
 670                    .child(cell)
 671            }),
 672    );
 673
 674    let row = if let Some(map_row) = table_context.map_row {
 675        map_row((row_index, row), window, cx)
 676    } else {
 677        row.into_any_element()
 678    };
 679
 680    div().size_full().child(row).into_any_element()
 681}
 682
 683pub fn render_table_header<const COLS: usize>(
 684    headers: [impl IntoElement; COLS],
 685    table_context: TableRenderContext<COLS>,
 686    columns_widths: Option<(
 687        WeakEntity<TableColumnWidths<COLS>>,
 688        [TableResizeBehavior; COLS],
 689        [DefiniteLength; COLS],
 690    )>,
 691    entity_id: Option<EntityId>,
 692    cx: &mut App,
 693) -> impl IntoElement {
 694    let column_widths = table_context
 695        .column_widths
 696        .map_or([None; COLS], |widths| widths.map(Some));
 697
 698    let element_id = entity_id
 699        .map(|entity| entity.to_string())
 700        .unwrap_or_default();
 701
 702    let shared_element_id: SharedString = format!("table-{}", element_id).into();
 703
 704    div()
 705        .flex()
 706        .flex_row()
 707        .items_center()
 708        .justify_between()
 709        .w_full()
 710        .p_2()
 711        .border_b_1()
 712        .border_color(cx.theme().colors().border)
 713        .children(headers.into_iter().enumerate().zip(column_widths).map(
 714            |((header_idx, h), width)| {
 715                base_cell_style_text(width, table_context.use_ui_font, cx)
 716                    .child(h)
 717                    .id(ElementId::NamedInteger(
 718                        shared_element_id.clone(),
 719                        header_idx as u64,
 720                    ))
 721                    .when_some(
 722                        columns_widths.as_ref().cloned(),
 723                        |this, (column_widths, resizables, initial_sizes)| {
 724                            if resizables[header_idx].is_resizable() {
 725                                this.on_click(move |event, window, cx| {
 726                                    if event.click_count() > 1 {
 727                                        column_widths
 728                                            .update(cx, |column, _| {
 729                                                column.on_double_click(
 730                                                    header_idx,
 731                                                    &initial_sizes,
 732                                                    &resizables,
 733                                                    window,
 734                                                );
 735                                            })
 736                                            .ok();
 737                                    }
 738                                })
 739                            } else {
 740                                this
 741                            }
 742                        },
 743                    )
 744            },
 745        ))
 746}
 747
 748#[derive(Clone)]
 749pub struct TableRenderContext<const COLS: usize> {
 750    pub striped: bool,
 751    pub total_row_count: usize,
 752    pub column_widths: Option<[Length; COLS]>,
 753    pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
 754    pub use_ui_font: bool,
 755}
 756
 757impl<const COLS: usize> TableRenderContext<COLS> {
 758    fn new(table: &Table<COLS>, cx: &App) -> Self {
 759        Self {
 760            striped: table.striped,
 761            total_row_count: table.rows.len(),
 762            column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)),
 763            map_row: table.map_row.clone(),
 764            use_ui_font: table.use_ui_font,
 765        }
 766    }
 767}
 768
 769impl<const COLS: usize> RenderOnce for Table<COLS> {
 770    fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
 771        let table_context = TableRenderContext::new(&self, cx);
 772        let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
 773        let current_widths = self
 774            .col_widths
 775            .as_ref()
 776            .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable)))
 777            .map(|(curr, resize_behavior)| (curr.downgrade(), resize_behavior));
 778
 779        let current_widths_with_initial_sizes = self
 780            .col_widths
 781            .as_ref()
 782            .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable, widths.initial)))
 783            .map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial));
 784
 785        let width = self.width;
 786        let no_rows_rendered = self.rows.is_empty();
 787
 788        let table = div()
 789            .when_some(width, |this, width| this.w(width))
 790            .h_full()
 791            .v_flex()
 792            .when_some(self.headers.take(), |this, headers| {
 793                this.child(render_table_header(
 794                    headers,
 795                    table_context.clone(),
 796                    current_widths_with_initial_sizes,
 797                    interaction_state.as_ref().map(Entity::entity_id),
 798                    cx,
 799                ))
 800            })
 801            .when_some(current_widths, {
 802                |this, (widths, resize_behavior)| {
 803                    this.on_drag_move::<DraggedColumn>({
 804                        let widths = widths.clone();
 805                        move |e, window, cx| {
 806                            widths
 807                                .update(cx, |widths, cx| {
 808                                    widths.on_drag_move(e, &resize_behavior, window, cx);
 809                                })
 810                                .ok();
 811                        }
 812                    })
 813                    .on_children_prepainted({
 814                        let widths = widths.clone();
 815                        move |bounds, _, cx| {
 816                            widths
 817                                .update(cx, |widths, _| {
 818                                    // This works because all children x axis bounds are the same
 819                                    widths.cached_bounds_width =
 820                                        bounds[0].right() - bounds[0].left();
 821                                })
 822                                .ok();
 823                        }
 824                    })
 825                    .on_drop::<DraggedColumn>(move |_, _, cx| {
 826                        widths
 827                            .update(cx, |widths, _| {
 828                                widths.widths = widths.visible_widths;
 829                            })
 830                            .ok();
 831                        // Finish the resize operation
 832                    })
 833                }
 834            })
 835            .child({
 836                let content = div()
 837                    .flex_grow()
 838                    .w_full()
 839                    .relative()
 840                    .overflow_hidden()
 841                    .map(|parent| match self.rows {
 842                        TableContents::Vec(items) => {
 843                            parent.children(items.into_iter().enumerate().map(|(index, row)| {
 844                                div().child(render_table_row(
 845                                    index,
 846                                    row,
 847                                    table_context.clone(),
 848                                    window,
 849                                    cx,
 850                                ))
 851                            }))
 852                        }
 853                        TableContents::UniformList(uniform_list_data) => parent.child(
 854                            uniform_list(
 855                                uniform_list_data.element_id,
 856                                uniform_list_data.row_count,
 857                                {
 858                                    let render_item_fn = uniform_list_data.render_item_fn;
 859                                    move |range: Range<usize>, window, cx| {
 860                                        let elements = render_item_fn(range.clone(), window, cx);
 861                                        elements
 862                                            .into_iter()
 863                                            .zip(range)
 864                                            .map(|(row, row_index)| {
 865                                                render_table_row(
 866                                                    row_index,
 867                                                    row,
 868                                                    table_context.clone(),
 869                                                    window,
 870                                                    cx,
 871                                                )
 872                                            })
 873                                            .collect()
 874                                    }
 875                                },
 876                            )
 877                            .size_full()
 878                            .flex_grow()
 879                            .with_sizing_behavior(ListSizingBehavior::Auto)
 880                            .with_horizontal_sizing_behavior(if width.is_some() {
 881                                ListHorizontalSizingBehavior::Unconstrained
 882                            } else {
 883                                ListHorizontalSizingBehavior::FitList
 884                            })
 885                            .when_some(
 886                                interaction_state.as_ref(),
 887                                |this, state| {
 888                                    this.track_scroll(
 889                                        &state.read_with(cx, |s, _| s.scroll_handle.clone()),
 890                                    )
 891                                },
 892                            ),
 893                        ),
 894                    })
 895                    .when_some(
 896                        self.col_widths.as_ref().zip(interaction_state.as_ref()),
 897                        |parent, (table_widths, state)| {
 898                            parent.child(state.update(cx, |state, cx| {
 899                                let resizable_columns = table_widths.resizable;
 900                                let column_widths = table_widths.lengths(cx);
 901                                let columns = table_widths.current.clone();
 902                                let initial_sizes = table_widths.initial;
 903                                state.render_resize_handles(
 904                                    &column_widths,
 905                                    &resizable_columns,
 906                                    initial_sizes,
 907                                    columns,
 908                                    window,
 909                                    cx,
 910                                )
 911                            }))
 912                        },
 913                    );
 914
 915                if let Some(state) = interaction_state.as_ref() {
 916                    let scrollbars = state
 917                        .read(cx)
 918                        .custom_scrollbar
 919                        .clone()
 920                        .unwrap_or_else(|| Scrollbars::new(super::ScrollAxes::Both));
 921                    content
 922                        .custom_scrollbars(
 923                            scrollbars.tracked_scroll_handle(&state.read(cx).scroll_handle),
 924                            window,
 925                            cx,
 926                        )
 927                        .into_any_element()
 928                } else {
 929                    content.into_any_element()
 930                }
 931            })
 932            .when_some(
 933                no_rows_rendered
 934                    .then_some(self.empty_table_callback)
 935                    .flatten(),
 936                |this, callback| {
 937                    this.child(
 938                        h_flex()
 939                            .size_full()
 940                            .p_3()
 941                            .items_start()
 942                            .justify_center()
 943                            .child(callback(window, cx)),
 944                    )
 945                },
 946            );
 947
 948        if let Some(interaction_state) = interaction_state.as_ref() {
 949            table
 950                .track_focus(&interaction_state.read(cx).focus_handle)
 951                .id(("table", interaction_state.entity_id()))
 952                .into_any_element()
 953        } else {
 954            table.into_any_element()
 955        }
 956    }
 957}
 958
 959impl Component for Table<3> {
 960    fn scope() -> ComponentScope {
 961        ComponentScope::Layout
 962    }
 963
 964    fn description() -> Option<&'static str> {
 965        Some("A table component for displaying data in rows and columns with optional styling.")
 966    }
 967
 968    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
 969        Some(
 970            v_flex()
 971                .gap_6()
 972                .children(vec![
 973                    example_group_with_title(
 974                        "Basic Tables",
 975                        vec![
 976                            single_example(
 977                                "Simple Table",
 978                                Table::new()
 979                                    .width(px(400.))
 980                                    .header(["Name", "Age", "City"])
 981                                    .row(["Alice", "28", "New York"])
 982                                    .row(["Bob", "32", "San Francisco"])
 983                                    .row(["Charlie", "25", "London"])
 984                                    .into_any_element(),
 985                            ),
 986                            single_example(
 987                                "Two Column Table",
 988                                Table::new()
 989                                    .header(["Category", "Value"])
 990                                    .width(px(300.))
 991                                    .row(["Revenue", "$100,000"])
 992                                    .row(["Expenses", "$75,000"])
 993                                    .row(["Profit", "$25,000"])
 994                                    .into_any_element(),
 995                            ),
 996                        ],
 997                    ),
 998                    example_group_with_title(
 999                        "Styled Tables",
1000                        vec![
1001                            single_example(
1002                                "Default",
1003                                Table::new()
1004                                    .width(px(400.))
1005                                    .header(["Product", "Price", "Stock"])
1006                                    .row(["Laptop", "$999", "In Stock"])
1007                                    .row(["Phone", "$599", "Low Stock"])
1008                                    .row(["Tablet", "$399", "Out of Stock"])
1009                                    .into_any_element(),
1010                            ),
1011                            single_example(
1012                                "Striped",
1013                                Table::new()
1014                                    .width(px(400.))
1015                                    .striped()
1016                                    .header(["Product", "Price", "Stock"])
1017                                    .row(["Laptop", "$999", "In Stock"])
1018                                    .row(["Phone", "$599", "Low Stock"])
1019                                    .row(["Tablet", "$399", "Out of Stock"])
1020                                    .row(["Headphones", "$199", "In Stock"])
1021                                    .into_any_element(),
1022                            ),
1023                        ],
1024                    ),
1025                    example_group_with_title(
1026                        "Mixed Content Table",
1027                        vec![single_example(
1028                            "Table with Elements",
1029                            Table::new()
1030                                .width(px(840.))
1031                                .header(["Status", "Name", "Priority", "Deadline", "Action"])
1032                                .row([
1033                                    Indicator::dot().color(Color::Success).into_any_element(),
1034                                    "Project A".into_any_element(),
1035                                    "High".into_any_element(),
1036                                    "2023-12-31".into_any_element(),
1037                                    Button::new("view_a", "View")
1038                                        .style(ButtonStyle::Filled)
1039                                        .full_width()
1040                                        .into_any_element(),
1041                                ])
1042                                .row([
1043                                    Indicator::dot().color(Color::Warning).into_any_element(),
1044                                    "Project B".into_any_element(),
1045                                    "Medium".into_any_element(),
1046                                    "2024-03-15".into_any_element(),
1047                                    Button::new("view_b", "View")
1048                                        .style(ButtonStyle::Filled)
1049                                        .full_width()
1050                                        .into_any_element(),
1051                                ])
1052                                .row([
1053                                    Indicator::dot().color(Color::Error).into_any_element(),
1054                                    "Project C".into_any_element(),
1055                                    "Low".into_any_element(),
1056                                    "2024-06-30".into_any_element(),
1057                                    Button::new("view_c", "View")
1058                                        .style(ButtonStyle::Filled)
1059                                        .full_width()
1060                                        .into_any_element(),
1061                                ])
1062                                .into_any_element(),
1063                        )],
1064                    ),
1065                ])
1066                .into_any_element(),
1067        )
1068    }
1069}
1070
1071#[cfg(test)]
1072mod test {
1073    use super::*;
1074
1075    fn is_almost_eq(a: &[f32], b: &[f32]) -> bool {
1076        a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6)
1077    }
1078
1079    fn cols_to_str<const COLS: usize>(cols: &[f32; COLS], total_size: f32) -> String {
1080        cols.map(|f| "*".repeat(f32::round(f * total_size) as usize))
1081            .join("|")
1082    }
1083
1084    fn parse_resize_behavior<const COLS: usize>(
1085        input: &str,
1086        total_size: f32,
1087    ) -> [TableResizeBehavior; COLS] {
1088        let mut resize_behavior = [TableResizeBehavior::None; COLS];
1089        let mut max_index = 0;
1090        for (index, col) in input.split('|').enumerate() {
1091            if col.starts_with('X') || col.is_empty() {
1092                resize_behavior[index] = TableResizeBehavior::None;
1093            } else if col.starts_with('*') {
1094                resize_behavior[index] =
1095                    TableResizeBehavior::MinSize(col.len() as f32 / total_size);
1096            } else {
1097                panic!("invalid test input: unrecognized resize behavior: {}", col);
1098            }
1099            max_index = index;
1100        }
1101
1102        if max_index + 1 != COLS {
1103            panic!("invalid test input: too many columns");
1104        }
1105        resize_behavior
1106    }
1107
1108    mod reset_column_size {
1109        use super::*;
1110
1111        fn parse<const COLS: usize>(input: &str) -> ([f32; COLS], f32, Option<usize>) {
1112            let mut widths = [f32::NAN; COLS];
1113            let mut column_index = None;
1114            for (index, col) in input.split('|').enumerate() {
1115                widths[index] = col.len() as f32;
1116                if col.starts_with('X') {
1117                    column_index = Some(index);
1118                }
1119            }
1120
1121            for w in widths {
1122                assert!(w.is_finite(), "incorrect number of columns");
1123            }
1124            let total = widths.iter().sum::<f32>();
1125            for width in &mut widths {
1126                *width /= total;
1127            }
1128            (widths, total, column_index)
1129        }
1130
1131        #[track_caller]
1132        fn check_reset_size<const COLS: usize>(
1133            initial_sizes: &str,
1134            widths: &str,
1135            expected: &str,
1136            resize_behavior: &str,
1137        ) {
1138            let (initial_sizes, total_1, None) = parse::<COLS>(initial_sizes) else {
1139                panic!("invalid test input: initial sizes should not be marked");
1140            };
1141            let (widths, total_2, Some(column_index)) = parse::<COLS>(widths) else {
1142                panic!("invalid test input: widths should be marked");
1143            };
1144            assert_eq!(
1145                total_1, total_2,
1146                "invalid test input: total width not the same {total_1}, {total_2}"
1147            );
1148            let (expected, total_3, None) = parse::<COLS>(expected) else {
1149                panic!("invalid test input: expected should not be marked: {expected:?}");
1150            };
1151            assert_eq!(
1152                total_2, total_3,
1153                "invalid test input: total width not the same"
1154            );
1155            let resize_behavior = parse_resize_behavior::<COLS>(resize_behavior, total_1);
1156            let result = TableColumnWidths::reset_to_initial_size(
1157                column_index,
1158                widths,
1159                initial_sizes,
1160                &resize_behavior,
1161            );
1162            let is_eq = is_almost_eq(&result, &expected);
1163            if !is_eq {
1164                let result_str = cols_to_str(&result, total_1);
1165                let expected_str = cols_to_str(&expected, total_1);
1166                panic!(
1167                    "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}"
1168                );
1169            }
1170        }
1171
1172        macro_rules! check_reset_size {
1173            (columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => {
1174                check_reset_size::<$cols>($initial, $current, $expected, $resizing);
1175            };
1176            ($name:ident, columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => {
1177                #[test]
1178                fn $name() {
1179                    check_reset_size::<$cols>($initial, $current, $expected, $resizing);
1180                }
1181            };
1182        }
1183
1184        check_reset_size!(
1185            basic_right,
1186            columns: 5,
1187            starting: "**|**|**|**|**",
1188            snapshot: "**|**|X|***|**",
1189            expected: "**|**|**|**|**",
1190            minimums: "X|*|*|*|*",
1191        );
1192
1193        check_reset_size!(
1194            basic_left,
1195            columns: 5,
1196            starting: "**|**|**|**|**",
1197            snapshot: "**|**|***|X|**",
1198            expected: "**|**|**|**|**",
1199            minimums: "X|*|*|*|**",
1200        );
1201
1202        check_reset_size!(
1203            squashed_left_reset_col2,
1204            columns: 6,
1205            starting: "*|***|**|**|****|*",
1206            snapshot: "*|*|X|*|*|********",
1207            expected: "*|*|**|*|*|*******",
1208            minimums: "X|*|*|*|*|*",
1209        );
1210
1211        check_reset_size!(
1212            grow_cascading_right,
1213            columns: 6,
1214            starting: "*|***|****|**|***|*",
1215            snapshot: "*|***|X|**|**|*****",
1216            expected: "*|***|****|*|*|****",
1217            minimums: "X|*|*|*|*|*",
1218        );
1219
1220        check_reset_size!(
1221           squashed_right_reset_col4,
1222           columns: 6,
1223           starting: "*|***|**|**|****|*",
1224           snapshot: "*|********|*|*|X|*",
1225           expected: "*|*****|*|*|****|*",
1226           minimums: "X|*|*|*|*|*",
1227        );
1228
1229        check_reset_size!(
1230            reset_col6_right,
1231            columns: 6,
1232            starting: "*|***|**|***|***|**",
1233            snapshot: "*|***|**|***|**|XXX",
1234            expected: "*|***|**|***|***|**",
1235            minimums: "X|*|*|*|*|*",
1236        );
1237
1238        check_reset_size!(
1239            reset_col6_left,
1240            columns: 6,
1241            starting: "*|***|**|***|***|**",
1242            snapshot: "*|***|**|***|****|X",
1243            expected: "*|***|**|***|***|**",
1244            minimums: "X|*|*|*|*|*",
1245        );
1246
1247        check_reset_size!(
1248            last_column_grow_cascading,
1249            columns: 6,
1250            starting: "*|***|**|**|**|***",
1251            snapshot: "*|*******|*|**|*|X",
1252            expected: "*|******|*|*|*|***",
1253            minimums: "X|*|*|*|*|*",
1254        );
1255
1256        check_reset_size!(
1257            goes_left_when_left_has_extreme_diff,
1258            columns: 6,
1259            starting: "*|***|****|**|**|***",
1260            snapshot: "*|********|X|*|**|**",
1261            expected: "*|*****|****|*|**|**",
1262            minimums: "X|*|*|*|*|*",
1263        );
1264
1265        check_reset_size!(
1266            basic_shrink_right,
1267            columns: 6,
1268            starting: "**|**|**|**|**|**",
1269            snapshot: "**|**|XXX|*|**|**",
1270            expected: "**|**|**|**|**|**",
1271            minimums: "X|*|*|*|*|*",
1272        );
1273
1274        check_reset_size!(
1275            shrink_should_go_left,
1276            columns: 6,
1277            starting: "*|***|**|*|*|*",
1278            snapshot: "*|*|XXX|**|*|*",
1279            expected: "*|**|**|**|*|*",
1280            minimums: "X|*|*|*|*|*",
1281        );
1282
1283        check_reset_size!(
1284            shrink_should_go_right,
1285            columns: 6,
1286            starting: "*|***|**|**|**|*",
1287            snapshot: "*|****|XXX|*|*|*",
1288            expected: "*|****|**|**|*|*",
1289            minimums: "X|*|*|*|*|*",
1290        );
1291    }
1292
1293    mod drag_handle {
1294        use super::*;
1295
1296        fn parse<const COLS: usize>(input: &str) -> ([f32; COLS], f32, Option<usize>) {
1297            let mut widths = [f32::NAN; COLS];
1298            let column_index = input.replace("*", "").find("I");
1299            for (index, col) in input.replace("I", "|").split('|').enumerate() {
1300                widths[index] = col.len() as f32;
1301            }
1302
1303            for w in widths {
1304                assert!(w.is_finite(), "incorrect number of columns");
1305            }
1306            let total = widths.iter().sum::<f32>();
1307            for width in &mut widths {
1308                *width /= total;
1309            }
1310            (widths, total, column_index)
1311        }
1312
1313        #[track_caller]
1314        fn check<const COLS: usize>(
1315            distance: i32,
1316            widths: &str,
1317            expected: &str,
1318            resize_behavior: &str,
1319        ) {
1320            let (mut widths, total_1, Some(column_index)) = parse::<COLS>(widths) else {
1321                panic!("invalid test input: widths should be marked");
1322            };
1323            let (expected, total_2, None) = parse::<COLS>(expected) else {
1324                panic!("invalid test input: expected should not be marked: {expected:?}");
1325            };
1326            assert_eq!(
1327                total_1, total_2,
1328                "invalid test input: total width not the same"
1329            );
1330            let resize_behavior = parse_resize_behavior::<COLS>(resize_behavior, total_1);
1331
1332            let distance = distance as f32 / total_1;
1333
1334            let result = TableColumnWidths::drag_column_handle(
1335                distance,
1336                column_index,
1337                &mut widths,
1338                &resize_behavior,
1339            );
1340
1341            let is_eq = is_almost_eq(&widths, &expected);
1342            if !is_eq {
1343                let result_str = cols_to_str(&widths, total_1);
1344                let expected_str = cols_to_str(&expected, total_1);
1345                panic!(
1346                    "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}"
1347                );
1348            }
1349        }
1350
1351        macro_rules! check {
1352            (columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => {
1353                check!($cols, $dist, $snapshot, $expected, $resizing);
1354            };
1355            ($name:ident, columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => {
1356                #[test]
1357                fn $name() {
1358                    check::<$cols>($dist, $current, $expected, $resizing);
1359                }
1360            };
1361        }
1362
1363        check!(
1364            basic_right_drag,
1365            columns: 3,
1366            distance: 1,
1367            snapshot: "**|**I**",
1368            expected: "**|***|*",
1369            minimums: "X|*|*",
1370        );
1371
1372        check!(
1373            drag_left_against_mins,
1374            columns: 5,
1375            distance: -1,
1376            snapshot: "*|*|*|*I*******",
1377            expected: "*|*|*|*|*******",
1378            minimums: "X|*|*|*|*",
1379        );
1380
1381        check!(
1382            drag_left,
1383            columns: 5,
1384            distance: -2,
1385            snapshot: "*|*|*|*****I***",
1386            expected: "*|*|*|***|*****",
1387            minimums: "X|*|*|*|*",
1388        );
1389    }
1390}