diff --git a/crates/csv_preview/src/csv_preview.rs b/crates/csv_preview/src/csv_preview.rs index 1b99139b004a940dfa0902e185f67fb4b77ed6a1..a1b10feea074f6e42974528a65b2b14ff46592bc 100644 --- a/crates/csv_preview/src/csv_preview.rs +++ b/crates/csv_preview/src/csv_preview.rs @@ -10,8 +10,8 @@ use std::{ use crate::table_data_engine::TableDataEngine; use ui::{ - AbsoluteLength, DefiniteLength, RedistributableColumnsState, SharedString, - TableInteractionState, TableResizeBehavior, prelude::*, + AbsoluteLength, ResizableColumnsState, SharedString, TableInteractionState, + TableResizeBehavior, prelude::*, }; use workspace::{Item, SplitDirection, Workspace}; @@ -56,27 +56,25 @@ pub fn init(cx: &mut App) { impl CsvPreviewView { pub(crate) fn sync_column_widths(&self, cx: &mut Context) { - // plus 1 for the rows column + // plus 1 for the row identifier column let cols = self.engine.contents.headers.cols() + 1; - let remaining_col_number = cols.saturating_sub(1); - let fraction = if remaining_col_number > 0 { - 1. / remaining_col_number as f32 - } else { - 1. - }; - let mut widths = vec![DefiniteLength::Fraction(fraction); cols]; let line_number_width = self.calculate_row_identifier_column_width(); - widths[0] = DefiniteLength::Absolute(AbsoluteLength::Pixels(line_number_width.into())); + + let mut widths: Vec = vec![AbsoluteLength::Pixels(px(150.)); cols]; + widths[0] = AbsoluteLength::Pixels(px(line_number_width)); let mut resize_behaviors = vec![TableResizeBehavior::Resizable; cols]; resize_behaviors[0] = TableResizeBehavior::None; self.column_widths.widths.update(cx, |state, _cx| { - if state.cols() != cols - || state.initial_widths().as_slice() != widths.as_slice() - || state.resize_behavior().as_slice() != resize_behaviors.as_slice() - { - *state = RedistributableColumnsState::new(cols, widths, resize_behaviors); + if state.cols() != cols { + *state = ResizableColumnsState::new(cols, widths, resize_behaviors); + } else { + state.set_column_configuration( + 0, + AbsoluteLength::Pixels(px(line_number_width)), + TableResizeBehavior::None, + ); } }); } @@ -207,7 +205,7 @@ impl CsvPreviewView { // Update list state with filtered row count let visible_rows = self.engine.d2d_mapping().visible_row_count(); - self.list_state = gpui::ListState::new(visible_rows, ListAlignment::Top, px(1.)); + self.list_state = gpui::ListState::new(visible_rows, ListAlignment::Top, px(100.)); } pub fn resolve_active_item_as_csv_editor( @@ -313,16 +311,16 @@ impl PerformanceMetrics { /// Holds state of column widths for a table component in CSV preview. pub(crate) struct ColumnWidths { - pub widths: Entity, + pub widths: Entity, } impl ColumnWidths { pub(crate) fn new(cx: &mut Context, cols: usize) -> Self { Self { widths: cx.new(|_cx| { - RedistributableColumnsState::new( + ResizableColumnsState::new( cols, - vec![ui::DefiniteLength::Fraction(1.0 / cols as f32); cols], + vec![AbsoluteLength::Pixels(px(150.)); cols], vec![ui::TableResizeBehavior::Resizable; cols], ) }), diff --git a/crates/csv_preview/src/renderer/render_table.rs b/crates/csv_preview/src/renderer/render_table.rs index fb3d7e5fc603ba5b109319cfb19466dc3ad7652f..71bb9b84c8955c7fbf40bfb5587f8a9aa4230ba2 100644 --- a/crates/csv_preview/src/renderer/render_table.rs +++ b/crates/csv_preview/src/renderer/render_table.rs @@ -1,9 +1,7 @@ use crate::types::TableCell; use gpui::{AnyElement, Entity}; use std::ops::Range; -use ui::{ - ColumnWidthConfig, RedistributableColumnsState, Table, UncheckedTableRow, div, prelude::*, -}; +use ui::{ColumnWidthConfig, ResizableColumnsState, Table, UncheckedTableRow, div, prelude::*}; use crate::{ CsvPreviewView, @@ -13,10 +11,10 @@ use crate::{ impl CsvPreviewView { /// Creates a new table. - /// Column number is derived from the `RedistributableColumnsState` entity. + /// Column number is derived from the `ResizableColumnsState` entity. pub(crate) fn create_table( &self, - current_widths: &Entity, + current_widths: &Entity, cx: &mut Context, ) -> AnyElement { self.create_table_inner(self.engine.contents.rows.len(), current_widths, cx) @@ -25,7 +23,7 @@ impl CsvPreviewView { fn create_table_inner( &self, row_count: usize, - current_widths: &Entity, + current_widths: &Entity, cx: &mut Context, ) -> AnyElement { let cols = current_widths.read(cx).cols(); @@ -54,7 +52,7 @@ impl CsvPreviewView { Table::new(cols) .interactable(&self.table_interaction_state) .striped() - .width_config(ColumnWidthConfig::redistributable(current_widths.clone())) + .width_config(ColumnWidthConfig::Resizable(current_widths.clone())) .header(headers) .disable_base_style() .map(|table| { diff --git a/crates/csv_preview/src/settings.rs b/crates/csv_preview/src/settings.rs index e627b3cc994a84f54268a05ba17534789f631fe0..9c64f6e9cfc8ff1cc570ffd9affbce1fd39eba23 100644 --- a/crates/csv_preview/src/settings.rs +++ b/crates/csv_preview/src/settings.rs @@ -1,10 +1,10 @@ #[derive(Default, Clone, Copy)] pub enum RowRenderMechanism { - /// Default behaviour - #[default] - VariableList, - /// More performance oriented, but all rows are same height + /// More correct for multiline content, but slower. #[allow(dead_code)] // Will be used when settings ui is added + VariableList, + /// Default behaviour for now while resizable columns are being stabilized. + #[default] UniformList, } diff --git a/crates/git_graph/src/git_graph.rs b/crates/git_graph/src/git_graph.rs index 7594a206f14705bf47a673dee9abefad5a3446de..370da2a8394f396138d5584d661488f75c3cf1ba 100644 --- a/crates/git_graph/src/git_graph.rs +++ b/crates/git_graph/src/git_graph.rs @@ -2564,7 +2564,8 @@ impl Render for GitGraph { this.child(self.render_loading_spinner(cx)) }) } else { - let header_resize_info = HeaderResizeInfo::from_state(&self.column_widths, cx); + let header_resize_info = + HeaderResizeInfo::from_redistributable(&self.column_widths, cx); let header_context = TableRenderContext::for_column_widths( Some(self.column_widths.read(cx).widths_to_render()), true, diff --git a/crates/ui/src/components/data_table.rs b/crates/ui/src/components/data_table.rs index e5a14a3ddabc0d918bfe6d6bcb077e32adeb6eb4..594cc188f5489e2901d1501aca3af36d5273fefd 100644 --- a/crates/ui/src/components/data_table.rs +++ b/crates/ui/src/components/data_table.rs @@ -1,22 +1,22 @@ use std::{ops::Range, rc::Rc}; -use gpui::{ - DefiniteLength, 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, Context, Div, ElementId, FixedWidth as _, FluentBuilder as _, HeaderResizeInfo, - Indicator, InteractiveElement, IntoElement, ParentElement, Pixels, RedistributableColumnsState, - RegisterComponent, RenderOnce, ScrollAxes, ScrollableHandle, Scrollbars, SharedString, - StatefulInteractiveElement, Styled, StyledExt as _, StyledTypography, Window, WithScrollbar, - bind_redistributable_columns, div, example_group_with_title, h_flex, px, + ComponentScope, Context, Div, DraggedColumn, ElementId, FixedWidth as _, FluentBuilder as _, + HeaderResizeInfo, Indicator, InteractiveElement, IntoElement, ParentElement, Pixels, + RESIZE_DIVIDER_WIDTH, RedistributableColumnsState, RegisterComponent, RenderOnce, ScrollAxes, + ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled, StyledExt as _, + StyledTypography, TableResizeBehavior, Window, WithScrollbar, bind_redistributable_columns, + div, example_group_with_title, h_flex, px, render_column_resize_divider, render_redistributable_columns_resize_handles, single_example, table_row::{IntoTableRow as _, TableRow}, v_flex, }; +use gpui::{ + AbsoluteLength, DefiniteLength, DragMoveEvent, Entity, EntityId, FocusHandle, Length, + ListHorizontalSizingBehavior, ListSizingBehavior, ListState, Point, ScrollHandle, Stateful, + UniformListScrollHandle, WeakEntity, list, transparent_black, uniform_list, +}; pub mod table_row; #[cfg(test)] @@ -26,6 +26,96 @@ mod tests; /// Will be converted into `TableRow` internally pub type UncheckedTableRow = Vec; +/// State for independently resizable columns (spreadsheet-style). +/// +/// Each column has its own absolute width; dragging a resize handle changes only +/// that column's width, growing or shrinking the overall table width. +pub struct ResizableColumnsState { + initial_widths: TableRow, + widths: TableRow, + resize_behavior: TableRow, +} + +impl ResizableColumnsState { + pub fn new( + cols: usize, + initial_widths: Vec>, + resize_behavior: Vec, + ) -> Self { + let widths: TableRow = initial_widths + .into_iter() + .map(Into::into) + .collect::>() + .into_table_row(cols); + Self { + initial_widths: widths.clone(), + widths, + resize_behavior: resize_behavior.into_table_row(cols), + } + } + + pub fn cols(&self) -> usize { + self.widths.cols() + } + + pub fn resize_behavior(&self) -> &TableRow { + &self.resize_behavior + } + + pub(crate) fn on_drag_move( + &mut self, + drag_event: &DragMoveEvent, + window: &mut Window, + cx: &mut Context, + ) { + let col_idx = drag_event.drag(cx).col_idx; + let rem_size = window.rem_size(); + let drag_x = drag_event.event.position.x - drag_event.bounds.left(); + + let left_edge: Pixels = self.widths.as_slice()[..col_idx] + .iter() + .map(|width| width.to_pixels(rem_size)) + .fold(px(0.), |acc, x| acc + x); + + let new_width = drag_x - left_edge; + let new_width = self.apply_min_size(new_width, self.resize_behavior[col_idx], rem_size); + + self.widths[col_idx] = AbsoluteLength::Pixels(new_width); + cx.notify(); + } + + pub fn set_column_configuration( + &mut self, + col_idx: usize, + width: impl Into, + resize_behavior: TableResizeBehavior, + ) { + let width = width.into(); + self.initial_widths[col_idx] = width; + self.widths[col_idx] = width; + self.resize_behavior[col_idx] = resize_behavior; + } + + pub fn reset_column_to_initial_width(&mut self, col_idx: usize) { + self.widths[col_idx] = self.initial_widths[col_idx]; + } + + fn apply_min_size( + &self, + width: Pixels, + behavior: TableResizeBehavior, + rem_size: Pixels, + ) -> Pixels { + match behavior.min_size() { + Some(min_rems) => { + let min_px = rem_size * min_rems; + width.max(min_px) + } + None => width, + } + } +} + struct UniformListData { render_list_of_rows_fn: Box, &mut Window, &mut App) -> Vec>>, @@ -71,6 +161,7 @@ impl TableContents { pub struct TableInteractionState { pub focus_handle: FocusHandle, pub scroll_handle: UniformListScrollHandle, + pub horizontal_scroll_handle: ScrollHandle, pub custom_scrollbar: Option, } @@ -79,6 +170,7 @@ impl TableInteractionState { Self { focus_handle: cx.focus_handle(), scroll_handle: UniformListScrollHandle::new(), + horizontal_scroll_handle: ScrollHandle::new(), custom_scrollbar: None, } } @@ -120,6 +212,9 @@ pub enum ColumnWidthConfig { columns_state: Entity, table_width: Option, }, + /// Independently resizable columns — dragging changes absolute column widths + /// and thus the overall table width. Like spreadsheets. + Resizable(Entity), } pub enum StaticColumnWidths { @@ -184,24 +279,52 @@ impl ColumnWidthConfig { columns_state: entity, .. } => Some(entity.read(cx).widths_to_render()), + ColumnWidthConfig::Resizable(entity) => { + let state = entity.read(cx); + Some( + state + .widths + .map_cloned(|abs| Length::Definite(DefiniteLength::Absolute(abs))), + ) + } } } /// Table-level width. - pub fn table_width(&self) -> Option { + pub fn table_width(&self, window: &Window, cx: &App) -> Option { match self { ColumnWidthConfig::Static { table_width, .. } | ColumnWidthConfig::Redistributable { table_width, .. } => { table_width.map(Length::Definite) } + ColumnWidthConfig::Resizable(entity) => { + let state = entity.read(cx); + let rem_size = window.rem_size(); + let total: Pixels = state + .widths + .as_slice() + .iter() + .map(|abs| abs.to_pixels(rem_size)) + .fold(px(0.), |acc, x| acc + x); + Some(Length::Definite(DefiniteLength::Absolute( + AbsoluteLength::Pixels(total), + ))) + } } } /// ListHorizontalSizingBehavior for uniform_list. - pub fn list_horizontal_sizing(&self) -> ListHorizontalSizingBehavior { - match self.table_width() { - Some(_) => ListHorizontalSizingBehavior::Unconstrained, - None => ListHorizontalSizingBehavior::FitList, + pub fn list_horizontal_sizing( + &self, + window: &Window, + cx: &App, + ) -> ListHorizontalSizingBehavior { + match self { + ColumnWidthConfig::Resizable(_) => ListHorizontalSizingBehavior::FitList, + _ => match self.table_width(window, cx) { + Some(_) => ListHorizontalSizingBehavior::Unconstrained, + None => ListHorizontalSizingBehavior::FitList, + }, } } } @@ -515,13 +638,7 @@ pub fn render_table_header( if info.resize_behavior[header_idx].is_resizable() { this.on_click(move |event, window, cx| { if event.click_count() > 1 { - info.columns_state - .update(cx, |column, _| { - column.reset_column_to_initial_width( - header_idx, window, - ); - }) - .ok(); + info.reset_column(header_idx, window, cx); } }) } else { @@ -572,6 +689,68 @@ impl TableRenderContext { } } +fn render_resize_handles_resizable( + columns_state: &Entity, + window: &mut Window, + cx: &mut App, +) -> AnyElement { + let (widths, resize_behavior) = { + let state = columns_state.read(cx); + (state.widths.clone(), state.resize_behavior.clone()) + }; + + let rem_size = window.rem_size(); + let resize_behavior = Rc::new(resize_behavior); + let n_cols = widths.cols(); + let mut dividers: Vec = Vec::with_capacity(n_cols); + let mut accumulated_px = px(0.); + + for col_idx in 0..n_cols { + let col_width_px = widths[col_idx].to_pixels(rem_size); + accumulated_px = accumulated_px + col_width_px; + + // Add a resize divider after every column, including the last. + // For the last column the divider is pulled 1px inward so it isn't clipped + // by the overflow_hidden content container. + { + let divider_left = if col_idx + 1 == n_cols { + accumulated_px - px(RESIZE_DIVIDER_WIDTH) + } else { + accumulated_px + }; + let divider = div().id(col_idx).absolute().top_0().left(divider_left); + let entity_id = columns_state.entity_id(); + let on_reset: Rc = { + let columns_state = columns_state.clone(); + Rc::new(move |_window, cx| { + columns_state.update(cx, |state, cx| { + state.reset_column_to_initial_width(col_idx); + cx.notify(); + }); + }) + }; + dividers.push(render_column_resize_divider( + divider, + col_idx, + resize_behavior[col_idx].is_resizable(), + entity_id, + on_reset, + None, + window, + cx, + )); + } + } + + div() + .id("resize-handles") + .absolute() + .inset_0() + .w_full() + .children(dividers) + .into_any_element() +} + impl RenderOnce for Table { fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement { let table_context = TableRenderContext::new(&self, cx); @@ -582,36 +761,47 @@ impl RenderOnce for Table { .as_ref() .and_then(|_| match &self.column_width_config { ColumnWidthConfig::Redistributable { columns_state, .. } => { - Some(HeaderResizeInfo::from_state(columns_state, cx)) + Some(HeaderResizeInfo::from_redistributable(columns_state, cx)) + } + ColumnWidthConfig::Resizable(entity) => { + Some(HeaderResizeInfo::from_resizable(entity, cx)) } _ => None, }); - let table_width = self.column_width_config.table_width(); - let horizontal_sizing = self.column_width_config.list_horizontal_sizing(); + let table_width = self.column_width_config.table_width(window, cx); + let horizontal_sizing = self.column_width_config.list_horizontal_sizing(window, cx); let no_rows_rendered = self.rows.is_empty(); - - // Extract redistributable entity for drag/drop/prepaint handlers - let redistributable_entity = - interaction_state - .as_ref() - .and_then(|_| match &self.column_width_config { - ColumnWidthConfig::Redistributable { - columns_state: entity, - .. - } => Some(entity.clone()), - _ => None, - }); - - let resize_handles = - interaction_state - .as_ref() - .and_then(|_| match &self.column_width_config { - ColumnWidthConfig::Redistributable { columns_state, .. } => Some( - render_redistributable_columns_resize_handles(columns_state, window, cx), + let variable_list_state = if let TableContents::VariableRowHeightList(data) = &self.rows { + Some(data.list_state.clone()) + } else { + None + }; + + let (redistributable_entity, resizable_entity, resize_handles) = + if let Some(_) = interaction_state.as_ref() { + match &self.column_width_config { + ColumnWidthConfig::Redistributable { columns_state, .. } => ( + Some(columns_state.clone()), + None, + Some(render_redistributable_columns_resize_handles( + columns_state, + window, + cx, + )), ), - _ => None, - }); + ColumnWidthConfig::Resizable(entity) => ( + None, + Some(entity.clone()), + Some(render_resize_handles_resizable(entity, window, cx)), + ), + _ => (None, None, None), + } + } else { + (None, None, None) + }; + + let is_resizable = resizable_entity.is_some(); let table = div() .when_some(table_width, |this, width| this.w(width)) @@ -629,6 +819,14 @@ impl RenderOnce for Table { .when_some(redistributable_entity, |this, widths| { bind_redistributable_columns(this, widths) }) + .when_some(resizable_entity, |this, entity| { + this.on_drag_move::(move |event, window, cx| { + if event.drag(cx).state_id != entity.entity_id() { + return; + } + entity.update(cx, |state, cx| state.on_drag_move(event, window, cx)); + }) + }) .child({ let content = div() .flex_grow() @@ -709,22 +907,7 @@ impl RenderOnce for Table { }) .when_some(resize_handles, |parent, handles| parent.child(handles)); - if let Some(state) = interaction_state.as_ref() { - let scrollbars = state - .read(cx) - .custom_scrollbar - .clone() - .unwrap_or_else(|| Scrollbars::new(ScrollAxes::Both)); - content - .custom_scrollbars( - scrollbars.tracked_scroll_handle(&state.read(cx).scroll_handle), - window, - cx, - ) - .into_any_element() - } else { - content.into_any_element() - } + content.into_any_element() }) .when_some( no_rows_rendered @@ -742,10 +925,52 @@ impl RenderOnce for Table { }, ); - if let Some(interaction_state) = interaction_state.as_ref() { - table - .track_focus(&interaction_state.read(cx).focus_handle) - .id(("table", interaction_state.entity_id())) + if let Some(state) = interaction_state.as_ref() { + // Resizable mode: wrap table in a horizontal scroll container first + let content = if is_resizable { + let mut h_scroll_container = div() + .id("table-h-scroll") + .overflow_x_scroll() + .flex_grow() + .h_full() + .track_scroll(&state.read(cx).horizontal_scroll_handle) + .child(table); + h_scroll_container.style().restrict_scroll_to_axis = Some(true); + div().size_full().child(h_scroll_container) + } else { + table + }; + + // Attach vertical scrollbars (converts Div → Stateful
) + let scrollbars = state + .read(cx) + .custom_scrollbar + .clone() + .unwrap_or_else(|| Scrollbars::new(ScrollAxes::Both)); + let mut content = if let Some(list_state) = variable_list_state { + content.custom_scrollbars(scrollbars.tracked_scroll_handle(&list_state), window, cx) + } else { + content.custom_scrollbars( + scrollbars.tracked_scroll_handle(&state.read(cx).scroll_handle), + window, + cx, + ) + }; + + // Add horizontal scrollbar when in resizable mode + if is_resizable { + content = content.custom_scrollbars( + Scrollbars::new(ScrollAxes::Horizontal) + .tracked_scroll_handle(&state.read(cx).horizontal_scroll_handle), + window, + cx, + ); + } + content.style().restrict_scroll_to_axis = Some(true); + + content + .track_focus(&state.read(cx).focus_handle) + .id(("table", state.entity_id())) .into_any_element() } else { table.into_any_element() diff --git a/crates/ui/src/components/redistributable_columns.rs b/crates/ui/src/components/redistributable_columns.rs index cd22c31e19736e72e5d88676178053b49a3e65fd..cb5da35d565185fc838203e4663f25db818769f1 100644 --- a/crates/ui/src/components/redistributable_columns.rs +++ b/crates/ui/src/components/redistributable_columns.rs @@ -1,23 +1,32 @@ use std::rc::Rc; use gpui::{ - AbsoluteLength, AppContext as _, Bounds, DefiniteLength, DragMoveEvent, Empty, Entity, Length, - WeakEntity, + AbsoluteLength, AppContext as _, Bounds, DefiniteLength, DragMoveEvent, Empty, Entity, + EntityId, Length, Stateful, WeakEntity, }; use itertools::intersperse_with; -use super::data_table::table_row::{IntoTableRow as _, TableRow}; +use super::data_table::{ + ResizableColumnsState, + table_row::{IntoTableRow as _, TableRow}, +}; use crate::{ ActiveTheme as _, AnyElement, App, Context, Div, FluentBuilder as _, InteractiveElement, IntoElement, ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, div, h_flex, px, }; -const RESIZE_COLUMN_WIDTH: f32 = 8.0; -const RESIZE_DIVIDER_WIDTH: f32 = 1.0; +pub(crate) const RESIZE_COLUMN_WIDTH: f32 = 8.0; +pub(crate) const RESIZE_DIVIDER_WIDTH: f32 = 1.0; +/// Drag payload for column resize handles. +/// Includes the `EntityId` of the owning column state so that +/// `on_drag_move` handlers on unrelated tables ignore the event. #[derive(Debug)] -struct DraggedColumn(usize); +pub(crate) struct DraggedColumn { + pub(crate) col_idx: usize, + pub(crate) state_id: EntityId, +} #[derive(Debug, Copy, Clone, PartialEq)] pub enum TableResizeBehavior { @@ -40,20 +49,56 @@ impl TableResizeBehavior { } } +#[derive(Clone)] +pub(crate) enum ColumnsStateRef { + Redistributable(WeakEntity), + Resizable(WeakEntity), +} + #[derive(Clone)] pub struct HeaderResizeInfo { - pub columns_state: WeakEntity, + pub(crate) columns_state: ColumnsStateRef, pub resize_behavior: TableRow, } impl HeaderResizeInfo { - pub fn from_state(columns_state: &Entity, cx: &App) -> Self { + pub fn from_redistributable( + columns_state: &Entity, + cx: &App, + ) -> Self { let resize_behavior = columns_state.read(cx).resize_behavior().clone(); Self { - columns_state: columns_state.downgrade(), + columns_state: ColumnsStateRef::Redistributable(columns_state.downgrade()), resize_behavior, } } + + pub fn from_resizable(columns_state: &Entity, cx: &App) -> Self { + let resize_behavior = columns_state.read(cx).resize_behavior().clone(); + Self { + columns_state: ColumnsStateRef::Resizable(columns_state.downgrade()), + resize_behavior, + } + } + + pub fn reset_column(&self, col_idx: usize, window: &mut Window, cx: &mut App) { + match &self.columns_state { + ColumnsStateRef::Redistributable(weak) => { + weak.update(cx, |state, cx| { + state.reset_column_to_initial_width(col_idx, window); + cx.notify(); + }) + .ok(); + } + ColumnsStateRef::Resizable(weak) => { + weak.update(cx, |state, cx| { + state.reset_column_to_initial_width(col_idx); + cx.notify(); + }) + .ok(); + } + } + } } pub struct RedistributableColumnsState { @@ -237,7 +282,7 @@ impl RedistributableColumnsState { let mut col_position = 0.0; let rem_size = window.rem_size(); - let col_idx = drag_event.drag(cx).0; + let col_idx = drag_event.drag(cx).col_idx; let divider_width = Self::get_fraction( &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_DIVIDER_WIDTH))), @@ -348,6 +393,9 @@ pub fn bind_redistributable_columns( .on_drag_move::({ let columns_state = columns_state.clone(); move |event, window, cx| { + if event.drag(cx).state_id != columns_state.entity_id() { + return; + } columns_state.update(cx, |columns, cx| { columns.on_drag_move(event, window, cx); }); @@ -394,67 +442,34 @@ pub fn render_redistributable_columns_resize_handles( let columns_state = columns_state.clone(); column_ix += 1; - window.with_id(current_column_ix, |window| { - let mut resize_divider = div() - .id(current_column_ix) - .relative() - .top_0() - .w(px(RESIZE_DIVIDER_WIDTH)) - .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 resize_behavior[current_column_ix].is_resizable() { - let is_highlighted = window.use_state(cx, |_window, _cx| false); - - resize_divider = resize_divider.when(*is_highlighted.read(cx), |div| { - div.bg(cx.theme().colors().border_focused) - }); - - resize_handle = resize_handle - .on_hover({ - let is_highlighted = is_highlighted.clone(); - move |&was_hovered, _, cx| is_highlighted.write(cx, was_hovered) - }) - .cursor_col_resize() - .on_click({ - let columns_state = columns_state.clone(); - move |event, window, cx| { - if event.click_count() >= 2 { - columns_state.update(cx, |columns, _| { - columns.reset_column_to_initial_width( - current_column_ix, - window, - ); - }); - } - - cx.stop_propagation(); - } - }) - .on_drag(DraggedColumn(current_column_ix), { - let is_highlighted = is_highlighted.clone(); - move |_, _offset, _window, cx| { - is_highlighted.write(cx, true); - cx.new(|_cx| Empty) - } - }) - .on_drop::(move |_, _, cx| { - is_highlighted.write(cx, false); - columns_state.update(cx, |state, _| { - state.commit_preview(); - }); + { + let divider = div().id(current_column_ix).relative().top_0(); + let entity_id = columns_state.entity_id(); + let on_reset: Rc = { + let columns_state = columns_state.clone(); + Rc::new(move |window, cx| { + columns_state.update(cx, |columns, cx| { + columns.reset_column_to_initial_width(current_column_ix, window); + cx.notify(); }); - } - - resize_divider.child(resize_handle).into_any_element() - }) + }) + }; + let on_drag_end: Option> = { + Some(Rc::new(move |cx| { + columns_state.update(cx, |state, _| state.commit_preview()); + })) + }; + render_column_resize_divider( + divider, + current_column_ix, + resize_behavior[current_column_ix].is_resizable(), + entity_id, + on_reset, + on_drag_end, + window, + cx, + ) + } }, ); @@ -467,6 +482,83 @@ pub fn render_redistributable_columns_resize_handles( .into_any_element() } +/// Builds a single column resize divider with an interactive drag handle. +/// +/// The caller provides: +/// - `divider`: a pre-positioned divider element (with absolute or relative positioning) +/// - `col_idx`: which column this divider is for +/// - `is_resizable`: whether the column supports resizing +/// - `entity_id`: the `EntityId` of the owning column state (for the drag payload) +/// - `on_reset`: called on double-click to reset the column to its initial width +/// - `on_drag_end`: called when the drag ends (e.g. to commit preview widths) +pub(crate) fn render_column_resize_divider( + divider: Stateful
, + col_idx: usize, + is_resizable: bool, + entity_id: EntityId, + on_reset: Rc, + on_drag_end: Option>, + window: &mut Window, + cx: &mut App, +) -> AnyElement { + window.with_id(col_idx, |window| { + let mut resize_divider = divider.w(px(RESIZE_DIVIDER_WIDTH)).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 is_resizable { + let is_highlighted = window.use_state(cx, |_window, _cx| false); + + resize_divider = resize_divider.when(*is_highlighted.read(cx), |div| { + div.bg(cx.theme().colors().border_focused) + }); + + resize_handle = resize_handle + .on_hover({ + let is_highlighted = is_highlighted.clone(); + move |&was_hovered, _, cx| is_highlighted.write(cx, was_hovered) + }) + .cursor_col_resize() + .on_click(move |event, window, cx| { + if event.click_count() >= 2 { + on_reset(window, cx); + } + cx.stop_propagation(); + }) + .on_drag( + DraggedColumn { + col_idx, + state_id: entity_id, + }, + { + let is_highlighted = is_highlighted.clone(); + move |_, _offset, _window, cx| { + is_highlighted.write(cx, true); + cx.new(|_cx| Empty) + } + }, + ) + .on_drop::(move |_, _, cx| { + is_highlighted.write(cx, false); + if let Some(on_drag_end) = &on_drag_end { + on_drag_end(cx); + } + }); + } + + resize_divider.child(resize_handle).into_any_element() + }) +} + fn resize_spacer(width: Length) -> Div { div().w(width).h_full() }