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