mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Rework column/table width API in data table (#51060)
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>
This commit is contained in:
parent
0b275eaa44
commit
a3964a565c
9 changed files with 456 additions and 417 deletions
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ impl CsvPreviewView {
|
|||
RowIdentifiers::SrcLines => RowIdentifiers::RowNum,
|
||||
RowIdentifiers::RowNum => RowIdentifiers::SrcLines,
|
||||
};
|
||||
this.sync_column_widths(cx);
|
||||
cx.notify();
|
||||
});
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
/// 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 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 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();
|
||||
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);
|
||||
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_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,
|
||||
);
|
||||
})
|
||||
}
|
||||
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>,
|
||||
},
|
||||
}
|
||||
|
||||
impl TableColumnWidths {
|
||||
pub fn new(cols: usize, _: &mut App) -> Self {
|
||||
pub enum StaticColumnWidths {
|
||||
/// All columns share space equally (flex-1 / Length::Auto).
|
||||
Auto,
|
||||
/// Each column has a specific width.
|
||||
Explicit(TableRow<DefiniteLength>),
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue