@@ -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);
@@ -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",
@@ -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