From 13e42dfac3ca08a775e8275f939b87ba8ab750a6 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 2 Oct 2024 22:02:21 +0800 Subject: [PATCH] table: Add to support fixed column on left to Table. (#295) - Update table details, fix table head height. - Add `table_row_border` theme color and use this for row border. ## Demo https://github.com/user-attachments/assets/91df4b9d-f079-4ddb-b14d-fbcf303dcc44 Fix #292 --- crates/story/src/table_story.rs | 10 +- crates/ui/src/table.rs | 387 ++++++++++++++++++-------------- crates/ui/src/theme.rs | 2 + 3 files changed, 230 insertions(+), 169 deletions(-) diff --git a/crates/story/src/table_story.rs b/crates/story/src/table_story.rs index a0d73e4b..d3f37162 100644 --- a/crates/story/src/table_story.rs +++ b/crates/story/src/table_story.rs @@ -13,7 +13,7 @@ use ui::{ input::{InputEvent, TextInput}, label::Label, prelude::FluentBuilder as _, - table::{ColSort, Table, TableDelegate, TableEvent}, + table::{ColFixed, ColSort, Table, TableDelegate, TableEvent}, v_flex, Selectable, Sizable, Size, }; @@ -290,6 +290,14 @@ impl TableDelegate for StockTableDelegate { } } + fn col_fixed(&self, col_ix: usize) -> Option { + if col_ix < 4 { + Some(ColFixed::Left) + } else { + None + } + } + fn can_resize_col(&self, col_ix: usize) -> bool { return self.col_resize && col_ix > 1; } diff --git a/crates/ui/src/table.rs b/crates/ui/src/table.rs index bf46bf17..549b601d 100644 --- a/crates/ui/src/table.rs +++ b/crates/ui/src/table.rs @@ -4,7 +4,7 @@ use crate::{ h_flex, scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState}, theme::ActiveTheme, - v_flex, Icon, IconName, Sizable, Size, StyledExt, + v_flex, Icon, IconName, Sizable, Size, }; use gpui::{ actions, canvas, div, prelude::FluentBuilder, px, uniform_list, AppContext, Bounds, Div, @@ -36,11 +36,17 @@ pub fn init(cx: &mut AppContext) { ]); } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ColFixed { + Left, +} + #[derive(Debug, Clone, Copy)] struct ColGroup { width: Option, bounds: Bounds, sort: Option, + fixed: Option, } #[derive(Clone)] @@ -156,6 +162,11 @@ pub trait TableDelegate: Sized + 'static { None } + /// Return the fixed side of the column at the given index. + fn col_fixed(&self, col_ix: usize) -> Option { + None + } + /// Perform sort on the column at the given index. fn perform_sort(&mut self, col_ix: usize, sort: ColSort, cx: &mut ViewContext>) {} @@ -299,6 +310,7 @@ where width: self.delegate.col_width(col_ix), bounds: Bounds::default(), sort: self.delegate.col_sort(col_ix), + fixed: self.delegate.col_fixed(col_ix), }) .collect(); cx.notify(); @@ -410,8 +422,8 @@ where .overflow_hidden() .whitespace_nowrap() .map(|this| match self.size { - Size::XSmall => this.text_sm().py_0().px_1(), - Size::Small => this.text_sm().py_0p5().px_1p5(), + Size::XSmall => this.text_sm().py_0p5().px_1(), + Size::Small => this.text_sm().py_1().px_1p5(), Size::Large => this.py_1p5().px_3(), _ => this.py_1().px_2(), }) @@ -473,8 +485,11 @@ where return div().into_any_element(); } + let group_id = SharedString::from(format!("resizable-handle:{}", ix)); + h_flex() .id(("resizable-handle", ix)) + .group(group_id.clone()) .occlude() .cursor_col_resize() .h_full() @@ -485,9 +500,9 @@ where .child( div() .h_full() - .h_5() .justify_center() - .bg(cx.theme().border) + .bg(cx.theme().table_row_border) + .group_hover(group_id, |this| this.bg(cx.theme().border).h_full()) .w(px(1.)), ) .on_drag_move(cx.listener(move |view, e: &DragMoveEvent, cx| { @@ -616,6 +631,18 @@ where cx.notify(); } + fn move_col(&mut self, col_ix: usize, to_ix: usize, cx: &mut ViewContext) { + if col_ix == to_ix { + return; + } + + self.delegate.move_col(col_ix, to_ix); + let col_group = self.col_groups.remove(col_ix); + self.col_groups.insert(to_ix, col_group); + + cx.notify(); + } + fn render_sort_icon( &self, col_ix: usize, @@ -750,16 +777,182 @@ where } } - fn move_col(&mut self, col_ix: usize, to_ix: usize, cx: &mut ViewContext) { - if col_ix == to_ix { - return; + fn render_table_head( + &mut self, + left_cols_count: usize, + cx: &mut ViewContext, + ) -> impl IntoElement { + let view = cx.view().clone(); + let horizontal_scroll_handle = self.horizontal_scroll_handle.clone(); + + h_flex() + .w_full() + .flex_shrink_0() + .border_b_1() + .border_color(cx.theme().border) + .child( + // Render left fixed columns + h_flex() + .id("table-head-fixed-left") + .h_full() + .bg(cx.theme().table_head) + .border_r_1() + .border_color(cx.theme().border) + .children( + self.col_groups + .iter() + .filter(|col| col.fixed == Some(ColFixed::Left)) + .enumerate() + .map(|(col_ix, _)| self.render_th(col_ix, cx)), + ), + ) + .child( + // Render other normal columns + uniform_list(view.clone(), "table-uniform-list-head", 1, { + let horizontal_scroll_handle = horizontal_scroll_handle.clone(); + let view = view.clone(); + move |table, _, cx| { + let view = view.clone(); + + // Columns + h_flex() + .id("table-head") + .h_full() + .w_full() + .overflow_scroll() + .relative() + .track_scroll(&horizontal_scroll_handle) + .bg(cx.theme().table_head) + .child( + h_flex() + .relative() + .children( + table + .col_groups + .iter() + .filter(|col| col.fixed == None) + .enumerate() + .map(|(col_ix, _)| { + table.render_th(left_cols_count + col_ix, cx) + }), + ) + .child(Self::last_empty_col(cx)) + .child( + canvas( + move |bounds, cx| { + view.update(cx, |r, _| { + r.head_content_bounds = bounds + }) + }, + |_, _, _| {}, + ) + .absolute() + .size_full(), + ), + ) + .map(|this| vec![this]) + } + }) + .h_full() + .flex_1(), + ) + } + + fn render_table_row( + &mut self, + row_ix: usize, + rows_count: usize, + left_cols_count: usize, + cols_count: usize, + cx: &mut ViewContext, + ) -> impl IntoElement { + let horizontal_scroll_handle = self.horizontal_scroll_handle.clone(); + let is_stripe_row = self.stripe && row_ix % 2 != 0; + let is_selected = self.selected_row == Some(row_ix); + + if row_ix < rows_count { + self.delegate + .render_tr(row_ix, cx) + .id(("table-row", row_ix)) + .w_full() + .when(row_ix > 0, |this| { + this.border_t_1().border_color(cx.theme().table_row_border) + }) + .when(is_stripe_row, |this| this.bg(cx.theme().table_even)) + .hover(|this| { + if is_selected { + this + } else { + this.bg(cx.theme().table_hover) + } + }) + .children(if left_cols_count > 0 { + // Left fixed columns + Some( + h_flex() + .border_r_1() + .border_color(cx.theme().table_row_border) + .children((0..left_cols_count).map(|col_ix| { + self.col_wrap(col_ix, cx).child( + self.render_cell(col_ix, cx) + .child(self.delegate.render_td(row_ix, col_ix, cx)), + ) + })), + ) + } else { + None + }) + .child( + h_flex() + .flex_1() + .overflow_hidden() + .children((left_cols_count..cols_count).map(|col_ix| { + self + // Make the row scroll sync with the + // horizontal_scroll_handle to support horizontal scrolling. + .col_wrap(col_ix, cx) + .left(horizontal_scroll_handle.offset().x) + .child( + self.render_cell(col_ix, cx) + .child(self.delegate.render_td(row_ix, col_ix, cx)), + ) + })) + .child(Self::last_empty_col(cx)), + ) + // Row selected style + .when_some(self.selected_row, |this, _| { + this.when( + is_selected && self.selection_state == SelectionState::Row, + |this| this.bg(cx.theme().table_active), + ) + }) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |this, _, cx| { + this.on_row_click(row_ix, cx); + }), + ) + } else { + // Render fake rows to fill the rest table space + self.delegate + .render_tr(row_ix, cx) + .id(("table-row-fake", row_ix)) + .w_full() + .h_full() + .border_t_1() + .border_color(cx.theme().table_row_border) + .when(is_stripe_row, |this| this.bg(cx.theme().table_even)) + .children((0..cols_count).map(|col_ix| { + h_flex() + .left(horizontal_scroll_handle.offset().x) + .child(self.render_cell(col_ix, cx)) + })) + .child(Self::last_empty_col(cx)) } + } - self.delegate.move_col(col_ix, to_ix); - let col_group = self.col_groups.remove(col_ix); - self.col_groups.insert(to_ix, col_group); - - cx.notify(); + fn last_empty_col(_: &mut WindowContext) -> Div { + h_flex().w(px(100.)).h_full().flex_shrink_0() } } @@ -816,13 +1009,11 @@ where } } - fn last_empty_col(_: &mut WindowContext) -> Div { - h_flex().w(px(100.)).h_full().flex_shrink_0() - } - - fn tr(_: &mut WindowContext) -> Div { - h_flex() - } + let left_cols_count = self + .col_groups + .iter() + .filter(|col| col.fixed == Some(ColFixed::Left)) + .count(); let inner_table = v_flex() .key_context("Table") @@ -835,59 +1026,7 @@ where .on_action(cx.listener(Self::action_select_prev_col)) .size_full() .overflow_hidden() - .child( - v_flex() - .flex_grow() - .h_10() - .w_full() - .flex_shrink_0() - .border_b_1() - .border_color(cx.theme().border) - .child( - uniform_list(view.clone(), "table-uniform-list-head", 1, { - let horizontal_scroll_handle = horizontal_scroll_handle.clone(); - let view = view.clone(); - move |table, _, cx| { - let view = view.clone(); - // Columns - tr(cx) - .id("table-head") - .w_full() - .h_10() - .overflow_scroll() - .track_scroll(&horizontal_scroll_handle) - .bg(cx.theme().table_head) - .child( - div() - .h_flex() - .relative() - .children( - table - .col_groups - .iter() - .enumerate() - .map(|(col_ix, _)| table.render_th(col_ix, cx)), - ) - .child(last_empty_col(cx)) - .child( - canvas( - move |bounds, cx| { - view.update(cx, |r, _| { - r.head_content_bounds = bounds - }) - }, - |_, _, _| {}, - ) - .absolute() - .size_full(), - ), - ) - .map(|this| vec![this]) - } - }) - .size_full(), - ), - ) + .child(self.render_table_head(left_cols_count, cx)) .map(|this| { if rows_count == 0 { this.child(div().size_full().child(self.delegate.render_empty(cx))) @@ -899,7 +1038,6 @@ where "table-uniform-list", rows_count + extra_rows_needed, { - let horizontal_scroll_handle = horizontal_scroll_handle.clone(); move |table, visible_range, cx| { table.load_more(visible_range.clone(), cx); @@ -914,100 +1052,13 @@ where visible_range .map(|row_ix| { // Render real rows for available data - if row_ix < rows_count { - table - .delegate - .render_tr(row_ix, cx) - .id(("table-row", row_ix)) - .w_full() - .when(row_ix > 0, |this| { - this.border_t_1() - .border_color(cx.theme().border) - }) - .when( - table.stripe && row_ix % 2 != 0, - |this| this.bg(cx.theme().table_even), - ) - .hover(|this| { - if table.selected_row == Some(row_ix) { - this - } else { - this.bg(cx.theme().table_hover) - } - }) - .children((0..cols_count).map(|col_ix| { - table - // Make the row scroll sync with the - // horizontal_scroll_handle to support horizontal scrolling. - .col_wrap(col_ix, cx) - .left( - horizontal_scroll_handle - .offset() - .x, - ) - .child( - table - .render_cell(col_ix, cx) - .child( - table - .delegate - .render_td( - row_ix, col_ix, - cx, - ), - ), - ) - })) - .child(last_empty_col(cx)) - // Row selected style - .when_some( - table.selected_row, - |this, selected_row| { - this.when( - row_ix == selected_row - && table.selection_state - == SelectionState::Row, - |this| { - this.bg(cx - .theme() - .table_active) - }, - ) - }, - ) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, _, cx| { - this.on_row_click(row_ix, cx); - }), - ) - } else { - // Render fake rows to fill the rest table space - table - .delegate - .render_tr(row_ix, cx) - .id(("table-row-fake", row_ix)) - .w_full() - .h_full() - .border_t_1() - .border_color(cx.theme().border) - .when( - table.stripe && row_ix % 2 != 0, - |this| this.bg(cx.theme().table_even), - ) - .children((0..cols_count).map(|col_ix| { - h_flex() - .left( - horizontal_scroll_handle - .offset() - .x, - ) - .child( - table.render_cell(col_ix, cx), - ) - })) - .child(last_empty_col(cx)) - } + table.render_table_row( + row_ix, + rows_count, + left_cols_count, + cols_count, + cx, + ) }) .collect::>() } diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index 7b42e63f..5653377c 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -315,6 +315,7 @@ pub struct Theme { pub table: Hsla, pub table_even: Hsla, pub table_head: Hsla, + pub table_row_border: Hsla, pub table_active: Hsla, pub table_hover: Hsla, pub link: Hsla, @@ -396,6 +397,7 @@ impl From for Theme { table_even: colors.list_even, table_active: colors.list_active, table_hover: colors.list_active.opacity(0.8), + table_row_border: colors.border.opacity(0.5), link: colors.link, link_hover: colors.link.lighten(0.2), link_active: colors.link.darken(0.2),