Rework column/table width API in data table (#51060)

Oleksandr Kholiavko and Anthony Eid created

data_table: Replace column width builder API with `ColumnWidthConfig`
enum

This PR consolidates the data table width configuration API from three
separate builder methods (`.column_widths()`, `.resizable_columns()`,
`.width()`) into a single `.width_config(ColumnWidthConfig)` call. This
makes invalid state combinations unrepresentable and clarifies the two
distinct width management modes.

**What changed:**

- Introduces `ColumnWidthConfig` enum with two variants:
  - `Static`: Fixed column widths, no resize handles
- `Redistributable`: Drag-to-resize columns that redistribute space
within a fixed table width
- Introduces `TableResizeBehavior` enum (`None`, `Resizable`,
`MinSize(f32)`) for per-column resize policy
- Renames `TableColumnWidths` → `RedistributableColumnsState` to better
reflect its purpose
- Extracts all width management logic into a new `width_management.rs`
module
- Updates all callers: `csv_preview`, `git_graph`, `keymap_editor`,
`edit_prediction_context_view`

```rust
pub enum ColumnWidthConfig {
    /// Static column widths (no resize handles).
    Static {
        widths: StaticColumnWidths,
        /// Controls widths of the whole table.
        table_width: Option<DefiniteLength>,
    },
    /// Redistributable columns — dragging redistributes the fixed available space
    /// among columns without changing the overall table width.
    Redistributable {
        entity: Entity<RedistributableColumnsState>,
        table_width: Option<DefiniteLength>,
    },
}
```

**Why:**

The old API allowed callers to combine methods incorrectly. The new
enum-based design enforces correct usage at compile time and provides a
clearer path for adding independently resizable columns in PR #3.

**Context:**

This is part 2 of a 3-PR series improving data table column width
handling:
1. [#51059](https://github.com/zed-industries/zed/pull/51059) - Extract
modules into separate files (mechanical change)
2. **This PR**: Introduce width config enum for redistributable column
widths (API rework)
3. Implement independently resizable column widths (new feature)

The series builds on previously merged infrastructure:
- [#46341](https://github.com/zed-industries/zed/pull/46341) - Data
table dynamic column support
- [#46190](https://github.com/zed-industries/zed/pull/46190) - Variable
row height mode for data tables

Primary beneficiary: CSV preview feature
([#48207](https://github.com/zed-industries/zed/pull/48207))


### Anthony's note

This PR also fixes the table dividers being a couple pixels off, and the
csv preview from having double line rendering for a single column in
some cases.

Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [ ] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <anthony@zed.dev>

Change summary

crates/csv_preview/src/csv_preview.rs              |  46 
crates/csv_preview/src/parser.rs                   |   5 
crates/csv_preview/src/renderer/render_table.rs    |  43 
crates/csv_preview/src/renderer/row_identifiers.rs |   1 
crates/csv_preview/src/renderer/table_cell.rs      |   1 
crates/git_graph/src/git_graph.rs                  |  58 
crates/keymap_editor/src/keymap_editor.rs          |  55 
crates/ui/src/components/data_table.rs             | 672 ++++++++-------
crates/ui/src/components/data_table/tests.rs       |   4 
9 files changed, 462 insertions(+), 423 deletions(-)

Detailed changes

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<Self>) {
+        // 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::<TabularDataPreviewFeatureFlag>(), |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<TableColumnWidths>,
+    pub widths: Entity<RedistributableColumnsState>,
 }
 
 impl ColumnWidths {
     pub(crate) fn new(cx: &mut Context<CsvPreviewView>, 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<CsvPreviewView>, cols: usize) {
-        self.widths
-            .update(cx, |entity, cx| *entity = TableColumnWidths::new(cols, cx));
-    }
 }

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();

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<TableColumnWidths>,
+        current_widths: &Entity<RedistributableColumnsState>,
         cx: &mut Context<Self>,
     ) -> 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<DefiniteLength>,
-        resize_behaviors: UncheckedTableRow<TableResizeBehavior>,
-        current_widths: &Entity<TableColumnWidths>,
+        current_widths: &Entity<RedistributableColumnsState>,
         cx: &mut Context<Self>,
     ) -> 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| {

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(),

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<ContextMenu>, Point<Pixels>, Subscription)>,
     row_height: Pixels,
     table_interaction_state: Entity<TableInteractionState>,
-    table_column_widths: Entity<TableColumnWidths>,
+    table_column_widths: Entity<RedistributableColumnsState>,
     horizontal_scroll_offset: Pixels,
     graph_viewport_width: Pixels,
     selected_entry_idx: Option<usize>,
@@ -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::<settings::SettingsStore>(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);

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<ContextMenu>, Point<Pixels>, Subscription)>,
     previous_edit: Option<PreviousEdit>,
     humanized_action_names: HumanizedActionNameCache,
-    current_widths: Entity<TableColumnWidths>,
+    current_widths: Entity<RedistributableColumnsState>,
     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",

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<T>` internally
 pub type UncheckedTableRow<T> = Vec<T>;
 
 #[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<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,
-                                        );
-                                    })
-                                }
+/// 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<Length>,
+    resizable_columns: &TableRow<TableResizeBehavior>,
+    initial_sizes: &TableRow<DefiniteLength>,
+    columns: Option<Entity<RedistributableColumnsState>>,
+    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<DefiniteLength>,
-    visible_widths: TableRow<DefiniteLength>,
-    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<DefiniteLength>,
+    },
+    /// Redistributable columns — dragging redistributes the fixed available space
+    /// among columns without changing the overall table width.
+    Redistributable {
+        columns_state: Entity<RedistributableColumnsState>,
+        table_width: Option<DefiniteLength>,
+    },
+}
+
+pub enum StaticColumnWidths {
+    /// All columns share space equally (flex-1 / Length::Auto).
+    Auto,
+    /// Each column has a specific width.
+    Explicit(TableRow<DefiniteLength>),
 }
 
-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<RedistributableColumnsState>) -> Self {
+        ColumnWidthConfig::Redistributable {
+            columns_state,
+            table_width: None,
+        }
+    }
+
+    /// Auto-width columns, fixed table width.
+    pub fn auto_with_table_width(width: impl Into<DefiniteLength>) -> Self {
+        ColumnWidthConfig::Static {
+            widths: StaticColumnWidths::Auto,
+            table_width: Some(width.into()),
+        }
+    }
+
+    /// Column widths for rendering.
+    pub fn widths_to_render(&self, cx: &App) -> Option<TableRow<Length>> {
+        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<Length> {
+        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<AnyElement> {
+        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<HeaderResizeInfo> {
+        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<RedistributableColumnsState>,
+    pub resize_behavior: TableRow<TableResizeBehavior>,
+    pub initial_widths: TableRow<DefiniteLength>,
+}
+
+pub struct RedistributableColumnsState {
+    pub(crate) initial_widths: TableRow<DefiniteLength>,
+    pub(crate) committed_widths: TableRow<DefiniteLength>,
+    pub(crate) preview_widths: TableRow<DefiniteLength>,
+    pub(crate) resize_behavior: TableRow<TableResizeBehavior>,
+    pub(crate) cached_table_width: Pixels,
+}
+
+impl RedistributableColumnsState {
+    pub fn new(
+        cols: usize,
+        initial_widths: UncheckedTableRow<impl Into<DefiniteLength>>,
+        resize_behavior: UncheckedTableRow<TableResizeBehavior>,
+    ) -> Self {
+        let widths: TableRow<DefiniteLength> = initial_widths
+            .into_iter()
+            .map(Into::into)
+            .collect::<Vec<_>>()
+            .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<DefiniteLength> {
+        &self.initial_widths
+    }
+
+    pub fn resize_behavior(&self) -> &TableRow<TableResizeBehavior> {
+        &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<DefiniteLength>,
         resize_behavior: &TableRow<TableResizeBehavior>,
         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<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
-        //
-        // 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<DraggedColumn>,
-        resize_behavior: &TableRow<TableResizeBehavior>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -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<f32>,
         resize_behavior: &TableRow<TableResizeBehavior>,
     ) {
-        // 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<f32>,
@@ -493,44 +608,16 @@ impl TableColumnWidths {
     }
 }
 
-pub struct TableWidths {
-    initial: TableRow<DefiniteLength>,
-    current: Option<Entity<TableColumnWidths>>,
-    resizable: TableRow<TableResizeBehavior>,
-}
-
-impl TableWidths {
-    pub fn new(widths: TableRow<impl Into<DefiniteLength>>) -> 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<Length> {
-        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<Length>,
     headers: Option<TableRow<AnyElement>>,
     rows: TableContents,
     interaction_state: Option<WeakEntity<TableInteractionState>>,
-    col_widths: Option<TableWidths>,
+    column_width_config: ColumnWidthConfig,
     map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
     use_ui_font: bool,
     empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> 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<Length>) -> 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<DefiniteLength>) -> 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<TableInteractionState>) -> Self {
         self.interaction_state = Some(interaction_state.downgrade());
         self
@@ -666,36 +758,6 @@ impl Table {
         self
     }
 
-    pub fn column_widths(mut self, widths: UncheckedTableRow<impl Into<DefiniteLength>>) -> 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<TableResizeBehavior>,
-        column_widths: &Entity<TableColumnWidths>,
-        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<impl IntoElement>,
     table_context: TableRenderContext,
-    columns_widths: Option<(
-        WeakEntity<TableColumnWidths>,
-        TableRow<TableResizeBehavior>,
-        TableRow<DefiniteLength>,
-    )>,
+    resize_info: Option<HeaderResizeInfo>,
     entity_id: Option<EntityId>,
     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::<DraggedColumn>({
                         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::<DraggedColumn>(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

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,