diff --git a/crates/csv_preview/src/csv_preview.rs b/crates/csv_preview/src/csv_preview.rs index b0b6ad4186758fd33693d5ee29bd2f0d4d28b816..c38cefb2456b3f44e3cac61b02294ab1ed1e79f4 100644 --- a/crates/csv_preview/src/csv_preview.rs +++ b/crates/csv_preview/src/csv_preview.rs @@ -9,7 +9,10 @@ use std::{ }; use crate::table_data_engine::TableDataEngine; -use ui::{SharedString, TableColumnWidths, TableInteractionState, prelude::*}; +use ui::{ + AbsoluteLength, DefiniteLength, RedistributableColumnsState, SharedString, + TableInteractionState, TableResizeBehavior, prelude::*, +}; use workspace::{Item, SplitDirection, Workspace}; use crate::{parser::EditorState, settings::CsvPreviewSettings, types::TableLikeContent}; @@ -52,6 +55,32 @@ pub fn init(cx: &mut App) { } impl CsvPreviewView { + pub(crate) fn sync_column_widths(&self, cx: &mut Context) { + // plus 1 for the rows 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 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); + } + }); + } + pub fn register(workspace: &mut Workspace) { workspace.register_action_renderer(|div, _, _, cx| { div.when(cx.has_flag::(), |div| { @@ -286,18 +315,19 @@ 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| TableColumnWidths::new(cols, cx)), + widths: cx.new(|_cx| { + RedistributableColumnsState::new( + cols, + vec![ui::DefiniteLength::Fraction(1.0 / cols as f32); cols], + vec![ui::TableResizeBehavior::Resizable; cols], + ) + }), } } - /// Replace the current `TableColumnWidths` entity with a new one for the given column count. - pub(crate) fn replace(&self, cx: &mut Context, cols: usize) { - self.widths - .update(cx, |entity, cx| *entity = TableColumnWidths::new(cols, cx)); - } } diff --git a/crates/csv_preview/src/parser.rs b/crates/csv_preview/src/parser.rs index b087404e0ebbd13cdaf20cab692f5470ea6ce292..efa3573d7aa53d97e2801ff00feb4665072830f4 100644 --- a/crates/csv_preview/src/parser.rs +++ b/crates/csv_preview/src/parser.rs @@ -80,11 +80,8 @@ impl CsvPreviewView { .insert("Parsing", (parse_duration, Instant::now())); log::debug!("Parsed {} rows", parsed_csv.rows.len()); - // Update table width so it can be rendered properly - let cols = parsed_csv.headers.cols(); - view.column_widths.replace(cx, cols + 1); // Add 1 for the line number column - view.engine.contents = parsed_csv; + view.sync_column_widths(cx); view.last_parse_end_time = Some(parse_end_time); view.apply_filter_sort(); diff --git a/crates/csv_preview/src/renderer/render_table.rs b/crates/csv_preview/src/renderer/render_table.rs index 0cc3bc3c46fb24570b3c99c9121dff3860c6b820..fb3d7e5fc603ba5b109319cfb19466dc3ad7652f 100644 --- a/crates/csv_preview/src/renderer/render_table.rs +++ b/crates/csv_preview/src/renderer/render_table.rs @@ -1,11 +1,9 @@ use crate::types::TableCell; use gpui::{AnyElement, Entity}; use std::ops::Range; -use ui::Table; -use ui::TableColumnWidths; -use ui::TableResizeBehavior; -use ui::UncheckedTableRow; -use ui::{DefiniteLength, div, prelude::*}; +use ui::{ + ColumnWidthConfig, RedistributableColumnsState, Table, UncheckedTableRow, div, prelude::*, +}; use crate::{ CsvPreviewView, @@ -15,44 +13,22 @@ use crate::{ impl CsvPreviewView { /// Creates a new table. - /// Column number is derived from the `TableColumnWidths` entity. + /// Column number is derived from the `RedistributableColumnsState` entity. pub(crate) fn create_table( &self, - current_widths: &Entity, + current_widths: &Entity, cx: &mut Context, ) -> AnyElement { - let cols = current_widths.read(cx).cols(); - let remaining_col_number = cols - 1; - let fraction = if remaining_col_number > 0 { - 1. / remaining_col_number as f32 - } else { - 1. // only column with line numbers is present. Put 100%, but it will be overwritten anyways :D - }; - 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 resize_behaviors = vec![TableResizeBehavior::Resizable; cols]; - resize_behaviors[0] = TableResizeBehavior::None; - - self.create_table_inner( - self.engine.contents.rows.len(), - widths, - resize_behaviors, - current_widths, - cx, - ) + self.create_table_inner(self.engine.contents.rows.len(), current_widths, cx) } fn create_table_inner( &self, row_count: usize, - widths: UncheckedTableRow, - resize_behaviors: UncheckedTableRow, - current_widths: &Entity, + current_widths: &Entity, cx: &mut Context, ) -> AnyElement { - let cols = widths.len(); + let cols = current_widths.read(cx).cols(); // Create headers array with interactive elements let mut headers = Vec::with_capacity(cols); @@ -78,8 +54,7 @@ impl CsvPreviewView { Table::new(cols) .interactable(&self.table_interaction_state) .striped() - .column_widths(widths) - .resizable_columns(resize_behaviors, current_widths, cx) + .width_config(ColumnWidthConfig::redistributable(current_widths.clone())) .header(headers) .disable_base_style() .map(|table| { diff --git a/crates/csv_preview/src/renderer/row_identifiers.rs b/crates/csv_preview/src/renderer/row_identifiers.rs index a122aa9bf3d803b9deb9c6211e117ba4aa593d93..fc8bf68845fd41917e7d60bf5f9276295534c902 100644 --- a/crates/csv_preview/src/renderer/row_identifiers.rs +++ b/crates/csv_preview/src/renderer/row_identifiers.rs @@ -139,6 +139,7 @@ impl CsvPreviewView { RowIdentifiers::SrcLines => RowIdentifiers::RowNum, RowIdentifiers::RowNum => RowIdentifiers::SrcLines, }; + this.sync_column_widths(cx); cx.notify(); }); }), diff --git a/crates/csv_preview/src/renderer/table_cell.rs b/crates/csv_preview/src/renderer/table_cell.rs index 32900ab77708936e218e9af10a4de5fba796e6a7..733488110fbcdb39761b150a74c135426ca6514a 100644 --- a/crates/csv_preview/src/renderer/table_cell.rs +++ b/crates/csv_preview/src/renderer/table_cell.rs @@ -53,7 +53,6 @@ fn create_table_cell( .px_1() .bg(cx.theme().colors().editor_background) .border_b_1() - .border_r_1() .border_color(cx.theme().colors().border_variant) .map(|div| match vertical_alignment { VerticalAlignment::Top => div.items_start(), diff --git a/crates/git_graph/src/git_graph.rs b/crates/git_graph/src/git_graph.rs index d169ba686098dddd4881915ece11c8a97148affa..a66e840b2f41405b5c76f3999ea14414daa19d39 100644 --- a/crates/git_graph/src/git_graph.rs +++ b/crates/git_graph/src/git_graph.rs @@ -41,9 +41,9 @@ use theme::AccentColors; use theme_settings::ThemeSettings; use time::{OffsetDateTime, UtcOffset, format_description::BorrowedFormatItem}; use ui::{ - ButtonLike, Chip, CommonAnimationExt as _, ContextMenu, DiffStat, Divider, HighlightedLabel, - ScrollableHandle, Table, TableColumnWidths, TableInteractionState, TableResizeBehavior, - Tooltip, WithScrollbar, prelude::*, + ButtonLike, Chip, ColumnWidthConfig, CommonAnimationExt as _, ContextMenu, DiffStat, Divider, + HighlightedLabel, RedistributableColumnsState, ScrollableHandle, Table, TableInteractionState, + TableResizeBehavior, Tooltip, WithScrollbar, prelude::*, }; use workspace::{ Workspace, @@ -901,7 +901,7 @@ pub struct GitGraph { context_menu: Option<(Entity, Point, Subscription)>, row_height: Pixels, table_interaction_state: Entity, - table_column_widths: Entity, + table_column_widths: Entity, horizontal_scroll_offset: Pixels, graph_viewport_width: Pixels, selected_entry_idx: Option, @@ -972,7 +972,23 @@ impl GitGraph { }); let table_interaction_state = cx.new(|cx| TableInteractionState::new(cx)); - let table_column_widths = cx.new(|cx| TableColumnWidths::new(4, cx)); + let table_column_widths = cx.new(|_cx| { + RedistributableColumnsState::new( + 4, + vec![ + DefiniteLength::Fraction(0.72), + DefiniteLength::Fraction(0.12), + DefiniteLength::Fraction(0.10), + DefiniteLength::Fraction(0.06), + ], + vec![ + TableResizeBehavior::Resizable, + TableResizeBehavior::Resizable, + TableResizeBehavior::Resizable, + TableResizeBehavior::Resizable, + ], + ) + }); let mut row_height = Self::row_height(cx); cx.observe_global_in::(window, move |this, _window, cx| { @@ -2459,11 +2475,6 @@ impl Render for GitGraph { self.search_state.state = QueryState::Empty; self.search(query, cx); } - let description_width_fraction = 0.72; - let date_width_fraction = 0.12; - let author_width_fraction = 0.10; - let commit_width_fraction = 0.06; - let (commit_count, is_loading) = match self.graph_data.max_commit_count { AllCommitCount::Loaded(count) => (count, true), AllCommitCount::NotLoaded => { @@ -2523,7 +2534,10 @@ impl Render for GitGraph { .flex_col() .child( div() - .p_2() + .flex() + .items_center() + .px_1() + .py_0p5() .border_b_1() .whitespace_nowrap() .border_color(cx.theme().colors().border) @@ -2565,25 +2579,9 @@ impl Render for GitGraph { Label::new("Author").color(Color::Muted).into_any_element(), Label::new("Commit").color(Color::Muted).into_any_element(), ]) - .column_widths( - [ - DefiniteLength::Fraction(description_width_fraction), - DefiniteLength::Fraction(date_width_fraction), - DefiniteLength::Fraction(author_width_fraction), - DefiniteLength::Fraction(commit_width_fraction), - ] - .to_vec(), - ) - .resizable_columns( - vec![ - TableResizeBehavior::Resizable, - TableResizeBehavior::Resizable, - TableResizeBehavior::Resizable, - TableResizeBehavior::Resizable, - ], - &self.table_column_widths, - cx, - ) + .width_config(ColumnWidthConfig::redistributable( + self.table_column_widths.clone(), + )) .map_row(move |(index, row), window, cx| { let is_selected = selected_entry_idx == Some(index); let is_hovered = hovered_entry_idx == Some(index); diff --git a/crates/keymap_editor/src/keymap_editor.rs b/crates/keymap_editor/src/keymap_editor.rs index 6a02289353f7fc0df8fd2b3fd99313d2ce650951..2e3172dac95fe91ed5b2a5a187ca57bbd9154fae 100644 --- a/crates/keymap_editor/src/keymap_editor.rs +++ b/crates/keymap_editor/src/keymap_editor.rs @@ -31,10 +31,10 @@ use settings::{ BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets, infer_json_indent_size, }; use ui::{ - ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, IconPosition, - Indicator, Modal, ModalFooter, ModalHeader, ParentElement as _, PopoverMenu, Render, Section, - SharedString, Styled as _, Table, TableColumnWidths, TableInteractionState, - TableResizeBehavior, Tooltip, Window, prelude::*, + ActiveTheme as _, App, Banner, BorrowAppContext, ColumnWidthConfig, ContextMenu, + IconButtonShape, IconPosition, Indicator, Modal, ModalFooter, ModalHeader, ParentElement as _, + PopoverMenu, RedistributableColumnsState, Render, Section, SharedString, Styled as _, Table, + TableInteractionState, TableResizeBehavior, Tooltip, Window, prelude::*, }; use ui_input::InputField; use util::ResultExt; @@ -450,7 +450,7 @@ struct KeymapEditor { context_menu: Option<(Entity, Point, Subscription)>, previous_edit: Option, humanized_action_names: HumanizedActionNameCache, - current_widths: Entity, + current_widths: Entity, show_hover_menus: bool, actions_with_schemas: HashSet<&'static str>, /// In order for the JSON LSP to run in the actions arguments editor, we @@ -623,7 +623,27 @@ impl KeymapEditor { actions_with_schemas: HashSet::default(), action_args_temp_dir: None, action_args_temp_dir_worktree: None, - current_widths: cx.new(|cx| TableColumnWidths::new(COLS, cx)), + current_widths: cx.new(|_cx| { + RedistributableColumnsState::new( + COLS, + vec![ + DefiniteLength::Absolute(AbsoluteLength::Pixels(px(36.))), + DefiniteLength::Fraction(0.25), + DefiniteLength::Fraction(0.20), + DefiniteLength::Fraction(0.14), + DefiniteLength::Fraction(0.45), + DefiniteLength::Fraction(0.08), + ], + vec![ + TableResizeBehavior::None, + TableResizeBehavior::Resizable, + TableResizeBehavior::Resizable, + TableResizeBehavior::Resizable, + TableResizeBehavior::Resizable, + TableResizeBehavior::Resizable, + ], + ) + }), }; this.on_keymap_changed(window, cx); @@ -2095,26 +2115,9 @@ impl Render for KeymapEditor { let this = cx.entity(); move |window, cx| this.read(cx).render_no_matches_hint(window, cx) }) - .column_widths(vec![ - DefiniteLength::Absolute(AbsoluteLength::Pixels(px(36.))), - DefiniteLength::Fraction(0.25), - DefiniteLength::Fraction(0.20), - DefiniteLength::Fraction(0.14), - DefiniteLength::Fraction(0.45), - DefiniteLength::Fraction(0.08), - ]) - .resizable_columns( - vec![ - TableResizeBehavior::None, - TableResizeBehavior::Resizable, - TableResizeBehavior::Resizable, - TableResizeBehavior::Resizable, - TableResizeBehavior::Resizable, - TableResizeBehavior::Resizable, // this column doesn't matter - ], - &self.current_widths, - cx, - ) + .width_config(ColumnWidthConfig::redistributable( + self.current_widths.clone(), + )) .header(vec!["", "Action", "Arguments", "Keystrokes", "Context", "Source"]) .uniform_list( "keymap-editor-table", diff --git a/crates/ui/src/components/data_table.rs b/crates/ui/src/components/data_table.rs index 3da30838ca8313b68608e432ce1e76870157c1fd..2012defc47d9cccea87849fa41470ad1183b552f 100644 --- a/crates/ui/src/components/data_table.rs +++ b/crates/ui/src/components/data_table.rs @@ -1,14 +1,15 @@ 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, + AbsoluteLength, AppContext as _, DefiniteLength, DragMoveEvent, Entity, EntityId, FocusHandle, + Length, ListHorizontalSizingBehavior, ListSizingBehavior, ListState, Point, Stateful, + UniformListScrollHandle, WeakEntity, list, transparent_black, uniform_list, }; +use itertools::intersperse_with; use crate::{ ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component, - ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, + ComponentScope, Context, 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, @@ -16,20 +17,20 @@ use crate::{ table_row::{IntoTableRow as _, TableRow}, v_flex, }; -use itertools::intersperse_with; pub mod table_row; #[cfg(test)] mod tests; const RESIZE_COLUMN_WIDTH: f32 = 8.0; +const RESIZE_DIVIDER_WIDTH: f32 = 1.0; /// Represents an unchecked table row, which is a vector of elements. /// Will be converted into `TableRow` internally pub type UncheckedTableRow = Vec; #[derive(Debug)] -struct DraggedColumn(usize); +pub(crate) struct DraggedColumn(pub(crate) usize); struct UniformListData { render_list_of_rows_fn: @@ -110,106 +111,103 @@ impl TableInteractionState { 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, - resizable_columns: &TableRow, - initial_sizes: &TableRow, - columns: Option>, - 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, - ); - }) - } +/// 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( + column_widths: &TableRow, + resizable_columns: &TableRow, + initial_sizes: &TableRow, + columns: Option>, + 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(); + + 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() + .id(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 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(); - }) + cx.stop_propagation(); }) - .on_drag(DraggedColumn(column_ix), |_, _offset, _window, cx| { - cx.new(|_cx| gpui::Empty) - }) - } + }) + .on_drag(DraggedColumn(column_ix), |_, _offset, _window, cx| { + cx.new(|_cx| gpui::Empty) + }) + } - column_ix += 1; - resize_divider.child(resize_handle).into_any_element() - }) - }); + 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() - } + h_flex() + .id("resize-handles") + .absolute() + .inset_0() + .w_full() + .children(dividers) + .into_any_element() } #[derive(Debug, Copy, Clone, PartialEq)] @@ -233,25 +231,181 @@ impl TableResizeBehavior { } } -pub struct TableColumnWidths { - widths: TableRow, - visible_widths: TableRow, - cached_bounds_width: Pixels, - initialized: bool, +pub enum ColumnWidthConfig { + /// Static column widths (no resize handles). + Static { + widths: StaticColumnWidths, + /// Controls widths of the whole table. + table_width: Option, + }, + /// Redistributable columns — dragging redistributes the fixed available space + /// among columns without changing the overall table width. + Redistributable { + columns_state: Entity, + table_width: Option, + }, +} + +pub enum StaticColumnWidths { + /// All columns share space equally (flex-1 / Length::Auto). + Auto, + /// Each column has a specific width. + Explicit(TableRow), } -impl TableColumnWidths { - pub fn new(cols: usize, _: &mut App) -> Self { +impl ColumnWidthConfig { + /// Auto-width columns, auto-size table. + pub fn auto() -> Self { + ColumnWidthConfig::Static { + widths: StaticColumnWidths::Auto, + table_width: None, + } + } + + /// Redistributable columns with no fixed table width. + pub fn redistributable(columns_state: Entity) -> Self { + ColumnWidthConfig::Redistributable { + columns_state, + table_width: None, + } + } + + /// Auto-width columns, fixed table width. + pub fn auto_with_table_width(width: impl Into) -> Self { + ColumnWidthConfig::Static { + widths: StaticColumnWidths::Auto, + table_width: Some(width.into()), + } + } + + /// Column widths for rendering. + pub fn widths_to_render(&self, cx: &App) -> Option> { + match self { + ColumnWidthConfig::Static { + widths: StaticColumnWidths::Auto, + .. + } => None, + ColumnWidthConfig::Static { + widths: StaticColumnWidths::Explicit(widths), + .. + } => Some(widths.map_cloned(Length::Definite)), + ColumnWidthConfig::Redistributable { + columns_state: entity, + .. + } => { + let state = entity.read(cx); + Some(state.preview_widths.map_cloned(Length::Definite)) + } + } + } + + /// Table-level width. + pub fn table_width(&self) -> Option { + match self { + ColumnWidthConfig::Static { table_width, .. } + | ColumnWidthConfig::Redistributable { table_width, .. } => { + table_width.map(Length::Definite) + } + } + } + + /// ListHorizontalSizingBehavior for uniform_list. + pub fn list_horizontal_sizing(&self) -> ListHorizontalSizingBehavior { + match self.table_width() { + Some(_) => ListHorizontalSizingBehavior::Unconstrained, + None => ListHorizontalSizingBehavior::FitList, + } + } + + /// Render resize handles overlay if applicable. + pub fn render_resize_handles(&self, window: &mut Window, cx: &mut App) -> Option { + match self { + ColumnWidthConfig::Redistributable { + columns_state: entity, + .. + } => { + let (column_widths, resize_behavior, initial_widths) = { + let state = entity.read(cx); + ( + state.preview_widths.map_cloned(Length::Definite), + state.resize_behavior.clone(), + state.initial_widths.clone(), + ) + }; + Some(render_resize_handles( + &column_widths, + &resize_behavior, + &initial_widths, + Some(entity.clone()), + window, + cx, + )) + } + _ => None, + } + } + + /// Returns info needed for header double-click-to-reset, if applicable. + pub fn header_resize_info(&self, cx: &App) -> Option { + match self { + ColumnWidthConfig::Redistributable { columns_state, .. } => { + let state = columns_state.read(cx); + Some(HeaderResizeInfo { + columns_state: columns_state.downgrade(), + resize_behavior: state.resize_behavior.clone(), + initial_widths: state.initial_widths.clone(), + }) + } + _ => None, + } + } +} + +#[derive(Clone)] +pub struct HeaderResizeInfo { + pub columns_state: WeakEntity, + pub resize_behavior: TableRow, + pub initial_widths: TableRow, +} + +pub struct RedistributableColumnsState { + pub(crate) initial_widths: TableRow, + pub(crate) committed_widths: TableRow, + pub(crate) preview_widths: TableRow, + pub(crate) resize_behavior: TableRow, + pub(crate) cached_table_width: Pixels, +} + +impl RedistributableColumnsState { + pub fn new( + cols: usize, + initial_widths: UncheckedTableRow>, + resize_behavior: UncheckedTableRow, + ) -> Self { + let widths: TableRow = initial_widths + .into_iter() + .map(Into::into) + .collect::>() + .into_table_row(cols); 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, + initial_widths: widths.clone(), + committed_widths: widths.clone(), + preview_widths: widths, + resize_behavior: resize_behavior.into_table_row(cols), + cached_table_width: Default::default(), } } pub fn cols(&self) -> usize { - self.widths.cols() + self.committed_widths.cols() + } + + pub fn initial_widths(&self) -> &TableRow { + &self.initial_widths + } + + pub fn resize_behavior(&self) -> &TableRow { + &self.resize_behavior } fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 { @@ -264,19 +418,19 @@ impl TableColumnWidths { } } - fn on_double_click( + pub(crate) fn on_double_click( &mut self, double_click_position: usize, initial_sizes: &TableRow, resize_behavior: &TableRow, window: &mut Window, ) { - let bounds_width = self.cached_bounds_width; + let bounds_width = self.cached_table_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 + .committed_widths .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size)); let updated_widths = Self::reset_to_initial_size( @@ -285,53 +439,16 @@ impl TableColumnWidths { initial_sizes, resize_behavior, ); - self.widths = updated_widths.map(DefiniteLength::Fraction); - self.visible_widths = self.widths.clone(); // previously was copy + self.committed_widths = updated_widths.map(DefiniteLength::Fraction); + self.preview_widths = self.committed_widths.clone(); } - fn reset_to_initial_size( + pub(crate) fn reset_to_initial_size( col_idx: usize, mut widths: TableRow, initial_sizes: TableRow, resize_behavior: &TableRow, ) -> TableRow { - // 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 - // - // Part 2: We need to decide which side column we should move and where - // - // If we want to grow our column we should check the left/right columns diff to see what side - // has a greater delta than their initial size. Likewise, if we shrink our column we should check - // the left/right column diffs to see what side has the smallest delta. - // - // Part 3: resize - // - // col_idx represents the column handle to the right of an active column - // - // If growing and right has the greater delta { - // shift col_idx to the right - // } else if growing and left has the greater delta { - // shift col_idx - 1 to the left - // } else if shrinking and the right has the greater delta { - // shift - // } { - // - // } - // } - // - // if we need to shrink, then if the right - // - - // DRAGGING - // we get diff which represents the change in the _drag handle_ position - // -diff => dragging left -> - // grow the column to the right of the handle as much as we can shrink columns to the left of the handle - // +diff => dragging right -> growing handles column - // grow the column to the left of the handle as much as we can shrink columns to the right of the handle - // - let diff = initial_sizes[col_idx] - widths[col_idx]; let left_diff = @@ -376,10 +493,9 @@ impl TableColumnWidths { widths } - fn on_drag_move( + pub(crate) fn on_drag_move( &mut self, drag_event: &DragMoveEvent, - resize_behavior: &TableRow, window: &mut Window, cx: &mut Context, ) { @@ -391,43 +507,42 @@ impl TableColumnWidths { let bounds_width = bounds.right() - bounds.left(); let col_idx = drag_event.drag(cx).0; - let column_handle_width = Self::get_fraction( - &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_COLUMN_WIDTH))), + let divider_width = Self::get_fraction( + &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_DIVIDER_WIDTH))), bounds_width, rem_size, ); let mut widths = self - .widths + .committed_widths .map_ref(|length| Self::get_fraction(length, bounds_width, rem_size)); for length in widths[0..=col_idx].iter() { - col_position += length + column_handle_width; + col_position += length + divider_width; } let mut total_length_ratio = col_position; for length in widths[col_idx + 1..].iter() { total_length_ratio += length; } - let cols = resize_behavior.cols(); - total_length_ratio += (cols - 1 - col_idx) as f32 * column_handle_width; + let cols = self.resize_behavior.cols(); + total_length_ratio += (cols - 1 - col_idx) as f32 * divider_width; let drag_fraction = (drag_position.x - bounds.left()) / bounds_width; let drag_fraction = drag_fraction * total_length_ratio; - let diff = drag_fraction - col_position - column_handle_width / 2.0; + let diff = drag_fraction - col_position - divider_width / 2.0; - Self::drag_column_handle(diff, col_idx, &mut widths, resize_behavior); + Self::drag_column_handle(diff, col_idx, &mut widths, &self.resize_behavior); - self.visible_widths = widths.map(DefiniteLength::Fraction); + self.preview_widths = widths.map(DefiniteLength::Fraction); } - fn drag_column_handle( + pub(crate) fn drag_column_handle( diff: f32, col_idx: usize, widths: &mut TableRow, resize_behavior: &TableRow, ) { - // if diff > 0.0 then go right if diff > 0.0 { Self::propagate_resize_diff(diff, col_idx, widths, resize_behavior, 1); } else { @@ -435,7 +550,7 @@ impl TableColumnWidths { } } - fn propagate_resize_diff( + pub(crate) fn propagate_resize_diff( diff: f32, col_idx: usize, widths: &mut TableRow, @@ -493,44 +608,16 @@ impl TableColumnWidths { } } -pub struct TableWidths { - initial: TableRow, - current: Option>, - resizable: TableRow, -} - -impl TableWidths { - pub fn new(widths: TableRow>) -> Self { - let widths = widths.map(Into::into); - - let expected_length = widths.cols(); - TableWidths { - initial: widths, - current: None, - resizable: vec![TableResizeBehavior::None; expected_length] - .into_table_row(expected_length), - } - } - - fn lengths(&self, cx: &App) -> TableRow { - self.current - .as_ref() - .map(|entity| entity.read(cx).visible_widths.map_cloned(Length::Definite)) - .unwrap_or_else(|| self.initial.map_cloned(Length::Definite)) - } -} - /// A table component #[derive(RegisterComponent, IntoElement)] pub struct Table { striped: bool, show_row_borders: bool, show_row_hover: bool, - width: Option, headers: Option>, rows: TableContents, interaction_state: Option>, - col_widths: Option, + column_width_config: ColumnWidthConfig, map_row: Option), &mut Window, &mut App) -> AnyElement>>, use_ui_font: bool, empty_table_callback: Option AnyElement>>, @@ -547,15 +634,14 @@ impl Table { striped: false, show_row_borders: true, show_row_hover: true, - width: None, headers: None, rows: TableContents::Vec(Vec::new()), interaction_state: None, map_row: None, use_ui_font: true, empty_table_callback: None, - col_widths: None, disable_base_cell_style: false, + column_width_config: ColumnWidthConfig::auto(), } } @@ -626,10 +712,18 @@ impl Table { self } - /// Sets the width of the table. - /// Will enable horizontal scrolling if [`Self::interactable`] is also called. - pub fn width(mut self, width: impl Into) -> Self { - self.width = Some(width.into()); + /// Sets a fixed table width with auto column widths. + /// + /// This is a shorthand for `.width_config(ColumnWidthConfig::auto_with_table_width(width))`. + /// For resizable columns or explicit column widths, use [`Table::width_config`] directly. + pub fn width(mut self, width: impl Into) -> Self { + self.column_width_config = ColumnWidthConfig::auto_with_table_width(width); + self + } + + /// Sets the column width configuration for the table. + pub fn width_config(mut self, config: ColumnWidthConfig) -> Self { + self.column_width_config = config; self } @@ -637,10 +731,8 @@ impl Table { /// /// Vertical scrolling will be enabled by default if the table is taller than its container. /// - /// Horizontal scrolling will only be enabled if [`Self::width`] is also called, otherwise - /// the list will always shrink the table columns to fit their contents I.e. If [`Self::uniform_list`] - /// is used without a width and with [`Self::interactable`], the [`ListHorizontalSizingBehavior`] will - /// be set to [`ListHorizontalSizingBehavior::FitList`]. + /// Horizontal scrolling will only be enabled if a table width is set via [`ColumnWidthConfig`], + /// otherwise the list will always shrink the table columns to fit their contents. pub fn interactable(mut self, interaction_state: &Entity) -> Self { self.interaction_state = Some(interaction_state.downgrade()); self @@ -666,36 +758,6 @@ impl Table { self } - pub fn column_widths(mut self, widths: UncheckedTableRow>) -> Self { - if self.col_widths.is_none() { - self.col_widths = Some(TableWidths::new(widths.into_table_row(self.cols))); - } - self - } - - pub fn resizable_columns( - mut self, - resizable: UncheckedTableRow, - column_widths: &Entity, - cx: &mut App, - ) -> Self { - if let Some(table_widths) = self.col_widths.as_mut() { - table_widths.resizable = resizable.into_table_row(self.cols); - let column_widths = table_widths - .current - .get_or_insert_with(|| column_widths.clone()); - - column_widths.update(cx, |widths, _| { - if !widths.initialized { - widths.initialized = true; - widths.widths = table_widths.initial.clone(); - widths.visible_widths = widths.widths.clone(); - } - }) - } - self - } - pub fn no_ui_font(mut self) -> Self { self.use_ui_font = false; self @@ -812,11 +874,7 @@ pub fn render_table_row( pub fn render_table_header( headers: TableRow, table_context: TableRenderContext, - columns_widths: Option<( - WeakEntity, - TableRow, - TableRow, - )>, + resize_info: Option, entity_id: Option, cx: &mut App, ) -> impl IntoElement { @@ -837,9 +895,7 @@ pub fn render_table_header( .flex() .flex_row() .items_center() - .justify_between() .w_full() - .p_2() .border_b_1() .border_color(cx.theme().colors().border) .children( @@ -850,34 +906,33 @@ pub fn render_table_header( .zip(column_widths.into_vec()) .map(|((header_idx, h), width)| { base_cell_style_text(width, table_context.use_ui_font, cx) + .px_1() + .py_0p5() .child(h) .id(ElementId::NamedInteger( shared_element_id.clone(), header_idx as u64, )) - .when_some( - columns_widths.as_ref().cloned(), - |this, (column_widths, resizables, initial_sizes)| { - if resizables[header_idx].is_resizable() { - this.on_click(move |event, window, cx| { - if event.click_count() > 1 { - column_widths - .update(cx, |column, _| { - column.on_double_click( - header_idx, - &initial_sizes, - &resizables, - window, - ); - }) - .ok(); - } - }) - } else { - this - } - }, - ) + .when_some(resize_info.as_ref().cloned(), |this, info| { + 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.on_double_click( + header_idx, + &info.initial_widths, + &info.resize_behavior, + window, + ); + }) + .ok(); + } + }) + } else { + this + } + }) }), ) } @@ -901,7 +956,7 @@ impl TableRenderContext { show_row_borders: table.show_row_borders, show_row_hover: table.show_row_hover, total_row_count: table.rows.len(), - column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)), + column_widths: table.column_width_config.widths_to_render(cx), map_row: table.map_row.clone(), use_ui_font: table.use_ui_font, disable_base_cell_style: table.disable_base_cell_style, @@ -913,48 +968,52 @@ impl RenderOnce for Table { fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement { let table_context = TableRenderContext::new(&self, cx); let interaction_state = self.interaction_state.and_then(|state| state.upgrade()); - let current_widths = self - .col_widths - .as_ref() - .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable.clone()))) - .map(|(curr, resize_behavior)| (curr.downgrade(), resize_behavior)); - let current_widths_with_initial_sizes = self - .col_widths + let header_resize_info = interaction_state .as_ref() - .and_then(|widths| { - Some(( - widths.current.as_ref()?, - widths.resizable.clone(), - widths.initial.clone(), - )) - }) - .map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial)); + .and_then(|_| self.column_width_config.header_resize_info(cx)); - let width = self.width; + let table_width = self.column_width_config.table_width(); + let horizontal_sizing = self.column_width_config.list_horizontal_sizing(); 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.downgrade()), + _ => None, + }); + + let resize_handles = interaction_state + .as_ref() + .and_then(|_| self.column_width_config.render_resize_handles(window, cx)); + let table = div() - .when_some(width, |this, width| this.w(width)) + .when_some(table_width, |this, width| this.w(width)) .h_full() .v_flex() .when_some(self.headers.take(), |this, headers| { this.child(render_table_header( headers, table_context.clone(), - current_widths_with_initial_sizes, + header_resize_info, interaction_state.as_ref().map(Entity::entity_id), cx, )) }) - .when_some(current_widths, { - |this, (widths, resize_behavior)| { + .when_some(redistributable_entity, { + |this, widths| { this.on_drag_move::({ let widths = widths.clone(); move |e, window, cx| { widths .update(cx, |widths, cx| { - widths.on_drag_move(e, &resize_behavior, window, cx); + widths.on_drag_move(e, window, cx); }) .ok(); } @@ -965,7 +1024,7 @@ impl RenderOnce for Table { widths .update(cx, |widths, _| { // This works because all children x axis bounds are the same - widths.cached_bounds_width = + widths.cached_table_width = bounds[0].right() - bounds[0].left(); }) .ok(); @@ -974,10 +1033,9 @@ impl RenderOnce for Table { .on_drop::(move |_, _, cx| { widths .update(cx, |widths, _| { - widths.widths = widths.visible_widths.clone(); + widths.committed_widths = widths.preview_widths.clone(); }) .ok(); - // Finish the resize operation }) } }) @@ -1029,11 +1087,7 @@ impl RenderOnce for Table { .size_full() .flex_grow() .with_sizing_behavior(ListSizingBehavior::Auto) - .with_horizontal_sizing_behavior(if width.is_some() { - ListHorizontalSizingBehavior::Unconstrained - } else { - ListHorizontalSizingBehavior::FitList - }) + .with_horizontal_sizing_behavior(horizontal_sizing) .when_some( interaction_state.as_ref(), |this, state| { @@ -1063,25 +1117,7 @@ impl RenderOnce for Table { .with_sizing_behavior(ListSizingBehavior::Auto), ), }) - .when_some( - self.col_widths.as_ref().zip(interaction_state.as_ref()), - |parent, (table_widths, state)| { - parent.child(state.update(cx, |state, cx| { - let resizable_columns = &table_widths.resizable; - let column_widths = table_widths.lengths(cx); - let columns = table_widths.current.clone(); - let initial_sizes = &table_widths.initial; - state.render_resize_handles( - &column_widths, - resizable_columns, - initial_sizes, - columns, - window, - cx, - ) - })) - }, - ); + .when_some(resize_handles, |parent, handles| parent.child(handles)); if let Some(state) = interaction_state.as_ref() { let scrollbars = state diff --git a/crates/ui/src/components/data_table/tests.rs b/crates/ui/src/components/data_table/tests.rs index f0982a8aa5abe5f5a9351ebaaaf4072ca17839e6..0936cd3088cc50bc08bf0a0a09d9a6fa7a2cdaf0 100644 --- a/crates/ui/src/components/data_table/tests.rs +++ b/crates/ui/src/components/data_table/tests.rs @@ -82,7 +82,7 @@ mod reset_column_size { let cols = initial_sizes.len(); let resize_behavior_vec = parse_resize_behavior(resize_behavior, total_1, cols); let resize_behavior = TableRow::from_vec(resize_behavior_vec, cols); - let result = TableColumnWidths::reset_to_initial_size( + let result = RedistributableColumnsState::reset_to_initial_size( column_index, TableRow::from_vec(widths, cols), TableRow::from_vec(initial_sizes, cols), @@ -259,7 +259,7 @@ mod drag_handle { let distance = distance as f32 / total_1; let mut widths_table_row = TableRow::from_vec(widths, cols); - TableColumnWidths::drag_column_handle( + RedistributableColumnsState::drag_column_handle( distance, column_index, &mut widths_table_row,