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