use std::{ops::Range, rc::Rc}; use gpui::{ AbsoluteLength, AppContext, Context, DefiniteLength, DragMoveEvent, Entity, EntityId, FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, ListState, Point, Stateful, UniformListScrollHandle, WeakEntity, list, transparent_black, uniform_list,}; use crate::{ ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component, ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, ScrollAxes, ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled, StyledExt as _, StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex, px, single_example, table_row::{IntoTableRow as _, TableRow}, v_flex,};use itertools::intersperse_with; pub mod table_row { //! A newtype for a table row that enforces a fixed column count at runtime. //! //! This type ensures that all rows in a table have the same width, preventing accidental creation or mutation of rows with inconsistent lengths. //! It is especially useful for CSV or tabular data where rectangular invariants must be maintained, but the number of columns is only known at runtime. //! By using `TableRow`, we gain stronger guarantees and safer APIs compared to a bare `Vec<T>`, without requiring const generics. use std::{ any::type_name, ops::{ Index, IndexMut, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive, }, }; #[derive(Clone, Debug, PartialEq, Eq)] pub struct TableRow<T>(Vec<T>); impl<T> TableRow<T> { /// Constructs a `TableRow` from a `Vec<T>`, panicking if the length does not match `expected_length`. /// /// Use this when you want to ensure at construction time that the row has the correct number of columns. /// This enforces the rectangular invariant for table data, preventing accidental creation of malformed rows. /// /// # Panics /// Panics if `data.len() != expected_length`. pub fn from_vec(data: Vec<T>, expected_length: usize) -> Self { Self::try_from_vec(data, expected_length).unwrap_or_else(|e| { let name = type_name::<Vec<T>>(); panic!("Expected {name} to be created successfully: {e}"); }) } /// Attempts to construct a `TableRow` from a `Vec<T>`, returning an error if the length does not match `expected_len`. /// /// This is a fallible alternative to `from_vec`, allowing you to handle inconsistent row lengths gracefully. /// Returns `Ok(TableRow)` if the length matches, or an `Err` with a descriptive message otherwise. pub fn try_from_vec(data: Vec<T>, expected_len: usize) -> Result<Self, String> { if data.len() != expected_len { Err(format!( "Row length {} does not match expected {}", data.len(), expected_len )) } else { Ok(Self(data)) } } /// Returns reference to element by column index. /// /// # Panics /// Panics if `col` is out of bounds (i.e., `col >= self.cols()`). pub fn expect_get(&self, col: usize) -> &T { self.0.get(col).unwrap_or_else(|| { panic!( "Expected table row of `{}` to have {col:?}", type_name::<T>() ) }) } pub fn get(&self, col: usize) -> Option<&T> { self.0.get(col) } pub fn as_slice(&self) -> &[T] { &self.0 } pub fn into_vec(self) -> Vec<T> { self.0 } /// Like [`map`], but borrows the row and clones each element before mapping. /// /// This is useful when you want to map over a borrowed row without consuming it, /// but your mapping function requires ownership of each element. /// /// # Difference /// - `map_cloned` takes `&self`, clones each element, and applies `f(T) -> U`. /// - [`map`] takes `self` by value and applies `f(T) -> U` directly, consuming the row. /// - [`map_ref`] takes `&self` and applies `f(&T) -> U` to references of each element. pub fn map_cloned<F, U>(&self, f: F) -> TableRow<U> where F: FnMut(T) -> U, T: Clone, { self.clone().map(f) } /// Consumes the row and transforms all elements within it in a length-safe way. /// /// # Difference /// - `map` takes ownership of the row (`self`) and applies `f(T) -> U` to each element. /// - Use this when you want to transform and consume the row in one step. /// - See also [`map_cloned`] (for mapping over a borrowed row with cloning) and [`map_ref`] (for mapping over references). pub fn map<F, U>(self, f: F) -> TableRow<U> where F: FnMut(T) -> U, { TableRow(self.0.into_iter().map(f).collect()) } /// Borrows the row and transforms all elements by reference in a length-safe way. /// /// # Difference /// - `map_ref` takes `&self` and applies `f(&T) -> U` to each element by reference. /// - Use this when you want to map over a borrowed row without cloning or consuming it. /// - See also [`map`] (for consuming the row) and [`map_cloned`] (for mapping with cloning). pub fn map_ref<F, U>(&self, f: F) -> TableRow<U> where F: FnMut(&T) -> U, { TableRow(self.0.iter().map(f).collect()) } /// Number of columns (alias to `len()` with more semantic meaning) pub fn cols(&self) -> usize { self.0.len() } } ///// Convenience traits ///// pub trait IntoTableRow<T> { fn into_table_row(self, expected_length: usize) -> TableRow<T>; } impl<T> IntoTableRow<T> for Vec<T> { fn into_table_row(self, expected_length: usize) -> TableRow<T> { TableRow::from_vec(self, expected_length) } } // Index implementations for convenient access impl<T> Index<usize> for TableRow<T> { type Output = T; fn index(&self, index: usize) -> &Self::Output { &self.0[index] } } impl<T> IndexMut<usize> for TableRow<T> { fn index_mut(&mut self, index: usize) -> &mut Self::Output { &mut self.0[index] } } // Range indexing implementations for slice operations impl<T> Index<Range<usize>> for TableRow<T> { type Output = [T]; fn index(&self, index: Range<usize>) -> &Self::Output { <Vec<T> as Index<Range<usize>>>::index(&self.0, index) } } impl<T> Index<RangeFrom<usize>> for TableRow<T> { type Output = [T]; fn index(&self, index: RangeFrom<usize>) -> &Self::Output { <Vec<T> as Index<RangeFrom<usize>>>::index(&self.0, index) } } impl<T> Index<RangeTo<usize>> for TableRow<T> { type Output = [T]; fn index(&self, index: RangeTo<usize>) -> &Self::Output { <Vec<T> as Index<RangeTo<usize>>>::index(&self.0, index) } } impl<T> Index<RangeToInclusive<usize>> for TableRow<T> { type Output = [T]; fn index(&self, index: RangeToInclusive<usize>) -> &Self::Output { <Vec<T> as Index<RangeToInclusive<usize>>>::index(&self.0, index) } } impl<T> Index<RangeFull> for TableRow<T> { type Output = [T]; fn index(&self, index: RangeFull) -> &Self::Output { <Vec<T> as Index<RangeFull>>::index(&self.0, index) } } impl<T> Index<RangeInclusive<usize>> for TableRow<T> { type Output = [T]; fn index(&self, index: RangeInclusive<usize>) -> &Self::Output { <Vec<T> as Index<RangeInclusive<usize>>>::index(&self.0, index) } } impl<T> IndexMut<RangeInclusive<usize>> for TableRow<T> { fn index_mut(&mut self, index: RangeInclusive<usize>) -> &mut Self::Output { <Vec<T> as IndexMut<RangeInclusive<usize>>>::index_mut(&mut self.0, index) } }} const RESIZE_COLUMN_WIDTH: f32 = 8.0; /// Represents an unchecked table row, which is a vector of elements./// Will be converted into `TableRow<T>` internallypub type UncheckedTableRow<T> = Vec<T>; #[derive(Debug)]struct DraggedColumn(usize); struct UniformListData { render_list_of_rows_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<UncheckedTableRow<AnyElement>>>, element_id: ElementId, row_count: usize,} struct VariableRowHeightListData { /// Unlike UniformList, this closure renders only single row, allowing each one to have its own height render_row_fn: Box<dyn Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement>>, list_state: ListState, row_count: usize,} enum TableContents { Vec(Vec<TableRow<AnyElement>>), UniformList(UniformListData), VariableRowHeightList(VariableRowHeightListData),} impl TableContents { fn rows_mut(&mut self) -> Option<&mut Vec<TableRow<AnyElement>>> { match self { TableContents::Vec(rows) => Some(rows), TableContents::UniformList(_) => None, TableContents::VariableRowHeightList(_) => None, } } fn len(&self) -> usize { match self { TableContents::Vec(rows) => rows.len(), TableContents::UniformList(data) => data.row_count, TableContents::VariableRowHeightList(data) => data.row_count, } } fn is_empty(&self) -> bool { self.len() == 0 }} pub struct TableInteractionState { pub focus_handle: FocusHandle, pub scroll_handle: UniformListScrollHandle, pub custom_scrollbar: Option<Scrollbars>,} impl TableInteractionState { pub fn new(cx: &mut App) -> Self { Self { focus_handle: cx.focus_handle(), scroll_handle: UniformListScrollHandle::new(), custom_scrollbar: None, } } pub fn with_custom_scrollbar(mut self, custom_scrollbar: Scrollbars) -> Self { self.custom_scrollbar = Some(custom_scrollbar); self } pub fn scroll_offset(&self) -> Point<Pixels> { self.scroll_handle.offset() } pub fn set_scroll_offset(&self, offset: Point<Pixels>) { self.scroll_handle.set_offset(offset); } pub fn listener<E: ?Sized>( this: &Entity<Self>, f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static, ) -> impl Fn(&E, &mut Window, &mut App) + 'static { let view = this.downgrade(); move |e: &E, window: &mut Window, cx: &mut App| { view.update(cx, |view, cx| f(view, e, window, cx)).ok(); } } /// Renders invisible resize handles overlaid on top of table content. /// /// - Spacer: invisible element that matches the width of table column content /// - Divider: contains the actual resize handle that users can drag to resize columns /// /// Structure: [spacer] [divider] [spacer] [divider] [spacer] /// /// Business logic: /// 1. Creates spacers matching each column width /// 2. Intersperses (inserts) resize handles between spacers (interactive only for resizable columns) /// 3. Each handle supports hover highlighting, double-click to reset, and drag to resize /// 4. Returns an absolute-positioned overlay that sits on top of table content fn render_resize_handles( &self, column_widths: &TableRow<Length>, resizable_columns: &TableRow<TableResizeBehavior>, initial_sizes: &TableRow<DefiniteLength>, columns: Option<Entity<TableColumnWidths>>, window: &mut Window, cx: &mut App, ) -> AnyElement { let spacers = column_widths .as_slice() .iter() .map(|width| base_cell_style(Some(*width)).into_any_element()); let mut column_ix = 0; let resizable_columns_shared = Rc::new(resizable_columns.clone()); let initial_sizes_shared = Rc::new(initial_sizes.clone()); let mut resizable_columns_iter = resizable_columns.as_slice().iter(); // Insert dividers between spacers (column content) let dividers = intersperse_with(spacers, || { let resizable_columns = Rc::clone(&resizable_columns_shared); let initial_sizes = Rc::clone(&initial_sizes_shared); window.with_id(column_ix, |window| { let mut resize_divider = div() // This is required because this is evaluated at a different time than the use_state call above .id(column_ix) .relative() .top_0() .w_px() .h_full() .bg(cx.theme().colors().border.opacity(0.8)); let mut resize_handle = div() .id("column-resize-handle") .absolute() .left_neg_0p5() .w(px(RESIZE_COLUMN_WIDTH)) .h_full(); if resizable_columns_iter .next() .is_some_and(TableResizeBehavior::is_resizable) { let hovered = window.use_state(cx, |_window, _cx| false); resize_divider = resize_divider.when(*hovered.read(cx), |div| { div.bg(cx.theme().colors().border_focused) }); resize_handle = resize_handle .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered)) .cursor_col_resize() .when_some(columns.clone(), |this, columns| { this.on_click(move |event, window, cx| { if event.click_count() >= 2 { columns.update(cx, |columns, _| { columns.on_double_click( column_ix, &initial_sizes, &resizable_columns, window, ); }) } cx.stop_propagation(); }) }) .on_drag(DraggedColumn(column_ix), |_, _offset, _window, cx| { cx.new(|_cx| gpui::Empty) }) } column_ix += 1; resize_divider.child(resize_handle).into_any_element() }) }); h_flex() .id("resize-handles") .absolute() .inset_0() .w_full() .children(dividers) .into_any_element() }} #[derive(Debug, Copy, Clone, PartialEq)]pub enum TableResizeBehavior { None, Resizable, MinSize(f32),} impl TableResizeBehavior { pub fn is_resizable(&self) -> bool { *self != TableResizeBehavior::None } pub fn min_size(&self) -> Option<f32> { match self { TableResizeBehavior::None => None, TableResizeBehavior::Resizable => Some(0.05), TableResizeBehavior::MinSize(min_size) => Some(*min_size), } }} pub struct TableColumnWidths { widths: TableRow<DefiniteLength>, visible_widths: TableRow<DefiniteLength>, cached_bounds_width: Pixels, initialized: bool,} impl TableColumnWidths { pub fn new(cols: usize, _: &mut App) -> Self { Self { widths: vec![DefiniteLength::default(); cols].into_table_row(cols), visible_widths: vec![DefiniteLength::default(); cols].into_table_row(cols), cached_bounds_width: Default::default(), initialized: false, } } pub fn cols(&self) -> usize { self.widths.cols() } fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 { match length { DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => *pixels / bounds_width, DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => { rems_width.to_pixels(rem_size) / bounds_width } DefiniteLength::Fraction(fraction) => *fraction, } } fn on_double_click( &mut self, double_click_position: usize, initial_sizes: &TableRow<DefiniteLength>, resize_behavior: &TableRow<TableResizeBehavior>, window: &mut Window, ) { let bounds_width = self.cached_bounds_width; let rem_size = window.rem_size(); let initial_sizes = initial_sizes.map_ref(|length| Self::get_fraction(length, bounds_width, rem_size)); let widths = self .widths .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size)); let updated_widths = Self::reset_to_initial_size( double_click_position, widths, initial_sizes, resize_behavior, ); self.widths = updated_widths.map(DefiniteLength::Fraction); self.visible_widths = self.widths.clone(); // previously was copy } fn reset_to_initial_size( col_idx: usize, mut widths: TableRow<f32>, initial_sizes: TableRow<f32>, resize_behavior: &TableRow<TableResizeBehavior>, ) -> TableRow<f32> { // RESET: // Part 1: // Figure out if we should shrink/grow the selected column // Get diff which represents the change in column we want to make initial size delta curr_size = diffLines 1–500 of 1745. Load more
use std::{ops::Range, rc::Rc}; use gpui::{ AbsoluteLength, AppContext, Context, DefiniteLength, DragMoveEvent, Entity, EntityId, FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, ListState, Point, Stateful, UniformListScrollHandle, WeakEntity, list, transparent_black, uniform_list, }; use crate::{ ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component, ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, ScrollAxes, ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled, StyledExt as _, StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex, px, single_example, table_row::{IntoTableRow as _, TableRow}, v_flex, }; use itertools::intersperse_with; pub mod table_row { //! A newtype for a table row that enforces a fixed column count at runtime. //! //! This type ensures that all rows in a table have the same width, preventing accidental creation or mutation of rows with inconsistent lengths. //! It is especially useful for CSV or tabular data where rectangular invariants must be maintained, but the number of columns is only known at runtime. //! By using `TableRow`, we gain stronger guarantees and safer APIs compared to a bare `Vec<T>`, without requiring const generics. use std::{ any::type_name, ops::{ Index, IndexMut, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive, }, }; #[derive(Clone, Debug, PartialEq, Eq)] pub struct TableRow<T>(Vec<T>); impl<T> TableRow<T> { /// Constructs a `TableRow` from a `Vec<T>`, panicking if the length does not match `expected_length`. /// /// Use this when you want to ensure at construction time that the row has the correct number of columns. /// This enforces the rectangular invariant for table data, preventing accidental creation of malformed rows. /// /// # Panics /// Panics if `data.len() != expected_length`. pub fn from_vec(data: Vec<T>, expected_length: usize) -> Self { Self::try_from_vec(data, expected_length).unwrap_or_else(|e| { let name = type_name::<Vec<T>>(); panic!("Expected {name} to be created successfully: {e}"); }) } /// Attempts to construct a `TableRow` from a `Vec<T>`, returning an error if the length does not match `expected_len`. /// /// This is a fallible alternative to `from_vec`, allowing you to handle inconsistent row lengths gracefully. /// Returns `Ok(TableRow)` if the length matches, or an `Err` with a descriptive message otherwise. pub fn try_from_vec(data: Vec<T>, expected_len: usize) -> Result<Self, String> { if data.len() != expected_len { Err(format!( "Row length {} does not match expected {}", data.len(), expected_len )) } else { Ok(Self(data)) } } /// Returns reference to element by column index. /// /// # Panics /// Panics if `col` is out of bounds (i.e., `col >= self.cols()`). pub fn expect_get(&self, col: usize) -> &T { self.0.get(col).unwrap_or_else(|| { panic!( "Expected table row of `{}` to have {col:?}", type_name::<T>() ) }) } pub fn get(&self, col: usize) -> Option<&T> { self.0.get(col) } pub fn as_slice(&self) -> &[T] { &self.0 } pub fn into_vec(self) -> Vec<T> { self.0 } /// Like [`map`], but borrows the row and clones each element before mapping. /// /// This is useful when you want to map over a borrowed row without consuming it, /// but your mapping function requires ownership of each element. /// /// # Difference /// - `map_cloned` takes `&self`, clones each element, and applies `f(T) -> U`. /// - [`map`] takes `self` by value and applies `f(T) -> U` directly, consuming the row. /// - [`map_ref`] takes `&self` and applies `f(&T) -> U` to references of each element. pub fn map_cloned<F, U>(&self, f: F) -> TableRow<U> where F: FnMut(T) -> U, T: Clone, { self.clone().map(f) } /// Consumes the row and transforms all elements within it in a length-safe way. /// /// # Difference /// - `map` takes ownership of the row (`self`) and applies `f(T) -> U` to each element. /// - Use this when you want to transform and consume the row in one step. /// - See also [`map_cloned`] (for mapping over a borrowed row with cloning) and [`map_ref`] (for mapping over references). pub fn map<F, U>(self, f: F) -> TableRow<U> where F: FnMut(T) -> U, { TableRow(self.0.into_iter().map(f).collect()) } /// Borrows the row and transforms all elements by reference in a length-safe way. /// /// # Difference /// - `map_ref` takes `&self` and applies `f(&T) -> U` to each element by reference. /// - Use this when you want to map over a borrowed row without cloning or consuming it. /// - See also [`map`] (for consuming the row) and [`map_cloned`] (for mapping with cloning). pub fn map_ref<F, U>(&self, f: F) -> TableRow<U> where F: FnMut(&T) -> U, { TableRow(self.0.iter().map(f).collect()) } /// Number of columns (alias to `len()` with more semantic meaning) pub fn cols(&self) -> usize { self.0.len() } } ///// Convenience traits ///// pub trait IntoTableRow<T> { fn into_table_row(self, expected_length: usize) -> TableRow<T>; } impl<T> IntoTableRow<T> for Vec<T> { fn into_table_row(self, expected_length: usize) -> TableRow<T> { TableRow::from_vec(self, expected_length) } } // Index implementations for convenient access impl<T> Index<usize> for TableRow<T> { type Output = T; fn index(&self, index: usize) -> &Self::Output { &self.0[index] } } impl<T> IndexMut<usize> for TableRow<T> { fn index_mut(&mut self, index: usize) -> &mut Self::Output { &mut self.0[index] } } // Range indexing implementations for slice operations impl<T> Index<Range<usize>> for TableRow<T> { type Output = [T]; fn index(&self, index: Range<usize>) -> &Self::Output { <Vec<T> as Index<Range<usize>>>::index(&self.0, index) } } impl<T> Index<RangeFrom<usize>> for TableRow<T> { type Output = [T]; fn index(&self, index: RangeFrom<usize>) -> &Self::Output { <Vec<T> as Index<RangeFrom<usize>>>::index(&self.0, index) } } impl<T> Index<RangeTo<usize>> for TableRow<T> { type Output = [T]; fn index(&self, index: RangeTo<usize>) -> &Self::Output { <Vec<T> as Index<RangeTo<usize>>>::index(&self.0, index) } } impl<T> Index<RangeToInclusive<usize>> for TableRow<T> { type Output = [T]; fn index(&self, index: RangeToInclusive<usize>) -> &Self::Output { <Vec<T> as Index<RangeToInclusive<usize>>>::index(&self.0, index) } } impl<T> Index<RangeFull> for TableRow<T> { type Output = [T]; fn index(&self, index: RangeFull) -> &Self::Output { <Vec<T> as Index<RangeFull>>::index(&self.0, index) } } impl<T> Index<RangeInclusive<usize>> for TableRow<T> { type Output = [T]; fn index(&self, index: RangeInclusive<usize>) -> &Self::Output { <Vec<T> as Index<RangeInclusive<usize>>>::index(&self.0, index) } } impl<T> IndexMut<RangeInclusive<usize>> for TableRow<T> { fn index_mut(&mut self, index: RangeInclusive<usize>) -> &mut Self::Output { <Vec<T> as IndexMut<RangeInclusive<usize>>>::index_mut(&mut self.0, index) } } } const RESIZE_COLUMN_WIDTH: f32 = 8.0; /// Represents an unchecked table row, which is a vector of elements. /// Will be converted into `TableRow<T>` internally pub type UncheckedTableRow<T> = Vec<T>; #[derive(Debug)] struct DraggedColumn(usize); struct UniformListData { render_list_of_rows_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<UncheckedTableRow<AnyElement>>>, element_id: ElementId, row_count: usize, } struct VariableRowHeightListData { /// Unlike UniformList, this closure renders only single row, allowing each one to have its own height render_row_fn: Box<dyn Fn(usize, &mut Window, &mut App) -> UncheckedTableRow<AnyElement>>, list_state: ListState, row_count: usize, } enum TableContents { Vec(Vec<TableRow<AnyElement>>), UniformList(UniformListData), VariableRowHeightList(VariableRowHeightListData), } impl TableContents { fn rows_mut(&mut self) -> Option<&mut Vec<TableRow<AnyElement>>> { match self { TableContents::Vec(rows) => Some(rows), TableContents::UniformList(_) => None, TableContents::VariableRowHeightList(_) => None, } } fn len(&self) -> usize { match self { TableContents::Vec(rows) => rows.len(), TableContents::UniformList(data) => data.row_count, TableContents::VariableRowHeightList(data) => data.row_count, } } fn is_empty(&self) -> bool { self.len() == 0 } } pub struct TableInteractionState { pub focus_handle: FocusHandle, pub scroll_handle: UniformListScrollHandle, pub custom_scrollbar: Option<Scrollbars>, } impl TableInteractionState { pub fn new(cx: &mut App) -> Self { Self { focus_handle: cx.focus_handle(), scroll_handle: UniformListScrollHandle::new(), custom_scrollbar: None, } } pub fn with_custom_scrollbar(mut self, custom_scrollbar: Scrollbars) -> Self { self.custom_scrollbar = Some(custom_scrollbar); self } pub fn scroll_offset(&self) -> Point<Pixels> { self.scroll_handle.offset() } pub fn set_scroll_offset(&self, offset: Point<Pixels>) { self.scroll_handle.set_offset(offset); } pub fn listener<E: ?Sized>( this: &Entity<Self>, f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static, ) -> impl Fn(&E, &mut Window, &mut App) + 'static { let view = this.downgrade(); move |e: &E, window: &mut Window, cx: &mut App| { view.update(cx, |view, cx| f(view, e, window, cx)).ok(); } } /// Renders invisible resize handles overlaid on top of table content. /// /// - Spacer: invisible element that matches the width of table column content /// - Divider: contains the actual resize handle that users can drag to resize columns /// /// Structure: [spacer] [divider] [spacer] [divider] [spacer] /// /// Business logic: /// 1. Creates spacers matching each column width /// 2. Intersperses (inserts) resize handles between spacers (interactive only for resizable columns) /// 3. Each handle supports hover highlighting, double-click to reset, and drag to resize /// 4. Returns an absolute-positioned overlay that sits on top of table content fn render_resize_handles( &self, column_widths: &TableRow<Length>, resizable_columns: &TableRow<TableResizeBehavior>, initial_sizes: &TableRow<DefiniteLength>, columns: Option<Entity<TableColumnWidths>>, window: &mut Window, cx: &mut App, ) -> AnyElement { let spacers = column_widths .as_slice() .iter() .map(|width| base_cell_style(Some(*width)).into_any_element()); let mut column_ix = 0; let resizable_columns_shared = Rc::new(resizable_columns.clone()); let initial_sizes_shared = Rc::new(initial_sizes.clone()); let mut resizable_columns_iter = resizable_columns.as_slice().iter(); // Insert dividers between spacers (column content) let dividers = intersperse_with(spacers, || { let resizable_columns = Rc::clone(&resizable_columns_shared); let initial_sizes = Rc::clone(&initial_sizes_shared); window.with_id(column_ix, |window| { let mut resize_divider = div() // This is required because this is evaluated at a different time than the use_state call above .id(column_ix) .relative() .top_0() .w_px() .h_full() .bg(cx.theme().colors().border.opacity(0.8)); let mut resize_handle = div() .id("column-resize-handle") .absolute() .left_neg_0p5() .w(px(RESIZE_COLUMN_WIDTH)) .h_full(); if resizable_columns_iter .next() .is_some_and(TableResizeBehavior::is_resizable) { let hovered = window.use_state(cx, |_window, _cx| false); resize_divider = resize_divider.when(*hovered.read(cx), |div| { div.bg(cx.theme().colors().border_focused) }); resize_handle = resize_handle .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered)) .cursor_col_resize() .when_some(columns.clone(), |this, columns| { this.on_click(move |event, window, cx| { if event.click_count() >= 2 { columns.update(cx, |columns, _| { columns.on_double_click( column_ix, &initial_sizes, &resizable_columns, window, ); }) } cx.stop_propagation(); }) }) .on_drag(DraggedColumn(column_ix), |_, _offset, _window, cx| { cx.new(|_cx| gpui::Empty) }) } column_ix += 1; resize_divider.child(resize_handle).into_any_element() }) }); h_flex() .id("resize-handles") .absolute() .inset_0() .w_full() .children(dividers) .into_any_element() } } #[derive(Debug, Copy, Clone, PartialEq)] pub enum TableResizeBehavior { None, Resizable, MinSize(f32), } impl TableResizeBehavior { pub fn is_resizable(&self) -> bool { *self != TableResizeBehavior::None } pub fn min_size(&self) -> Option<f32> { match self { TableResizeBehavior::None => None, TableResizeBehavior::Resizable => Some(0.05), TableResizeBehavior::MinSize(min_size) => Some(*min_size), } } } pub struct TableColumnWidths { widths: TableRow<DefiniteLength>, visible_widths: TableRow<DefiniteLength>, cached_bounds_width: Pixels, initialized: bool, } impl TableColumnWidths { pub fn new(cols: usize, _: &mut App) -> Self { Self { widths: vec![DefiniteLength::default(); cols].into_table_row(cols), visible_widths: vec![DefiniteLength::default(); cols].into_table_row(cols), cached_bounds_width: Default::default(), initialized: false, } } pub fn cols(&self) -> usize { self.widths.cols() } fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 { match length { DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => *pixels / bounds_width, DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => { rems_width.to_pixels(rem_size) / bounds_width } DefiniteLength::Fraction(fraction) => *fraction, } } fn on_double_click( &mut self, double_click_position: usize, initial_sizes: &TableRow<DefiniteLength>, resize_behavior: &TableRow<TableResizeBehavior>, window: &mut Window, ) { let bounds_width = self.cached_bounds_width; let rem_size = window.rem_size(); let initial_sizes = initial_sizes.map_ref(|length| Self::get_fraction(length, bounds_width, rem_size)); let widths = self .widths .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size)); let updated_widths = Self::reset_to_initial_size( double_click_position, widths, initial_sizes, resize_behavior, ); self.widths = updated_widths.map(DefiniteLength::Fraction); self.visible_widths = self.widths.clone(); // previously was copy } fn reset_to_initial_size( col_idx: usize, mut widths: TableRow<f32>, initial_sizes: TableRow<f32>, resize_behavior: &TableRow<TableResizeBehavior>, ) -> TableRow<f32> { // RESET: // Part 1: // Figure out if we should shrink/grow the selected column // Get diff which represents the change in column we want to make initial size delta curr_size = diff