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