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
This commit is contained in:
Jason Lee 2024-10-02 22:02:21 +08:00 committed by GitHub
parent 33e5fdcb78
commit 13e42dfac3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 230 additions and 169 deletions

View file

@ -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<ui::table::ColFixed> {
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;
}

View file

@ -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<Pixels>,
bounds: Bounds<Pixels>,
sort: Option<ColSort>,
fixed: Option<ColFixed>,
}
#[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<ColFixed> {
None
}
/// Perform sort on the column at the given index.
fn perform_sort(&mut self, col_ix: usize, sort: ColSort, cx: &mut ViewContext<Table<Self>>) {}
@ -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<ResizeCol>, cx| {
@ -616,6 +631,18 @@ where
cx.notify();
}
fn move_col(&mut self, col_ix: usize, to_ix: usize, cx: &mut ViewContext<Self>) {
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<Self>) {
if col_ix == to_ix {
return;
fn render_table_head(
&mut self,
left_cols_count: usize,
cx: &mut ViewContext<Self>,
) -> 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<Self>,
) -> 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::<Vec<_>>()
}

View file

@ -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<Colors> 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),