table: Add drag to move col. (#76)

https://github.com/user-attachments/assets/9964f3da-0376-4b53-b64e-c9652c324aa3
This commit is contained in:
Jason Lee 2024-07-28 22:20:23 +08:00 committed by GitHub
parent 79958fc95c
commit 7bfdfcb2ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 245 additions and 84 deletions

View file

@ -59,8 +59,8 @@ A UI components for building desktop application by using [GPUI](https://gpui.rs
- [x] Left, Right / Up, Down to selection column or row.
- [x] Horizontal scroll
- [x] Vertical scroll
- [ ] Column resizing
- [ ] Column ordering
- [x] Column resizing
- [x] Column ordering
- [ ] Sort event emit
- [ ] Drawer
- [ ] Modal

View file

@ -25,26 +25,6 @@ struct Customer {
verified: bool,
confirmed: bool,
}
impl Customer {
fn col_names() -> Vec<SharedString> {
vec![
"ID".into(),
"Login".into(),
"First Name".into(),
"Last Name".into(),
"Company".into(),
"City".into(),
"Country".into(),
"Email".into(),
"Phone".into(),
"Gender".into(),
"Age".into(),
"Verified".into(),
"Confirmed".into(),
"Twitter".into(),
]
}
}
fn randome_customers(size: usize) -> Vec<Customer> {
(0..size)
@ -67,80 +47,106 @@ fn randome_customers(size: usize) -> Vec<Customer> {
}
struct CustomerTableDelegate {
customers: Vec<Customer>,
col_names: Vec<(SharedString, SharedString)>,
loop_selection: bool,
col_resize: bool,
col_order: bool,
}
impl CustomerTableDelegate {
fn new(size: usize) -> Self {
Self {
customers: randome_customers(size),
col_names: vec![
("id".into(), "ID".into()),
("login".into(), "Login".into()),
("first_name".into(), "First Name".into()),
("last_name".into(), "Last Name".into()),
("company".into(), "Company".into()),
("city".into(), "City".into()),
("country".into(), "Country".into()),
("email".into(), "Email".into()),
("phone".into(), "Phone".into()),
("gender".into(), "Gender".into()),
("age".into(), "Age".into()),
("verified".into(), "Verified".into()),
("confirmed".into(), "Confirmed".into()),
("twitter".into(), "Twitter".into()),
],
loop_selection: true,
col_resize: true,
col_order: true,
}
}
}
impl TableDelegate for CustomerTableDelegate {
fn cols_count(&self) -> usize {
Customer::col_names().len()
self.col_names.len()
}
fn rows_count(&self) -> usize {
self.customers.len()
}
fn column_name(&self, col_ix: usize) -> SharedString {
if let Some(name) = Customer::col_names().get(col_ix) {
name.clone()
fn col_name(&self, col_ix: usize) -> SharedString {
if let Some(col) = self.col_names.get(col_ix) {
col.1.clone()
} else {
"--".into()
}
}
fn col_width(&self, col_ix: usize) -> Option<Pixels> {
match col_ix {
0 => Some(50.0),
1 => Some(220.0),
2 => Some(150.0),
3 => Some(150.0),
4 => Some(300.0),
5 => Some(200.0),
6 => Some(200.0),
7 => Some(350.0),
8 => Some(240.0),
9 => Some(80.0),
10 => Some(90.0),
11 => Some(90.0),
12 => Some(90.0),
13 => Some(90.0),
_ => None,
if let Some(col) = self.col_names.get(col_ix) {
Some(
match col.0.as_ref() {
"id" => 50.0,
"login" => 220.0,
"first_name" => 150.0,
"last_name" => 150.0,
"company" => 300.0,
"city" => 200.0,
"country" => 200.0,
"email" => 350.0,
"phone" => 240.0,
"gender" => 80.0,
"age" => 90.0,
"verified" => 90.0,
"confirmed" => 90.0,
"twitter" => 90.0,
_ => 200.0,
}
.into(),
)
} else {
None
}
.map(Pixels::from)
}
fn can_resize_col(&self, col_ix: usize) -> bool {
return col_ix > 1;
}
fn on_col_widths_changed(&mut self, col_widths: Vec<Option<Pixels>>) {
println!("Col widths changed: {:?}", col_widths);
return self.col_resize && col_ix > 1;
}
fn render_td(&self, row_ix: usize, col_ix: usize) -> impl gpui::IntoElement {
let customer = self.customers.get(row_ix).unwrap();
let text = match col_ix {
0 => customer.id.to_string(),
1 => customer.login.clone(),
2 => customer.first_name.clone(),
3 => customer.last_name.clone(),
4 => customer.company.clone(),
5 => customer.city.clone(),
6 => customer.country.clone(),
7 => customer.email.clone(),
8 => customer.phone.clone(),
9 => customer.gender.to_string(),
10 => customer.age.to_string(),
11 => customer.verified.to_string(),
12 => customer.confirmed.to_string(),
let col = self.col_names.get(col_ix).unwrap();
let text = match col.0.as_ref() {
"id" => customer.id.to_string(),
"login" => customer.login.clone(),
"first_name" => customer.first_name.clone(),
"last_name" => customer.last_name.clone(),
"company" => customer.company.clone(),
"city" => customer.city.clone(),
"country" => customer.country.clone(),
"email" => customer.email.clone(),
"phone" => customer.phone.clone(),
"gender" => customer.gender.to_string(),
"age" => customer.age.to_string(),
"verified" => customer.verified.to_string(),
"confirmed" => customer.confirmed.to_string(),
"twitter" => "twitter".to_string(),
_ => "--".to_string(),
};
@ -150,6 +156,15 @@ impl TableDelegate for CustomerTableDelegate {
fn can_loop_select(&self) -> bool {
self.loop_selection
}
fn can_move_col(&self, _: usize) -> bool {
self.col_order
}
fn move_col(&mut self, col_ix: usize, to_ix: usize) {
let col = self.col_names.remove(col_ix);
self.col_names.insert(to_ix, col);
}
}
pub struct TableStory {
@ -178,6 +193,22 @@ impl TableStory {
});
}
fn toggle_col_resize(&mut self, s: &Selection, cx: &mut ViewContext<Self>) {
let table = self.table.clone();
table.update(cx, |table, cx| {
table.delegate_mut().col_resize = s.is_selected();
cx.notify();
});
}
fn toggle_col_order(&mut self, s: &Selection, cx: &mut ViewContext<Self>) {
let table = self.table.clone();
table.update(cx, |table, cx| {
table.delegate_mut().col_order = s.is_selected();
cx.notify();
});
}
fn on_table_event(
&mut self,
_: View<Table<CustomerTableDelegate>>,
@ -202,12 +233,27 @@ impl Render for TableStory {
.size_full()
.gap_2()
.child(
h_flex().items_center().child(
Checkbox::new("loop-selection")
.label("Loop Selection")
.selected(delegate.loop_selection)
.on_click(cx.listener(Self::toggle_loop_selection)),
),
h_flex()
.items_center()
.gap_2()
.child(
Checkbox::new("loop-selection")
.label("Loop Selection")
.selected(delegate.loop_selection)
.on_click(cx.listener(Self::toggle_loop_selection)),
)
.child(
Checkbox::new("col-resize")
.label("Column Resize")
.selected(delegate.col_resize)
.on_click(cx.listener(Self::toggle_col_resize)),
)
.child(
Checkbox::new("col-order")
.label("Column Order")
.selected(delegate.col_order)
.on_click(cx.listener(Self::toggle_col_order)),
),
)
.child(self.table.clone())
}

View file

@ -9,7 +9,7 @@ use crate::{
use gpui::{
actions, canvas, div, prelude::FluentBuilder as _, px, uniform_list, AppContext, Bounds, Div,
DragMoveEvent, EntityId, EventEmitter, FocusHandle, FocusableView, InteractiveElement as _,
IntoElement, KeyBinding, MouseButton, ParentElement as _, Pixels, Render, ScrollHandle,
IntoElement, KeyBinding, MouseButton, ParentElement as _, Pixels, Point, Render, ScrollHandle,
SharedString, StatefulInteractiveElement as _, Styled, UniformListScrollHandle, ViewContext,
VisualContext as _, WindowContext,
};
@ -36,13 +36,38 @@ pub fn init(cx: &mut AppContext) {
]);
}
#[derive(Debug, Clone, Copy)]
struct ColGroup {
width: Option<Pixels>,
bounds: Bounds<Pixels>,
}
#[derive(Clone)]
pub(crate) struct DragCol {
pub(crate) entity_id: EntityId,
pub(crate) name: SharedString,
pub(crate) width: Option<Pixels>,
pub(crate) col_ix: usize,
}
impl Render for DragCol {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.px_4()
.py_1()
.bg(cx.theme().table_head)
.border_1()
.border_color(cx.theme().border)
.shadow_md()
.when_some(self.width.clone(), |this, width| this.w(width))
.min_w(px(100.))
.max_w(px(450.))
.child(self.name.clone())
}
}
#[derive(Clone, Render)]
pub struct DragCol(pub (EntityId, usize));
pub struct ResizeCol(pub (EntityId, usize));
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum SelectionState {
@ -60,6 +85,8 @@ pub enum TableEvent {
pub struct Table<D: TableDelegate> {
focus_handle: FocusHandle,
delegate: D,
/// The bounds of the table.
bounds: Bounds<Pixels>,
horizontal_scroll_handle: ScrollHandle,
vertical_scroll_handle: UniformListScrollHandle,
col_groups: Vec<ColGroup>,
@ -82,7 +109,7 @@ pub trait TableDelegate: Sized + 'static {
fn rows_count(&self) -> usize;
/// Returns the name of the column at the given index.
fn column_name(&self, col_ix: usize) -> SharedString;
fn col_name(&self, col_ix: usize) -> SharedString;
/// Returns whether the column at the given index can be resized. Default: true
fn can_resize_col(&self, col_ix: usize) -> bool {
@ -95,12 +122,9 @@ pub trait TableDelegate: Sized + 'static {
/// This is only called when the table initializes.
fn col_width(&self, col_ix: usize) -> Option<Pixels>;
/// When the column has resized, this method is called.
fn on_col_widths_changed(&mut self, col_widths: Vec<Option<Pixels>>) {}
/// Render the header cell at the given column index, default to the column name.
fn render_th(&self, col_ix: usize) -> impl IntoElement {
div().size_full().child(self.column_name(col_ix))
div().size_full().child(self.col_name(col_ix))
}
/// Render cell at the given row and column.
@ -114,6 +138,14 @@ pub trait TableDelegate: Sized + 'static {
fn can_loop_select(&self) -> bool {
true
}
/// Return true to enable column order change.
fn can_move_col(&self, col_ix: usize) -> bool {
false
}
/// Move the column at the given `col_ix` to insert before the column at the given `to_ix`.
fn move_col(&mut self, col_ix: usize, to_ix: usize) {}
}
impl<D> Table<D>
@ -132,6 +164,7 @@ where
selected_row: None,
selected_col: None,
resizing_col: None,
bounds: Bounds::default(),
};
this.prepare_col_groups(cx);
@ -316,9 +349,9 @@ where
)
.hover(|this| this.bg(cx.theme().drag_border))
.when(is_resizing, |this| this.bg(cx.theme().drag_border))
.on_drag_move(cx.listener(
move |view, e: &DragMoveEvent<DragCol>, cx| match e.drag(cx) {
DragCol((entity_id, ix)) => {
.on_drag_move(cx.listener(move |view, e: &DragMoveEvent<ResizeCol>, cx| {
match e.drag(cx) {
ResizeCol((entity_id, ix)) => {
if cx.entity_id() != *entity_id {
return;
}
@ -331,16 +364,24 @@ where
let ix = *ix;
view.resizing_col = Some(ix);
let col_group = view.col_groups.get(ix).expect("BUG: invalid col index");
let col_group = view
.col_groups
.get(ix)
.expect("BUG: invalid col index")
.clone();
view.resize_cols(
ix,
e.event.position.x - HANDLE_SIZE - col_group.bounds.left(),
cx,
);
// scroll the table if the drag is near the edge
view.scroll_table_by_col_resizing(e.event.position, col_group, cx);
}
},
))
.on_drag(DragCol((cx.entity_id(), ix)), |drag, cx| {
};
}))
.on_drag(ResizeCol((cx.entity_id(), ix)), |drag, cx| {
cx.stop_propagation();
cx.new_view(|_| drag.clone())
})
@ -361,10 +402,32 @@ where
.into_any_element()
}
/// Scroll table when mouse position is near the edge of the table bounds.
fn scroll_table_by_col_resizing(
&mut self,
pos: Point<Pixels>,
col_group: ColGroup,
_: &mut ViewContext<Self>,
) {
let mut offset = self.horizontal_scroll_handle.offset();
let col_bounds = col_group.bounds;
if pos.x < self.bounds.left() && col_bounds.right() < self.bounds.left() + px(20.) {
offset.x += px(1.);
} else if pos.x > self.bounds.right() && col_bounds.right() > self.bounds.right() - px(20.)
{
offset.x -= px(1.);
}
self.horizontal_scroll_handle.set_offset(offset);
}
/// The `ix`` is the index of the col to resize,
/// and the `size` is the new size for the col.
fn resize_cols(&mut self, ix: usize, size: Pixels, cx: &mut ViewContext<Self>) {
const MIN_WIDTH: Pixels = px(10.0);
const MAX_WIDTH: Pixels = px(1200.0);
if !self.delegate.can_resize_col(ix) {
return;
}
@ -380,7 +443,7 @@ where
if changed_width > px(-1.0) && changed_width < px(1.0) {
return;
}
self.col_groups[ix].width = Some(new_width);
self.col_groups[ix].width = Some(new_width.min(MAX_WIDTH));
// Resize next col, table not need to resize the right cols.
// let next_width = self.col_groups[ix + 1].width.unwrap_or_default();
@ -395,16 +458,51 @@ where
/// Becuase the horizontal scroll handle will use the child_item_bounds to
/// calculate the item position for itself's `scroll_to_item` method.
fn render_th(&self, col_ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
let entity_id = cx.entity_id();
let col_group = self.col_groups.get(col_ix).expect("BUG: invalid col index");
let name = self.delegate.col_name(col_ix);
self.col_wrap(col_ix, cx)
.child(
self.render_cell(col_ix, cx)
.id(("col-header", col_ix))
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, _, cx| {
this.on_col_head_click(col_ix, cx);
}),
)
.child(self.delegate.render_th(col_ix)),
.child(self.delegate.render_th(col_ix))
.when(self.delegate.can_move_col(col_ix), |this| {
this.on_drag(
DragCol {
entity_id,
col_ix,
name,
width: col_group.width,
},
|drag, cx| {
cx.stop_propagation();
cx.new_view(|_| drag.clone())
},
)
.drag_over::<DragCol>(|this, _, cx| {
this.rounded_l_none()
.border_l_2()
.border_r_0()
.border_color(cx.theme().drag_border)
})
.on_drop(cx.listener(
move |table, drag: &DragCol, cx| {
// If the drag col is not the same as the drop col, then swap the cols.
if drag.entity_id != cx.entity_id() {
return;
}
table.move_col(drag.col_ix, col_ix, cx);
},
))
}),
)
// resize handle
.child(self.render_resize_handle(col_ix, cx))
@ -419,6 +517,18 @@ where
.size_full()
})
}
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();
}
}
impl<D> FocusableView for Table<D>
@ -558,6 +668,7 @@ where
),
);
let view = cx.view().clone();
div()
.size_full()
.rounded_md()
@ -571,5 +682,9 @@ where
ScrollableAxis::Horizontal,
&horizontal_scroll_handle,
))
.child(canvas(
move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds),
|_, _, _| {},
))
}
}