table.rs

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