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, ListState, Point,
   6    Stateful, UniformListScrollHandle, WeakEntity, list, 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    ScrollAxes, ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled,
  14    StyledExt as _, StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex,
  15    px, single_example,
  16    table_row::{IntoTableRow as _, TableRow},
  17    v_flex,
  18};
  19use itertools::intersperse_with;
  20
  21pub mod table_row {
  22    //! A newtype for a table row that enforces a fixed column count at runtime.
  23    //!
  24    //! This type ensures that all rows in a table have the same width, preventing accidental creation or mutation of rows with inconsistent lengths.
  25    //! It is especially useful for CSV or tabular data where rectangular invariants must be maintained, but the number of columns is only known at runtime.
  26    //! By using `TableRow`, we gain stronger guarantees and safer APIs compared to a bare `Vec<T>`, without requiring const generics.
  27
  28    use std::{
  29        any::type_name,
  30        ops::{
  31            Index, IndexMut, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive,
  32        },
  33    };
  34
  35    #[derive(Clone, Debug, PartialEq, Eq)]
  36    pub struct TableRow<T>(Vec<T>);
  37
  38    impl<T> TableRow<T> {
  39        /// Constructs a `TableRow` from a `Vec<T>`, panicking if the length does not match `expected_length`.
  40        ///
  41        /// Use this when you want to ensure at construction time that the row has the correct number of columns.
  42        /// This enforces the rectangular invariant for table data, preventing accidental creation of malformed rows.
  43        ///
  44        /// # Panics
  45        /// Panics if `data.len() != expected_length`.
  46        pub fn from_vec(data: Vec<T>, expected_length: usize) -> Self {
  47            Self::try_from_vec(data, expected_length).unwrap_or_else(|e| {
  48                let name = type_name::<Vec<T>>();
  49                panic!("Expected {name} to be created successfully: {e}");
  50            })
  51        }
  52
  53        /// Attempts to construct a `TableRow` from a `Vec<T>`, returning an error if the length does not match `expected_len`.
  54        ///
  55        /// This is a fallible alternative to `from_vec`, allowing you to handle inconsistent row lengths gracefully.
  56        /// Returns `Ok(TableRow)` if the length matches, or an `Err` with a descriptive message otherwise.
  57        pub fn try_from_vec(data: Vec<T>, expected_len: usize) -> Result<Self, String> {
  58            if data.len() != expected_len {
  59                Err(format!(
  60                    "Row length {} does not match expected {}",
  61                    data.len(),
  62                    expected_len
  63                ))
  64            } else {
  65                Ok(Self(data))
  66            }
  67        }
  68
  69        /// Returns reference to element by column index.
  70        ///
  71        /// # Panics
  72        /// Panics if `col` is out of bounds (i.e., `col >= self.cols()`).
  73        pub fn expect_get(&self, col: usize) -> &T {
  74            self.0.get(col).unwrap_or_else(|| {
  75                panic!(
  76                    "Expected table row of `{}` to have {col:?}",
  77                    type_name::<T>()
  78                )
  79            })
  80        }
  81
  82        pub fn get(&self, col: usize) -> Option<&T> {
  83            self.0.get(col)
  84        }
  85
  86        pub fn as_slice(&self) -> &[T] {
  87            &self.0
  88        }
  89
  90        pub fn into_vec(self) -> Vec<T> {
  91            self.0
  92        }
  93
  94        /// Like [`map`], but borrows the row and clones each element before mapping.
  95        ///
  96        /// This is useful when you want to map over a borrowed row without consuming it,
  97        /// but your mapping function requires ownership of each element.
  98        ///
  99        /// # Difference
 100        /// - `map_cloned` takes `&self`, clones each element, and applies `f(T) -> U`.
 101        /// - [`map`] takes `self` by value and applies `f(T) -> U` directly, consuming the row.
 102        /// - [`map_ref`] takes `&self` and applies `f(&T) -> U` to references of each element.
 103        pub fn map_cloned<F, U>(&self, f: F) -> TableRow<U>
 104        where
 105            F: FnMut(T) -> U,
 106            T: Clone,
 107        {
 108            self.clone().map(f)
 109        }
 110
 111        /// Consumes the row and transforms all elements within it in a length-safe way.
 112        ///
 113        /// # Difference
 114        /// - `map` takes ownership of the row (`self`) and applies `f(T) -> U` to each element.
 115        /// - Use this when you want to transform and consume the row in one step.
 116        /// - See also [`map_cloned`] (for mapping over a borrowed row with cloning) and [`map_ref`] (for mapping over references).
 117        pub fn map<F, U>(self, f: F) -> TableRow<U>
 118        where
 119            F: FnMut(T) -> U,
 120        {
 121            TableRow(self.0.into_iter().map(f).collect())
 122        }
 123
 124        /// Borrows the row and transforms all elements by reference in a length-safe way.
 125        ///
 126        /// # Difference
 127        /// - `map_ref` takes `&self` and applies `f(&T) -> U` to each element by reference.
 128        /// - Use this when you want to map over a borrowed row without cloning or consuming it.
 129        /// - See also [`map`] (for consuming the row) and [`map_cloned`] (for mapping with cloning).
 130        pub fn map_ref<F, U>(&self, f: F) -> TableRow<U>
 131        where
 132            F: FnMut(&T) -> U,
 133        {
 134            TableRow(self.0.iter().map(f).collect())
 135        }
 136
 137        /// Number of columns (alias to `len()` with more semantic meaning)
 138        pub fn cols(&self) -> usize {
 139            self.0.len()
 140        }
 141    }
 142
 143    ///// Convenience traits /////
 144    pub trait IntoTableRow<T> {
 145        fn into_table_row(self, expected_length: usize) -> TableRow<T>;
 146    }
 147    impl<T> IntoTableRow<T> for Vec<T> {
 148        fn into_table_row(self, expected_length: usize) -> TableRow<T> {
 149            TableRow::from_vec(self, expected_length)
 150        }
 151    }
 152
 153    // Index implementations for convenient access
 154    impl<T> Index<usize> for TableRow<T> {
 155        type Output = T;
 156
 157        fn index(&self, index: usize) -> &Self::Output {
 158            &self.0[index]
 159        }
 160    }
 161
 162    impl<T> IndexMut<usize> for TableRow<T> {
 163        fn index_mut(&mut self, index: usize) -> &mut Self::Output {
 164            &mut self.0[index]
 165        }
 166    }
 167
 168    // Range indexing implementations for slice operations
 169    impl<T> Index<Range<usize>> for TableRow<T> {
 170        type Output = [T];
 171
 172        fn index(&self, index: Range<usize>) -> &Self::Output {
 173            <Vec<T> as Index<Range<usize>>>::index(&self.0, index)
 174        }
 175    }
 176
 177    impl<T> Index<RangeFrom<usize>> for TableRow<T> {
 178        type Output = [T];
 179
 180        fn index(&self, index: RangeFrom<usize>) -> &Self::Output {
 181            <Vec<T> as Index<RangeFrom<usize>>>::index(&self.0, index)
 182        }
 183    }
 184
 185    impl<T> Index<RangeTo<usize>> for TableRow<T> {
 186        type Output = [T];
 187
 188        fn index(&self, index: RangeTo<usize>) -> &Self::Output {
 189            <Vec<T> as Index<RangeTo<usize>>>::index(&self.0, index)
 190        }
 191    }
 192
 193    impl<T> Index<RangeToInclusive<usize>> for TableRow<T> {
 194        type Output = [T];
 195
 196        fn index(&self, index: RangeToInclusive<usize>) -> &Self::Output {
 197            <Vec<T> as Index<RangeToInclusive<usize>>>::index(&self.0, index)
 198        }
 199    }
 200
 201    impl<T> Index<RangeFull> for TableRow<T> {
 202        type Output = [T];
 203
 204        fn index(&self, index: RangeFull) -> &Self::Output {
 205            <Vec<T> as Index<RangeFull>>::index(&self.0, index)
 206        }
 207    }
 208
 209    impl<T> Index<RangeInclusive<usize>> for TableRow<T> {
 210        type Output = [T];
 211
 212        fn index(&self, index: RangeInclusive<usize>) -> &Self::Output {
 213            <Vec<T> as Index<RangeInclusive<usize>>>::index(&self.0, index)
 214        }
 215    }
 216
 217    impl<T> IndexMut<RangeInclusive<usize>> for TableRow<T> {
 218        fn index_mut(&mut self, index: RangeInclusive<usize>) -> &mut Self::Output {
 219            <Vec<T> as IndexMut<RangeInclusive<usize>>>::index_mut(&mut self.0, index)
 220        }
 221    }
 222}
 223
 224const RESIZE_COLUMN_WIDTH: f32 = 8.0;
 225
 226/// Represents an unchecked table row, which is a vector of elements.
 227/// Will be converted into `TableRow<T>` internally
 228pub type UncheckedTableRow<T> = Vec<T>;
 229
 230#[derive(Debug)]
 231struct DraggedColumn(usize);
 232
 233struct UniformListData {
 234    render_list_of_rows_fn:
 235        Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<UncheckedTableRow<AnyElement>>>,
 236    element_id: ElementId,
 237    row_count: usize,
 238}
 239
 240struct VariableRowHeightListData {
 241    /// Unlike UniformList, this closure renders only single row, allowing each one to have its own height
 242    render_row_fn: Box<dyn Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement>>,
 243    list_state: ListState,
 244    row_count: usize,
 245}
 246
 247enum TableContents {
 248    Vec(Vec<TableRow<AnyElement>>),
 249    UniformList(UniformListData),
 250    VariableRowHeightList(VariableRowHeightListData),
 251}
 252
 253impl TableContents {
 254    fn rows_mut(&mut self) -> Option<&mut Vec<TableRow<AnyElement>>> {
 255        match self {
 256            TableContents::Vec(rows) => Some(rows),
 257            TableContents::UniformList(_) => None,
 258            TableContents::VariableRowHeightList(_) => None,
 259        }
 260    }
 261
 262    fn len(&self) -> usize {
 263        match self {
 264            TableContents::Vec(rows) => rows.len(),
 265            TableContents::UniformList(data) => data.row_count,
 266            TableContents::VariableRowHeightList(data) => data.row_count,
 267        }
 268    }
 269
 270    fn is_empty(&self) -> bool {
 271        self.len() == 0
 272    }
 273}
 274
 275pub struct TableInteractionState {
 276    pub focus_handle: FocusHandle,
 277    pub scroll_handle: UniformListScrollHandle,
 278    pub custom_scrollbar: Option<Scrollbars>,
 279}
 280
 281impl TableInteractionState {
 282    pub fn new(cx: &mut App) -> Self {
 283        Self {
 284            focus_handle: cx.focus_handle(),
 285            scroll_handle: UniformListScrollHandle::new(),
 286            custom_scrollbar: None,
 287        }
 288    }
 289
 290    pub fn with_custom_scrollbar(mut self, custom_scrollbar: Scrollbars) -> Self {
 291        self.custom_scrollbar = Some(custom_scrollbar);
 292        self
 293    }
 294
 295    pub fn scroll_offset(&self) -> Point<Pixels> {
 296        self.scroll_handle.offset()
 297    }
 298
 299    pub fn set_scroll_offset(&self, offset: Point<Pixels>) {
 300        self.scroll_handle.set_offset(offset);
 301    }
 302
 303    pub fn listener<E: ?Sized>(
 304        this: &Entity<Self>,
 305        f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static,
 306    ) -> impl Fn(&E, &mut Window, &mut App) + 'static {
 307        let view = this.downgrade();
 308        move |e: &E, window: &mut Window, cx: &mut App| {
 309            view.update(cx, |view, cx| f(view, e, window, cx)).ok();
 310        }
 311    }
 312
 313    /// Renders invisible resize handles overlaid on top of table content.
 314    ///
 315    /// - Spacer: invisible element that matches the width of table column content
 316    /// - Divider: contains the actual resize handle that users can drag to resize columns
 317    ///
 318    /// Structure: [spacer] [divider] [spacer] [divider] [spacer]
 319    ///
 320    /// Business logic:
 321    /// 1. Creates spacers matching each column width
 322    /// 2. Intersperses (inserts) resize handles between spacers (interactive only for resizable columns)
 323    /// 3. Each handle supports hover highlighting, double-click to reset, and drag to resize
 324    /// 4. Returns an absolute-positioned overlay that sits on top of table content
 325    fn render_resize_handles(
 326        &self,
 327        column_widths: &TableRow<Length>,
 328        resizable_columns: &TableRow<TableResizeBehavior>,
 329        initial_sizes: &TableRow<DefiniteLength>,
 330        columns: Option<Entity<TableColumnWidths>>,
 331        window: &mut Window,
 332        cx: &mut App,
 333    ) -> AnyElement {
 334        let spacers = column_widths
 335            .as_slice()
 336            .iter()
 337            .map(|width| base_cell_style(Some(*width)).into_any_element());
 338
 339        let mut column_ix = 0;
 340        let resizable_columns_shared = Rc::new(resizable_columns.clone());
 341        let initial_sizes_shared = Rc::new(initial_sizes.clone());
 342        let mut resizable_columns_iter = resizable_columns.as_slice().iter();
 343
 344        // Insert dividers between spacers (column content)
 345        let dividers = intersperse_with(spacers, || {
 346            let resizable_columns = Rc::clone(&resizable_columns_shared);
 347            let initial_sizes = Rc::clone(&initial_sizes_shared);
 348            window.with_id(column_ix, |window| {
 349                let mut resize_divider = div()
 350                    // This is required because this is evaluated at a different time than the use_state call above
 351                    .id(column_ix)
 352                    .relative()
 353                    .top_0()
 354                    .w_px()
 355                    .h_full()
 356                    .bg(cx.theme().colors().border.opacity(0.8));
 357
 358                let mut resize_handle = div()
 359                    .id("column-resize-handle")
 360                    .absolute()
 361                    .left_neg_0p5()
 362                    .w(px(RESIZE_COLUMN_WIDTH))
 363                    .h_full();
 364
 365                if resizable_columns_iter
 366                    .next()
 367                    .is_some_and(TableResizeBehavior::is_resizable)
 368                {
 369                    let hovered = window.use_state(cx, |_window, _cx| false);
 370
 371                    resize_divider = resize_divider.when(*hovered.read(cx), |div| {
 372                        div.bg(cx.theme().colors().border_focused)
 373                    });
 374
 375                    resize_handle = resize_handle
 376                        .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered))
 377                        .cursor_col_resize()
 378                        .when_some(columns.clone(), |this, columns| {
 379                            this.on_click(move |event, window, cx| {
 380                                if event.click_count() >= 2 {
 381                                    columns.update(cx, |columns, _| {
 382                                        columns.on_double_click(
 383                                            column_ix,
 384                                            &initial_sizes,
 385                                            &resizable_columns,
 386                                            window,
 387                                        );
 388                                    })
 389                                }
 390
 391                                cx.stop_propagation();
 392                            })
 393                        })
 394                        .on_drag(DraggedColumn(column_ix), |_, _offset, _window, cx| {
 395                            cx.new(|_cx| gpui::Empty)
 396                        })
 397                }
 398
 399                column_ix += 1;
 400                resize_divider.child(resize_handle).into_any_element()
 401            })
 402        });
 403
 404        h_flex()
 405            .id("resize-handles")
 406            .absolute()
 407            .inset_0()
 408            .w_full()
 409            .children(dividers)
 410            .into_any_element()
 411    }
 412}
 413
 414#[derive(Debug, Copy, Clone, PartialEq)]
 415pub enum TableResizeBehavior {
 416    None,
 417    Resizable,
 418    MinSize(f32),
 419}
 420
 421impl TableResizeBehavior {
 422    pub fn is_resizable(&self) -> bool {
 423        *self != TableResizeBehavior::None
 424    }
 425
 426    pub fn min_size(&self) -> Option<f32> {
 427        match self {
 428            TableResizeBehavior::None => None,
 429            TableResizeBehavior::Resizable => Some(0.05),
 430            TableResizeBehavior::MinSize(min_size) => Some(*min_size),
 431        }
 432    }
 433}
 434
 435pub struct TableColumnWidths {
 436    widths: TableRow<DefiniteLength>,
 437    visible_widths: TableRow<DefiniteLength>,
 438    cached_bounds_width: Pixels,
 439    initialized: bool,
 440}
 441
 442impl TableColumnWidths {
 443    pub fn new(cols: usize, _: &mut App) -> Self {
 444        Self {
 445            widths: vec![DefiniteLength::default(); cols].into_table_row(cols),
 446            visible_widths: vec![DefiniteLength::default(); cols].into_table_row(cols),
 447            cached_bounds_width: Default::default(),
 448            initialized: false,
 449        }
 450    }
 451
 452    pub fn cols(&self) -> usize {
 453        self.widths.cols()
 454    }
 455
 456    fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 {
 457        match length {
 458            DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => *pixels / bounds_width,
 459            DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => {
 460                rems_width.to_pixels(rem_size) / bounds_width
 461            }
 462            DefiniteLength::Fraction(fraction) => *fraction,
 463        }
 464    }
 465
 466    fn on_double_click(
 467        &mut self,
 468        double_click_position: usize,
 469        initial_sizes: &TableRow<DefiniteLength>,
 470        resize_behavior: &TableRow<TableResizeBehavior>,
 471        window: &mut Window,
 472    ) {
 473        let bounds_width = self.cached_bounds_width;
 474        let rem_size = window.rem_size();
 475        let initial_sizes =
 476            initial_sizes.map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
 477        let widths = self
 478            .widths
 479            .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
 480
 481        let updated_widths = Self::reset_to_initial_size(
 482            double_click_position,
 483            widths,
 484            initial_sizes,
 485            resize_behavior,
 486        );
 487        self.widths = updated_widths.map(DefiniteLength::Fraction);
 488        self.visible_widths = self.widths.clone(); // previously was copy
 489    }
 490
 491    fn reset_to_initial_size(
 492        col_idx: usize,
 493        mut widths: TableRow<f32>,
 494        initial_sizes: TableRow<f32>,
 495        resize_behavior: &TableRow<TableResizeBehavior>,
 496    ) -> TableRow<f32> {
 497        // RESET:
 498        // Part 1:
 499        // Figure out if we should shrink/grow the selected column
 500        // Get diff which represents the change in column we want to make initial size delta curr_size = diff
 501        //
 502        // Part 2: We need to decide which side column we should move and where
 503        //
 504        // If we want to grow our column we should check the left/right columns diff to see what side
 505        // has a greater delta than their initial size. Likewise, if we shrink our column we should check
 506        // the left/right column diffs to see what side has the smallest delta.
 507        //
 508        // Part 3: resize
 509        //
 510        // col_idx represents the column handle to the right of an active column
 511        //
 512        // If growing and right has the greater delta {
 513        //    shift col_idx to the right
 514        // } else if growing and left has the greater delta {
 515        //  shift col_idx - 1 to the left
 516        // } else if shrinking and the right has the greater delta {
 517        //  shift
 518        // } {
 519        //
 520        // }
 521        // }
 522        //
 523        // if we need to shrink, then if the right
 524        //
 525
 526        // DRAGGING
 527        // we get diff which represents the change in the _drag handle_ position
 528        // -diff => dragging left ->
 529        //      grow the column to the right of the handle as much as we can shrink columns to the left of the handle
 530        // +diff => dragging right -> growing handles column
 531        //      grow the column to the left of the handle as much as we can shrink columns to the right of the handle
 532        //
 533
 534        let diff = initial_sizes[col_idx] - widths[col_idx];
 535
 536        let left_diff =
 537            initial_sizes[..col_idx].iter().sum::<f32>() - widths[..col_idx].iter().sum::<f32>();
 538        let right_diff = initial_sizes[col_idx + 1..].iter().sum::<f32>()
 539            - widths[col_idx + 1..].iter().sum::<f32>();
 540
 541        let go_left_first = if diff < 0.0 {
 542            left_diff > right_diff
 543        } else {
 544            left_diff < right_diff
 545        };
 546
 547        if !go_left_first {
 548            let diff_remaining =
 549                Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, 1);
 550
 551            if diff_remaining != 0.0 && col_idx > 0 {
 552                Self::propagate_resize_diff(
 553                    diff_remaining,
 554                    col_idx,
 555                    &mut widths,
 556                    resize_behavior,
 557                    -1,
 558                );
 559            }
 560        } else {
 561            let diff_remaining =
 562                Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, -1);
 563
 564            if diff_remaining != 0.0 {
 565                Self::propagate_resize_diff(
 566                    diff_remaining,
 567                    col_idx,
 568                    &mut widths,
 569                    resize_behavior,
 570                    1,
 571                );
 572            }
 573        }
 574
 575        widths
 576    }
 577
 578    fn on_drag_move(
 579        &mut self,
 580        drag_event: &DragMoveEvent<DraggedColumn>,
 581        resize_behavior: &TableRow<TableResizeBehavior>,
 582        window: &mut Window,
 583        cx: &mut Context<Self>,
 584    ) {
 585        let drag_position = drag_event.event.position;
 586        let bounds = drag_event.bounds;
 587
 588        let mut col_position = 0.0;
 589        let rem_size = window.rem_size();
 590        let bounds_width = bounds.right() - bounds.left();
 591        let col_idx = drag_event.drag(cx).0;
 592
 593        let column_handle_width = Self::get_fraction(
 594            &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_COLUMN_WIDTH))),
 595            bounds_width,
 596            rem_size,
 597        );
 598
 599        let mut widths = self
 600            .widths
 601            .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size));
 602
 603        for length in widths[0..=col_idx].iter() {
 604            col_position += length + column_handle_width;
 605        }
 606
 607        let mut total_length_ratio = col_position;
 608        for length in widths[col_idx + 1..].iter() {
 609            total_length_ratio += length;
 610        }
 611        let cols = resize_behavior.cols();
 612        total_length_ratio += (cols - 1 - col_idx) as f32 * column_handle_width;
 613
 614        let drag_fraction = (drag_position.x - bounds.left()) / bounds_width;
 615        let drag_fraction = drag_fraction * total_length_ratio;
 616        let diff = drag_fraction - col_position - column_handle_width / 2.0;
 617
 618        Self::drag_column_handle(diff, col_idx, &mut widths, resize_behavior);
 619
 620        self.visible_widths = widths.map(DefiniteLength::Fraction);
 621    }
 622
 623    fn drag_column_handle(
 624        diff: f32,
 625        col_idx: usize,
 626        widths: &mut TableRow<f32>,
 627        resize_behavior: &TableRow<TableResizeBehavior>,
 628    ) {
 629        // if diff > 0.0 then go right
 630        if diff > 0.0 {
 631            Self::propagate_resize_diff(diff, col_idx, widths, resize_behavior, 1);
 632        } else {
 633            Self::propagate_resize_diff(-diff, col_idx + 1, widths, resize_behavior, -1);
 634        }
 635    }
 636
 637    fn propagate_resize_diff(
 638        diff: f32,
 639        col_idx: usize,
 640        widths: &mut TableRow<f32>,
 641        resize_behavior: &TableRow<TableResizeBehavior>,
 642        direction: i8,
 643    ) -> f32 {
 644        let mut diff_remaining = diff;
 645        if resize_behavior[col_idx].min_size().is_none() {
 646            return diff;
 647        }
 648
 649        let step_right;
 650        let step_left;
 651        if direction < 0 {
 652            step_right = 0;
 653            step_left = 1;
 654        } else {
 655            step_right = 1;
 656            step_left = 0;
 657        }
 658        if col_idx == 0 && direction < 0 {
 659            return diff;
 660        }
 661        let mut curr_column = col_idx + step_right - step_left;
 662
 663        while diff_remaining != 0.0 && curr_column < widths.cols() {
 664            let Some(min_size) = resize_behavior[curr_column].min_size() else {
 665                if curr_column == 0 {
 666                    break;
 667                }
 668                curr_column -= step_left;
 669                curr_column += step_right;
 670                continue;
 671            };
 672
 673            let curr_width = widths[curr_column] - diff_remaining;
 674            widths[curr_column] = curr_width;
 675
 676            if min_size > curr_width {
 677                diff_remaining = min_size - curr_width;
 678                widths[curr_column] = min_size;
 679            } else {
 680                diff_remaining = 0.0;
 681                break;
 682            }
 683            if curr_column == 0 {
 684                break;
 685            }
 686            curr_column -= step_left;
 687            curr_column += step_right;
 688        }
 689        widths[col_idx] = widths[col_idx] + (diff - diff_remaining);
 690
 691        diff_remaining
 692    }
 693}
 694
 695pub struct TableWidths {
 696    initial: TableRow<DefiniteLength>,
 697    current: Option<Entity<TableColumnWidths>>,
 698    resizable: TableRow<TableResizeBehavior>,
 699}
 700
 701impl TableWidths {
 702    pub fn new(widths: TableRow<impl Into<DefiniteLength>>) -> Self {
 703        let widths = widths.map(Into::into);
 704
 705        let expected_length = widths.cols();
 706        TableWidths {
 707            initial: widths,
 708            current: None,
 709            resizable: vec![TableResizeBehavior::None; expected_length]
 710                .into_table_row(expected_length),
 711        }
 712    }
 713
 714    fn lengths(&self, cx: &App) -> TableRow<Length> {
 715        self.current
 716            .as_ref()
 717            .map(|entity| entity.read(cx).visible_widths.map_cloned(Length::Definite))
 718            .unwrap_or_else(|| self.initial.map_cloned(Length::Definite))
 719    }
 720}
 721
 722/// A table component
 723#[derive(RegisterComponent, IntoElement)]
 724pub struct Table {
 725    striped: bool,
 726    show_row_borders: bool,
 727    show_row_hover: bool,
 728    width: Option<Length>,
 729    headers: Option<TableRow<AnyElement>>,
 730    rows: TableContents,
 731    interaction_state: Option<WeakEntity<TableInteractionState>>,
 732    col_widths: Option<TableWidths>,
 733    map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
 734    use_ui_font: bool,
 735    empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
 736    /// The number of columns in the table. Used to assert column numbers in `TableRow` collections
 737    cols: usize,
 738}
 739
 740impl Table {
 741    /// Creates a new table with the specified number of columns.
 742    pub fn new(cols: usize) -> Self {
 743        Self {
 744            cols,
 745            striped: false,
 746            show_row_borders: true,
 747            show_row_hover: true,
 748            width: None,
 749            headers: None,
 750            rows: TableContents::Vec(Vec::new()),
 751            interaction_state: None,
 752            map_row: None,
 753            use_ui_font: true,
 754            empty_table_callback: None,
 755            col_widths: None,
 756        }
 757    }
 758
 759    /// Enables uniform list rendering.
 760    /// The provided function will be passed directly to the `uniform_list` element.
 761    /// Therefore, if this method is called, any calls to [`Table::row`] before or after
 762    /// this method is called will be ignored.
 763    pub fn uniform_list(
 764        mut self,
 765        id: impl Into<ElementId>,
 766        row_count: usize,
 767        render_item_fn: impl Fn(
 768            Range<usize>,
 769            &mut Window,
 770            &mut App,
 771        ) -> Vec<UncheckedTableRow<AnyElement>>
 772        + 'static,
 773    ) -> Self {
 774        self.rows = TableContents::UniformList(UniformListData {
 775            element_id: id.into(),
 776            row_count,
 777            render_list_of_rows_fn: Box::new(render_item_fn),
 778        });
 779        self
 780    }
 781
 782    /// Enables rendering of tables with variable row heights, allowing each row to have its own height.
 783    ///
 784    /// This mode is useful for displaying content such as CSV data or multiline cells, where rows may not have uniform heights.
 785    /// It is generally slower than [`Table::uniform_list`] due to the need to measure each row individually, but it provides correct layout for non-uniform or multiline content.
 786    ///
 787    /// # Parameters
 788    /// - `row_count`: The total number of rows in the table.
 789    /// - `list_state`: The [`ListState`] used for managing scroll position and virtualization. This must be initialized and managed by the caller, and should be kept in sync with the number of rows.
 790    /// - `render_row_fn`: A closure that renders a single row, given the row index, a mutable reference to [`Window`], and a mutable reference to [`App`]. It should return an array of [`AnyElement`]s, one for each column.
 791    pub fn variable_row_height_list(
 792        mut self,
 793        row_count: usize,
 794        list_state: ListState,
 795        render_row_fn: impl Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement> + 'static,
 796    ) -> Self {
 797        self.rows = TableContents::VariableRowHeightList(VariableRowHeightListData {
 798            render_row_fn: Box::new(render_row_fn),
 799            list_state,
 800            row_count,
 801        });
 802        self
 803    }
 804
 805    /// Enables row striping (alternating row colors)
 806    pub fn striped(mut self) -> Self {
 807        self.striped = true;
 808        self
 809    }
 810
 811    /// Hides the border lines between rows
 812    pub fn hide_row_borders(mut self) -> Self {
 813        self.show_row_borders = false;
 814        self
 815    }
 816
 817    /// Sets the width of the table.
 818    /// Will enable horizontal scrolling if [`Self::interactable`] is also called.
 819    pub fn width(mut self, width: impl Into<Length>) -> Self {
 820        self.width = Some(width.into());
 821        self
 822    }
 823
 824    /// Enables interaction (primarily scrolling) with the table.
 825    ///
 826    /// Vertical scrolling will be enabled by default if the table is taller than its container.
 827    ///
 828    /// Horizontal scrolling will only be enabled if [`Self::width`] is also called, otherwise
 829    /// the list will always shrink the table columns to fit their contents I.e. If [`Self::uniform_list`]
 830    /// is used without a width and with [`Self::interactable`], the [`ListHorizontalSizingBehavior`] will
 831    /// be set to [`ListHorizontalSizingBehavior::FitList`].
 832    pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
 833        self.interaction_state = Some(interaction_state.downgrade());
 834        self
 835    }
 836
 837    pub fn header(mut self, headers: UncheckedTableRow<impl IntoElement>) -> Self {
 838        self.headers = Some(
 839            headers
 840                .into_table_row(self.cols)
 841                .map(IntoElement::into_any_element),
 842        );
 843        self
 844    }
 845
 846    pub fn row(mut self, items: UncheckedTableRow<impl IntoElement>) -> Self {
 847        if let Some(rows) = self.rows.rows_mut() {
 848            rows.push(
 849                items
 850                    .into_table_row(self.cols)
 851                    .map(IntoElement::into_any_element),
 852            );
 853        }
 854        self
 855    }
 856
 857    pub fn column_widths(mut self, widths: UncheckedTableRow<impl Into<DefiniteLength>>) -> Self {
 858        if self.col_widths.is_none() {
 859            self.col_widths = Some(TableWidths::new(widths.into_table_row(self.cols)));
 860        }
 861        self
 862    }
 863
 864    pub fn resizable_columns(
 865        mut self,
 866        resizable: UncheckedTableRow<TableResizeBehavior>,
 867        column_widths: &Entity<TableColumnWidths>,
 868        cx: &mut App,
 869    ) -> Self {
 870        if let Some(table_widths) = self.col_widths.as_mut() {
 871            table_widths.resizable = resizable.into_table_row(self.cols);
 872            let column_widths = table_widths
 873                .current
 874                .get_or_insert_with(|| column_widths.clone());
 875
 876            column_widths.update(cx, |widths, _| {
 877                if !widths.initialized {
 878                    widths.initialized = true;
 879                    widths.widths = table_widths.initial.clone();
 880                    widths.visible_widths = widths.widths.clone();
 881                }
 882            })
 883        }
 884        self
 885    }
 886
 887    pub fn no_ui_font(mut self) -> Self {
 888        self.use_ui_font = false;
 889        self
 890    }
 891
 892    pub fn map_row(
 893        mut self,
 894        callback: impl Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement + 'static,
 895    ) -> Self {
 896        self.map_row = Some(Rc::new(callback));
 897        self
 898    }
 899
 900    /// Hides the default hover background on table rows.
 901    /// Use this when you want to handle row hover styling manually via `map_row`.
 902    pub fn hide_row_hover(mut self) -> Self {
 903        self.show_row_hover = false;
 904        self
 905    }
 906
 907    /// Provide a callback that is invoked when the table is rendered without any rows
 908    pub fn empty_table_callback(
 909        mut self,
 910        callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
 911    ) -> Self {
 912        self.empty_table_callback = Some(Rc::new(callback));
 913        self
 914    }
 915}
 916
 917fn base_cell_style(width: Option<Length>) -> Div {
 918    div()
 919        .px_1p5()
 920        .when_some(width, |this, width| this.w(width))
 921        .when(width.is_none(), |this| this.flex_1())
 922        .whitespace_nowrap()
 923        .text_ellipsis()
 924        .overflow_hidden()
 925}
 926
 927fn base_cell_style_text(width: Option<Length>, use_ui_font: bool, cx: &App) -> Div {
 928    base_cell_style(width).when(use_ui_font, |el| el.text_ui(cx))
 929}
 930
 931pub fn render_table_row(
 932    row_index: usize,
 933    items: TableRow<impl IntoElement>,
 934    table_context: TableRenderContext,
 935    window: &mut Window,
 936    cx: &mut App,
 937) -> AnyElement {
 938    let is_striped = table_context.striped;
 939    let is_last = row_index == table_context.total_row_count - 1;
 940    let bg = if row_index % 2 == 1 && is_striped {
 941        Some(cx.theme().colors().text.opacity(0.05))
 942    } else {
 943        None
 944    };
 945    let cols = items.cols();
 946    let column_widths = table_context
 947        .column_widths
 948        .map_or(vec![None; cols].into_table_row(cols), |widths| {
 949            widths.map(Some)
 950        });
 951
 952    let mut row = div()
 953        // NOTE: `h_flex()` sneakily applies `items_center()` which is not default behavior for div element.
 954        // Applying `.flex().flex_row()` manually to overcome that
 955        .flex()
 956        .flex_row()
 957        .id(("table_row", row_index))
 958        .size_full()
 959        .when_some(bg, |row, bg| row.bg(bg))
 960        .when(table_context.show_row_hover, |row| {
 961            row.hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.6)))
 962        })
 963        .when(!is_striped && table_context.show_row_borders, |row| {
 964            row.border_b_1()
 965                .border_color(transparent_black())
 966                .when(!is_last, |row| row.border_color(cx.theme().colors().border))
 967        });
 968
 969    row = row.children(
 970        items
 971            .map(IntoElement::into_any_element)
 972            .into_vec()
 973            .into_iter()
 974            .zip(column_widths.into_vec())
 975            .map(|(cell, width)| {
 976                base_cell_style_text(width, table_context.use_ui_font, cx)
 977                    .px_1()
 978                    .py_0p5()
 979                    .child(cell)
 980            }),
 981    );
 982
 983    let row = if let Some(map_row) = table_context.map_row {
 984        map_row((row_index, row), window, cx)
 985    } else {
 986        row.into_any_element()
 987    };
 988
 989    div().size_full().child(row).into_any_element()
 990}
 991
 992pub fn render_table_header(
 993    headers: TableRow<impl IntoElement>,
 994    table_context: TableRenderContext,
 995    columns_widths: Option<(
 996        WeakEntity<TableColumnWidths>,
 997        TableRow<TableResizeBehavior>,
 998        TableRow<DefiniteLength>,
 999    )>,
1000    entity_id: Option<EntityId>,
1001    cx: &mut App,
1002) -> impl IntoElement {
1003    let cols = headers.cols();
1004    let column_widths = table_context
1005        .column_widths
1006        .map_or(vec![None; cols].into_table_row(cols), |widths| {
1007            widths.map(Some)
1008        });
1009
1010    let element_id = entity_id
1011        .map(|entity| entity.to_string())
1012        .unwrap_or_default();
1013
1014    let shared_element_id: SharedString = format!("table-{}", element_id).into();
1015
1016    div()
1017        .flex()
1018        .flex_row()
1019        .items_center()
1020        .justify_between()
1021        .w_full()
1022        .p_2()
1023        .border_b_1()
1024        .border_color(cx.theme().colors().border)
1025        .children(
1026            headers
1027                .into_vec()
1028                .into_iter()
1029                .enumerate()
1030                .zip(column_widths.into_vec())
1031                .map(|((header_idx, h), width)| {
1032                    base_cell_style_text(width, table_context.use_ui_font, cx)
1033                        .child(h)
1034                        .id(ElementId::NamedInteger(
1035                            shared_element_id.clone(),
1036                            header_idx as u64,
1037                        ))
1038                        .when_some(
1039                            columns_widths.as_ref().cloned(),
1040                            |this, (column_widths, resizables, initial_sizes)| {
1041                                if resizables[header_idx].is_resizable() {
1042                                    this.on_click(move |event, window, cx| {
1043                                        if event.click_count() > 1 {
1044                                            column_widths
1045                                                .update(cx, |column, _| {
1046                                                    column.on_double_click(
1047                                                        header_idx,
1048                                                        &initial_sizes,
1049                                                        &resizables,
1050                                                        window,
1051                                                    );
1052                                                })
1053                                                .ok();
1054                                        }
1055                                    })
1056                                } else {
1057                                    this
1058                                }
1059                            },
1060                        )
1061                }),
1062        )
1063}
1064
1065#[derive(Clone)]
1066pub struct TableRenderContext {
1067    pub striped: bool,
1068    pub show_row_borders: bool,
1069    pub show_row_hover: bool,
1070    pub total_row_count: usize,
1071    pub column_widths: Option<TableRow<Length>>,
1072    pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
1073    pub use_ui_font: bool,
1074}
1075
1076impl TableRenderContext {
1077    fn new(table: &Table, cx: &App) -> Self {
1078        Self {
1079            striped: table.striped,
1080            show_row_borders: table.show_row_borders,
1081            show_row_hover: table.show_row_hover,
1082            total_row_count: table.rows.len(),
1083            column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)),
1084            map_row: table.map_row.clone(),
1085            use_ui_font: table.use_ui_font,
1086        }
1087    }
1088}
1089
1090impl RenderOnce for Table {
1091    fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1092        let table_context = TableRenderContext::new(&self, cx);
1093        let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
1094        let current_widths = self
1095            .col_widths
1096            .as_ref()
1097            .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable.clone())))
1098            .map(|(curr, resize_behavior)| (curr.downgrade(), resize_behavior));
1099
1100        let current_widths_with_initial_sizes = self
1101            .col_widths
1102            .as_ref()
1103            .and_then(|widths| {
1104                Some((
1105                    widths.current.as_ref()?,
1106                    widths.resizable.clone(),
1107                    widths.initial.clone(),
1108                ))
1109            })
1110            .map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial));
1111
1112        let width = self.width;
1113        let no_rows_rendered = self.rows.is_empty();
1114
1115        let table = div()
1116            .when_some(width, |this, width| this.w(width))
1117            .h_full()
1118            .v_flex()
1119            .when_some(self.headers.take(), |this, headers| {
1120                this.child(render_table_header(
1121                    headers,
1122                    table_context.clone(),
1123                    current_widths_with_initial_sizes,
1124                    interaction_state.as_ref().map(Entity::entity_id),
1125                    cx,
1126                ))
1127            })
1128            .when_some(current_widths, {
1129                |this, (widths, resize_behavior)| {
1130                    this.on_drag_move::<DraggedColumn>({
1131                        let widths = widths.clone();
1132                        move |e, window, cx| {
1133                            widths
1134                                .update(cx, |widths, cx| {
1135                                    widths.on_drag_move(e, &resize_behavior, window, cx);
1136                                })
1137                                .ok();
1138                        }
1139                    })
1140                    .on_children_prepainted({
1141                        let widths = widths.clone();
1142                        move |bounds, _, cx| {
1143                            widths
1144                                .update(cx, |widths, _| {
1145                                    // This works because all children x axis bounds are the same
1146                                    widths.cached_bounds_width =
1147                                        bounds[0].right() - bounds[0].left();
1148                                })
1149                                .ok();
1150                        }
1151                    })
1152                    .on_drop::<DraggedColumn>(move |_, _, cx| {
1153                        widths
1154                            .update(cx, |widths, _| {
1155                                widths.widths = widths.visible_widths.clone();
1156                            })
1157                            .ok();
1158                        // Finish the resize operation
1159                    })
1160                }
1161            })
1162            .child({
1163                let content = div()
1164                    .flex_grow()
1165                    .w_full()
1166                    .relative()
1167                    .overflow_hidden()
1168                    .map(|parent| match self.rows {
1169                        TableContents::Vec(items) => {
1170                            parent.children(items.into_iter().enumerate().map(|(index, row)| {
1171                                div().child(render_table_row(
1172                                    index,
1173                                    row,
1174                                    table_context.clone(),
1175                                    window,
1176                                    cx,
1177                                ))
1178                            }))
1179                        }
1180                        TableContents::UniformList(uniform_list_data) => parent.child(
1181                            uniform_list(
1182                                uniform_list_data.element_id,
1183                                uniform_list_data.row_count,
1184                                {
1185                                    let render_item_fn = uniform_list_data.render_list_of_rows_fn;
1186                                    move |range: Range<usize>, window, cx| {
1187                                        let elements = render_item_fn(range.clone(), window, cx)
1188                                            .into_iter()
1189                                            .map(|raw_row| raw_row.into_table_row(self.cols))
1190                                            .collect::<Vec<_>>();
1191                                        elements
1192                                            .into_iter()
1193                                            .zip(range)
1194                                            .map(|(row, row_index)| {
1195                                                render_table_row(
1196                                                    row_index,
1197                                                    row,
1198                                                    table_context.clone(),
1199                                                    window,
1200                                                    cx,
1201                                                )
1202                                            })
1203                                            .collect()
1204                                    }
1205                                },
1206                            )
1207                            .size_full()
1208                            .flex_grow()
1209                            .with_sizing_behavior(ListSizingBehavior::Auto)
1210                            .with_horizontal_sizing_behavior(if width.is_some() {
1211                                ListHorizontalSizingBehavior::Unconstrained
1212                            } else {
1213                                ListHorizontalSizingBehavior::FitList
1214                            })
1215                            .when_some(
1216                                interaction_state.as_ref(),
1217                                |this, state| {
1218                                    this.track_scroll(
1219                                        &state.read_with(cx, |s, _| s.scroll_handle.clone()),
1220                                    )
1221                                },
1222                            ),
1223                        ),
1224                        TableContents::VariableRowHeightList(variable_list_data) => parent.child(
1225                            list(variable_list_data.list_state.clone(), {
1226                                let render_item_fn = variable_list_data.render_row_fn;
1227                                move |row_index: usize, window: &mut Window, cx: &mut App| {
1228                                    let row = render_item_fn(row_index, window, cx)
1229                                        .into_table_row(self.cols);
1230                                    render_table_row(
1231                                        row_index,
1232                                        row,
1233                                        table_context.clone(),
1234                                        window,
1235                                        cx,
1236                                    )
1237                                }
1238                            })
1239                            .size_full()
1240                            .flex_grow()
1241                            .with_sizing_behavior(ListSizingBehavior::Auto),
1242                        ),
1243                    })
1244                    .when_some(
1245                        self.col_widths.as_ref().zip(interaction_state.as_ref()),
1246                        |parent, (table_widths, state)| {
1247                            parent.child(state.update(cx, |state, cx| {
1248                                let resizable_columns = &table_widths.resizable;
1249                                let column_widths = table_widths.lengths(cx);
1250                                let columns = table_widths.current.clone();
1251                                let initial_sizes = &table_widths.initial;
1252                                state.render_resize_handles(
1253                                    &column_widths,
1254                                    resizable_columns,
1255                                    initial_sizes,
1256                                    columns,
1257                                    window,
1258                                    cx,
1259                                )
1260                            }))
1261                        },
1262                    );
1263
1264                if let Some(state) = interaction_state.as_ref() {
1265                    let scrollbars = state
1266                        .read(cx)
1267                        .custom_scrollbar
1268                        .clone()
1269                        .unwrap_or_else(|| Scrollbars::new(ScrollAxes::Both));
1270                    content
1271                        .custom_scrollbars(
1272                            scrollbars.tracked_scroll_handle(&state.read(cx).scroll_handle),
1273                            window,
1274                            cx,
1275                        )
1276                        .into_any_element()
1277                } else {
1278                    content.into_any_element()
1279                }
1280            })
1281            .when_some(
1282                no_rows_rendered
1283                    .then_some(self.empty_table_callback)
1284                    .flatten(),
1285                |this, callback| {
1286                    this.child(
1287                        h_flex()
1288                            .size_full()
1289                            .p_3()
1290                            .items_start()
1291                            .justify_center()
1292                            .child(callback(window, cx)),
1293                    )
1294                },
1295            );
1296
1297        if let Some(interaction_state) = interaction_state.as_ref() {
1298            table
1299                .track_focus(&interaction_state.read(cx).focus_handle)
1300                .id(("table", interaction_state.entity_id()))
1301                .into_any_element()
1302        } else {
1303            table.into_any_element()
1304        }
1305    }
1306}
1307
1308impl Component for Table {
1309    fn scope() -> ComponentScope {
1310        ComponentScope::Layout
1311    }
1312
1313    fn description() -> Option<&'static str> {
1314        Some("A table component for displaying data in rows and columns with optional styling.")
1315    }
1316
1317    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
1318        Some(
1319            v_flex()
1320                .gap_6()
1321                .children(vec![
1322                    example_group_with_title(
1323                        "Basic Tables",
1324                        vec![
1325                            single_example(
1326                                "Simple Table",
1327                                Table::new(3)
1328                                    .width(px(400.))
1329                                    .header(vec!["Name", "Age", "City"])
1330                                    .row(vec!["Alice", "28", "New York"])
1331                                    .row(vec!["Bob", "32", "San Francisco"])
1332                                    .row(vec!["Charlie", "25", "London"])
1333                                    .into_any_element(),
1334                            ),
1335                            single_example(
1336                                "Two Column Table",
1337                                Table::new(2)
1338                                    .header(vec!["Category", "Value"])
1339                                    .width(px(300.))
1340                                    .row(vec!["Revenue", "$100,000"])
1341                                    .row(vec!["Expenses", "$75,000"])
1342                                    .row(vec!["Profit", "$25,000"])
1343                                    .into_any_element(),
1344                            ),
1345                        ],
1346                    ),
1347                    example_group_with_title(
1348                        "Styled Tables",
1349                        vec![
1350                            single_example(
1351                                "Default",
1352                                Table::new(3)
1353                                    .width(px(400.))
1354                                    .header(vec!["Product", "Price", "Stock"])
1355                                    .row(vec!["Laptop", "$999", "In Stock"])
1356                                    .row(vec!["Phone", "$599", "Low Stock"])
1357                                    .row(vec!["Tablet", "$399", "Out of Stock"])
1358                                    .into_any_element(),
1359                            ),
1360                            single_example(
1361                                "Striped",
1362                                Table::new(3)
1363                                    .width(px(400.))
1364                                    .striped()
1365                                    .header(vec!["Product", "Price", "Stock"])
1366                                    .row(vec!["Laptop", "$999", "In Stock"])
1367                                    .row(vec!["Phone", "$599", "Low Stock"])
1368                                    .row(vec!["Tablet", "$399", "Out of Stock"])
1369                                    .row(vec!["Headphones", "$199", "In Stock"])
1370                                    .into_any_element(),
1371                            ),
1372                        ],
1373                    ),
1374                    example_group_with_title(
1375                        "Mixed Content Table",
1376                        vec![single_example(
1377                            "Table with Elements",
1378                            Table::new(5)
1379                                .width(px(840.))
1380                                .header(vec!["Status", "Name", "Priority", "Deadline", "Action"])
1381                                .row(vec![
1382                                    Indicator::dot().color(Color::Success).into_any_element(),
1383                                    "Project A".into_any_element(),
1384                                    "High".into_any_element(),
1385                                    "2023-12-31".into_any_element(),
1386                                    Button::new("view_a", "View")
1387                                        .style(ButtonStyle::Filled)
1388                                        .full_width()
1389                                        .into_any_element(),
1390                                ])
1391                                .row(vec![
1392                                    Indicator::dot().color(Color::Warning).into_any_element(),
1393                                    "Project B".into_any_element(),
1394                                    "Medium".into_any_element(),
1395                                    "2024-03-15".into_any_element(),
1396                                    Button::new("view_b", "View")
1397                                        .style(ButtonStyle::Filled)
1398                                        .full_width()
1399                                        .into_any_element(),
1400                                ])
1401                                .row(vec![
1402                                    Indicator::dot().color(Color::Error).into_any_element(),
1403                                    "Project C".into_any_element(),
1404                                    "Low".into_any_element(),
1405                                    "2024-06-30".into_any_element(),
1406                                    Button::new("view_c", "View")
1407                                        .style(ButtonStyle::Filled)
1408                                        .full_width()
1409                                        .into_any_element(),
1410                                ])
1411                                .into_any_element(),
1412                        )],
1413                    ),
1414                ])
1415                .into_any_element(),
1416        )
1417    }
1418}
1419
1420#[cfg(test)]
1421mod test {
1422    use super::*;
1423
1424    fn is_almost_eq(a: &[f32], b: &[f32]) -> bool {
1425        a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6)
1426    }
1427
1428    fn cols_to_str(cols: &[f32], total_size: f32) -> String {
1429        cols.iter()
1430            .map(|f| "*".repeat(f32::round(f * total_size) as usize))
1431            .collect::<Vec<String>>()
1432            .join("|")
1433    }
1434
1435    fn parse_resize_behavior(
1436        input: &str,
1437        total_size: f32,
1438        expected_cols: usize,
1439    ) -> Vec<TableResizeBehavior> {
1440        let mut resize_behavior = Vec::with_capacity(expected_cols);
1441        for col in input.split('|') {
1442            if col.starts_with('X') || col.is_empty() {
1443                resize_behavior.push(TableResizeBehavior::None);
1444            } else if col.starts_with('*') {
1445                resize_behavior.push(TableResizeBehavior::MinSize(col.len() as f32 / total_size));
1446            } else {
1447                panic!("invalid test input: unrecognized resize behavior: {}", col);
1448            }
1449        }
1450
1451        if resize_behavior.len() != expected_cols {
1452            panic!(
1453                "invalid test input: expected {} columns, got {}",
1454                expected_cols,
1455                resize_behavior.len()
1456            );
1457        }
1458        resize_behavior
1459    }
1460
1461    mod reset_column_size {
1462        use super::*;
1463
1464        fn parse(input: &str) -> (Vec<f32>, f32, Option<usize>) {
1465            let mut widths = Vec::new();
1466            let mut column_index = None;
1467            for (index, col) in input.split('|').enumerate() {
1468                widths.push(col.len() as f32);
1469                if col.starts_with('X') {
1470                    column_index = Some(index);
1471                }
1472            }
1473
1474            for w in &widths {
1475                assert!(w.is_finite(), "incorrect number of columns");
1476            }
1477            let total = widths.iter().sum::<f32>();
1478            for width in &mut widths {
1479                *width /= total;
1480            }
1481            (widths, total, column_index)
1482        }
1483
1484        #[track_caller]
1485        fn check_reset_size(
1486            initial_sizes: &str,
1487            widths: &str,
1488            expected: &str,
1489            resize_behavior: &str,
1490        ) {
1491            let (initial_sizes, total_1, None) = parse(initial_sizes) else {
1492                panic!("invalid test input: initial sizes should not be marked");
1493            };
1494            let (widths, total_2, Some(column_index)) = parse(widths) else {
1495                panic!("invalid test input: widths should be marked");
1496            };
1497            assert_eq!(
1498                total_1, total_2,
1499                "invalid test input: total width not the same {total_1}, {total_2}"
1500            );
1501            let (expected, total_3, None) = parse(expected) else {
1502                panic!("invalid test input: expected should not be marked: {expected:?}");
1503            };
1504            assert_eq!(
1505                total_2, total_3,
1506                "invalid test input: total width not the same"
1507            );
1508            let cols = initial_sizes.len();
1509            let resize_behavior_vec = parse_resize_behavior(resize_behavior, total_1, cols);
1510            let resize_behavior = TableRow::from_vec(resize_behavior_vec, cols);
1511            let result = TableColumnWidths::reset_to_initial_size(
1512                column_index,
1513                TableRow::from_vec(widths, cols),
1514                TableRow::from_vec(initial_sizes, cols),
1515                &resize_behavior,
1516            );
1517            let result_slice = result.as_slice();
1518            let is_eq = is_almost_eq(result_slice, &expected);
1519            if !is_eq {
1520                let result_str = cols_to_str(result_slice, total_1);
1521                let expected_str = cols_to_str(&expected, total_1);
1522                panic!(
1523                    "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result_slice:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}"
1524                );
1525            }
1526        }
1527
1528        macro_rules! check_reset_size {
1529            (columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => {
1530                check_reset_size($initial, $current, $expected, $resizing);
1531            };
1532            ($name:ident, columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => {
1533                #[test]
1534                fn $name() {
1535                    check_reset_size($initial, $current, $expected, $resizing);
1536                }
1537            };
1538        }
1539
1540        check_reset_size!(
1541            basic_right,
1542            columns: 5,
1543            starting: "**|**|**|**|**",
1544            snapshot: "**|**|X|***|**",
1545            expected: "**|**|**|**|**",
1546            minimums: "X|*|*|*|*",
1547        );
1548
1549        check_reset_size!(
1550            basic_left,
1551            columns: 5,
1552            starting: "**|**|**|**|**",
1553            snapshot: "**|**|***|X|**",
1554            expected: "**|**|**|**|**",
1555            minimums: "X|*|*|*|**",
1556        );
1557
1558        check_reset_size!(
1559            squashed_left_reset_col2,
1560            columns: 6,
1561            starting: "*|***|**|**|****|*",
1562            snapshot: "*|*|X|*|*|********",
1563            expected: "*|*|**|*|*|*******",
1564            minimums: "X|*|*|*|*|*",
1565        );
1566
1567        check_reset_size!(
1568            grow_cascading_right,
1569            columns: 6,
1570            starting: "*|***|****|**|***|*",
1571            snapshot: "*|***|X|**|**|*****",
1572            expected: "*|***|****|*|*|****",
1573            minimums: "X|*|*|*|*|*",
1574        );
1575
1576        check_reset_size!(
1577           squashed_right_reset_col4,
1578           columns: 6,
1579           starting: "*|***|**|**|****|*",
1580           snapshot: "*|********|*|*|X|*",
1581           expected: "*|*****|*|*|****|*",
1582           minimums: "X|*|*|*|*|*",
1583        );
1584
1585        check_reset_size!(
1586            reset_col6_right,
1587            columns: 6,
1588            starting: "*|***|**|***|***|**",
1589            snapshot: "*|***|**|***|**|XXX",
1590            expected: "*|***|**|***|***|**",
1591            minimums: "X|*|*|*|*|*",
1592        );
1593
1594        check_reset_size!(
1595            reset_col6_left,
1596            columns: 6,
1597            starting: "*|***|**|***|***|**",
1598            snapshot: "*|***|**|***|****|X",
1599            expected: "*|***|**|***|***|**",
1600            minimums: "X|*|*|*|*|*",
1601        );
1602
1603        check_reset_size!(
1604            last_column_grow_cascading,
1605            columns: 6,
1606            starting: "*|***|**|**|**|***",
1607            snapshot: "*|*******|*|**|*|X",
1608            expected: "*|******|*|*|*|***",
1609            minimums: "X|*|*|*|*|*",
1610        );
1611
1612        check_reset_size!(
1613            goes_left_when_left_has_extreme_diff,
1614            columns: 6,
1615            starting: "*|***|****|**|**|***",
1616            snapshot: "*|********|X|*|**|**",
1617            expected: "*|*****|****|*|**|**",
1618            minimums: "X|*|*|*|*|*",
1619        );
1620
1621        check_reset_size!(
1622            basic_shrink_right,
1623            columns: 6,
1624            starting: "**|**|**|**|**|**",
1625            snapshot: "**|**|XXX|*|**|**",
1626            expected: "**|**|**|**|**|**",
1627            minimums: "X|*|*|*|*|*",
1628        );
1629
1630        check_reset_size!(
1631            shrink_should_go_left,
1632            columns: 6,
1633            starting: "*|***|**|*|*|*",
1634            snapshot: "*|*|XXX|**|*|*",
1635            expected: "*|**|**|**|*|*",
1636            minimums: "X|*|*|*|*|*",
1637        );
1638
1639        check_reset_size!(
1640            shrink_should_go_right,
1641            columns: 6,
1642            starting: "*|***|**|**|**|*",
1643            snapshot: "*|****|XXX|*|*|*",
1644            expected: "*|****|**|**|*|*",
1645            minimums: "X|*|*|*|*|*",
1646        );
1647    }
1648
1649    mod drag_handle {
1650        use super::*;
1651
1652        fn parse(input: &str) -> (Vec<f32>, f32, Option<usize>) {
1653            let mut widths = Vec::new();
1654            let column_index = input.replace("*", "").find("I");
1655            for col in input.replace("I", "|").split('|') {
1656                widths.push(col.len() as f32);
1657            }
1658
1659            for w in &widths {
1660                assert!(w.is_finite(), "incorrect number of columns");
1661            }
1662            let total = widths.iter().sum::<f32>();
1663            for width in &mut widths {
1664                *width /= total;
1665            }
1666            (widths, total, column_index)
1667        }
1668
1669        #[track_caller]
1670        fn check(distance: i32, widths: &str, expected: &str, resize_behavior: &str) {
1671            let (widths, total_1, Some(column_index)) = parse(widths) else {
1672                panic!("invalid test input: widths should be marked");
1673            };
1674            let (expected, total_2, None) = parse(expected) else {
1675                panic!("invalid test input: expected should not be marked: {expected:?}");
1676            };
1677            assert_eq!(
1678                total_1, total_2,
1679                "invalid test input: total width not the same"
1680            );
1681            let cols = widths.len();
1682            let resize_behavior_vec = parse_resize_behavior(resize_behavior, total_1, cols);
1683            let resize_behavior = TableRow::from_vec(resize_behavior_vec, cols);
1684
1685            let distance = distance as f32 / total_1;
1686
1687            let mut widths_table_row = TableRow::from_vec(widths, cols);
1688            TableColumnWidths::drag_column_handle(
1689                distance,
1690                column_index,
1691                &mut widths_table_row,
1692                &resize_behavior,
1693            );
1694
1695            let result_widths = widths_table_row.as_slice();
1696            let is_eq = is_almost_eq(result_widths, &expected);
1697            if !is_eq {
1698                let result_str = cols_to_str(result_widths, total_1);
1699                let expected_str = cols_to_str(&expected, total_1);
1700                panic!(
1701                    "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result_widths:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}"
1702                );
1703            }
1704        }
1705
1706        macro_rules! check {
1707            (columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => {
1708                check($dist, $current, $expected, $resizing);
1709            };
1710            ($name:ident, columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => {
1711                #[test]
1712                fn $name() {
1713                    check($dist, $current, $expected, $resizing);
1714                }
1715            };
1716        }
1717
1718        check!(
1719            basic_right_drag,
1720            columns: 3,
1721            distance: 1,
1722            snapshot: "**|**I**",
1723            expected: "**|***|*",
1724            minimums: "X|*|*",
1725        );
1726
1727        check!(
1728            drag_left_against_mins,
1729            columns: 5,
1730            distance: -1,
1731            snapshot: "*|*|*|*I*******",
1732            expected: "*|*|*|*|*******",
1733            minimums: "X|*|*|*|*",
1734        );
1735
1736        check!(
1737            drag_left,
1738            columns: 5,
1739            distance: -2,
1740            snapshot: "*|*|*|*****I***",
1741            expected: "*|*|*|***|*****",
1742            minimums: "X|*|*|*|*",
1743        );
1744    }
1745}