table.rs

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