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}, input::{InputEvent, TextInput},
label::Label, label::Label,
prelude::FluentBuilder as _, prelude::FluentBuilder as _,
table::{ColSort, Table, TableDelegate, TableEvent}, table::{ColFixed, ColSort, Table, TableDelegate, TableEvent},
v_flex, Selectable, Sizable, Size, 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 { fn can_resize_col(&self, col_ix: usize) -> bool {
return self.col_resize && col_ix > 1; return self.col_resize && col_ix > 1;
} }

View file

@ -4,7 +4,7 @@ use crate::{
h_flex, h_flex,
scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState}, scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState},
theme::ActiveTheme, theme::ActiveTheme,
v_flex, Icon, IconName, Sizable, Size, StyledExt, v_flex, Icon, IconName, Sizable, Size,
}; };
use gpui::{ use gpui::{
actions, canvas, div, prelude::FluentBuilder, px, uniform_list, AppContext, Bounds, Div, 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)] #[derive(Debug, Clone, Copy)]
struct ColGroup { struct ColGroup {
width: Option<Pixels>, width: Option<Pixels>,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
sort: Option<ColSort>, sort: Option<ColSort>,
fixed: Option<ColFixed>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -156,6 +162,11 @@ pub trait TableDelegate: Sized + 'static {
None 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. /// Perform sort on the column at the given index.
fn perform_sort(&mut self, col_ix: usize, sort: ColSort, cx: &mut ViewContext<Table<Self>>) {} 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), width: self.delegate.col_width(col_ix),
bounds: Bounds::default(), bounds: Bounds::default(),
sort: self.delegate.col_sort(col_ix), sort: self.delegate.col_sort(col_ix),
fixed: self.delegate.col_fixed(col_ix),
}) })
.collect(); .collect();
cx.notify(); cx.notify();
@ -410,8 +422,8 @@ where
.overflow_hidden() .overflow_hidden()
.whitespace_nowrap() .whitespace_nowrap()
.map(|this| match self.size { .map(|this| match self.size {
Size::XSmall => this.text_sm().py_0().px_1(), Size::XSmall => this.text_sm().py_0p5().px_1(),
Size::Small => this.text_sm().py_0p5().px_1p5(), Size::Small => this.text_sm().py_1().px_1p5(),
Size::Large => this.py_1p5().px_3(), Size::Large => this.py_1p5().px_3(),
_ => this.py_1().px_2(), _ => this.py_1().px_2(),
}) })
@ -473,8 +485,11 @@ where
return div().into_any_element(); return div().into_any_element();
} }
let group_id = SharedString::from(format!("resizable-handle:{}", ix));
h_flex() h_flex()
.id(("resizable-handle", ix)) .id(("resizable-handle", ix))
.group(group_id.clone())
.occlude() .occlude()
.cursor_col_resize() .cursor_col_resize()
.h_full() .h_full()
@ -485,9 +500,9 @@ where
.child( .child(
div() div()
.h_full() .h_full()
.h_5()
.justify_center() .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.)), .w(px(1.)),
) )
.on_drag_move(cx.listener(move |view, e: &DragMoveEvent<ResizeCol>, cx| { .on_drag_move(cx.listener(move |view, e: &DragMoveEvent<ResizeCol>, cx| {
@ -616,6 +631,18 @@ where
cx.notify(); 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( fn render_sort_icon(
&self, &self,
col_ix: usize, col_ix: usize,
@ -750,16 +777,182 @@ where
} }
} }
fn move_col(&mut self, col_ix: usize, to_ix: usize, cx: &mut ViewContext<Self>) { fn render_table_head(
if col_ix == to_ix { &mut self,
return; 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); fn last_empty_col(_: &mut WindowContext) -> Div {
let col_group = self.col_groups.remove(col_ix); h_flex().w(px(100.)).h_full().flex_shrink_0()
self.col_groups.insert(to_ix, col_group);
cx.notify();
} }
} }
@ -816,13 +1009,11 @@ where
} }
} }
fn last_empty_col(_: &mut WindowContext) -> Div { let left_cols_count = self
h_flex().w(px(100.)).h_full().flex_shrink_0() .col_groups
} .iter()
.filter(|col| col.fixed == Some(ColFixed::Left))
fn tr(_: &mut WindowContext) -> Div { .count();
h_flex()
}
let inner_table = v_flex() let inner_table = v_flex()
.key_context("Table") .key_context("Table")
@ -835,59 +1026,7 @@ where
.on_action(cx.listener(Self::action_select_prev_col)) .on_action(cx.listener(Self::action_select_prev_col))
.size_full() .size_full()
.overflow_hidden() .overflow_hidden()
.child( .child(self.render_table_head(left_cols_count, cx))
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(),
),
)
.map(|this| { .map(|this| {
if rows_count == 0 { if rows_count == 0 {
this.child(div().size_full().child(self.delegate.render_empty(cx))) this.child(div().size_full().child(self.delegate.render_empty(cx)))
@ -899,7 +1038,6 @@ where
"table-uniform-list", "table-uniform-list",
rows_count + extra_rows_needed, rows_count + extra_rows_needed,
{ {
let horizontal_scroll_handle = horizontal_scroll_handle.clone();
move |table, visible_range, cx| { move |table, visible_range, cx| {
table.load_more(visible_range.clone(), cx); table.load_more(visible_range.clone(), cx);
@ -914,100 +1052,13 @@ where
visible_range visible_range
.map(|row_ix| { .map(|row_ix| {
// Render real rows for available data // Render real rows for available data
if row_ix < rows_count { table.render_table_row(
table row_ix,
.delegate rows_count,
.render_tr(row_ix, cx) left_cols_count,
.id(("table-row", row_ix)) cols_count,
.w_full() cx,
.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))
}
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }

View file

@ -315,6 +315,7 @@ pub struct Theme {
pub table: Hsla, pub table: Hsla,
pub table_even: Hsla, pub table_even: Hsla,
pub table_head: Hsla, pub table_head: Hsla,
pub table_row_border: Hsla,
pub table_active: Hsla, pub table_active: Hsla,
pub table_hover: Hsla, pub table_hover: Hsla,
pub link: Hsla, pub link: Hsla,
@ -396,6 +397,7 @@ impl From<Colors> for Theme {
table_even: colors.list_even, table_even: colors.list_even,
table_active: colors.list_active, table_active: colors.list_active,
table_hover: colors.list_active.opacity(0.8), table_hover: colors.list_active.opacity(0.8),
table_row_border: colors.border.opacity(0.5),
link: colors.link, link: colors.link,
link_hover: colors.link.lighten(0.2), link_hover: colors.link.lighten(0.2),
link_active: colors.link.darken(0.2), link_active: colors.link.darken(0.2),