Add to support Table column resizing. (#75)

https://github.com/user-attachments/assets/d56f2cd7-df08-437b-9eea-849d68094e1f
This commit is contained in:
Jason Lee 2024-07-28 00:51:37 +08:00 committed by GitHub
parent 11034ec2ac
commit 79958fc95c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 276 additions and 92 deletions

View file

@ -1,13 +1,12 @@
use fake::Fake; use fake::Fake;
use gpui::{ use gpui::{
ParentElement, Render, SharedString, Styled, View, ViewContext, ParentElement, Pixels, Render, SharedString, Styled, View, ViewContext, VisualContext as _,
VisualContext as _, WindowContext, WindowContext,
}; };
use ui::{ use ui::{
checkbox::Checkbox, checkbox::Checkbox,
h_flex, h_flex,
table::{Table, TableDelegate}, table::{Table, TableDelegate, TableEvent},
v_flex, Selectable, Selection, v_flex, Selectable, Selection,
}; };
@ -97,7 +96,7 @@ impl TableDelegate for CustomerTableDelegate {
} }
} }
fn col_width(&self, col_ix: usize) -> Option<f32> { fn col_width(&self, col_ix: usize) -> Option<Pixels> {
match col_ix { match col_ix {
0 => Some(50.0), 0 => Some(50.0),
1 => Some(220.0), 1 => Some(220.0),
@ -115,6 +114,15 @@ impl TableDelegate for CustomerTableDelegate {
13 => Some(90.0), 13 => Some(90.0),
_ => None, _ => 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);
} }
fn render_td(&self, row_ix: usize, col_ix: usize) -> impl gpui::IntoElement { fn render_td(&self, row_ix: usize, col_ix: usize) -> impl gpui::IntoElement {
@ -156,6 +164,9 @@ impl TableStory {
fn new(cx: &mut ViewContext<Self>) -> Self { fn new(cx: &mut ViewContext<Self>) -> Self {
let delegate = CustomerTableDelegate::new(2000); let delegate = CustomerTableDelegate::new(2000);
let table = cx.new_view(|cx| Table::new(delegate, cx)); let table = cx.new_view(|cx| Table::new(delegate, cx));
cx.subscribe(&table, Self::on_table_event).detach();
Self { table } Self { table }
} }
@ -166,6 +177,21 @@ impl TableStory {
cx.notify(); cx.notify();
}); });
} }
fn on_table_event(
&mut self,
_: View<Table<CustomerTableDelegate>>,
event: &TableEvent,
_cx: &mut ViewContext<Self>,
) {
match event {
TableEvent::ColWidthsChanged(col_widths) => {
println!("Col widths changed: {:?}", col_widths)
}
TableEvent::SelectCol(ix) => println!("Select col: {}", ix),
TableEvent::SelectRow(ix) => println!("Select row: {}", ix),
}
}
} }
impl Render for TableStory { impl Render for TableStory {

View file

@ -3,14 +3,15 @@ use std::{cell::Cell, rc::Rc};
use crate::{ use crate::{
h_flex, h_flex,
scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState}, scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState},
theme::{ActiveTheme, Colorize}, theme::ActiveTheme,
v_flex, v_flex,
}; };
use gpui::{ use gpui::{
actions, div, prelude::FluentBuilder as _, px, uniform_list, AppContext, Div, FocusHandle, actions, canvas, div, prelude::FluentBuilder as _, px, uniform_list, AppContext, Bounds, Div,
FocusableView, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, DragMoveEvent, EntityId, EventEmitter, FocusHandle, FocusableView, InteractiveElement as _,
ParentElement as _, Render, ScrollHandle, SharedString, StatefulInteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement as _, Pixels, Render, ScrollHandle,
Styled, UniformListScrollHandle, ViewContext, WindowContext, SharedString, StatefulInteractiveElement as _, Styled, UniformListScrollHandle, ViewContext,
VisualContext as _, WindowContext,
}; };
actions!( actions!(
@ -36,15 +37,26 @@ pub fn init(cx: &mut AppContext) {
} }
struct ColGroup { struct ColGroup {
width: Option<f32>, width: Option<Pixels>,
bounds: Bounds<Pixels>,
} }
#[derive(Clone, Render)]
pub struct DragCol(pub (EntityId, usize));
#[derive(Copy, Clone, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum SelectionState { enum SelectionState {
Column, Column,
Row, Row,
} }
#[derive(Clone)]
pub enum TableEvent {
SelectRow(usize),
SelectCol(usize),
ColWidthsChanged(Vec<Option<Pixels>>),
}
pub struct Table<D: TableDelegate> { pub struct Table<D: TableDelegate> {
focus_handle: FocusHandle, focus_handle: FocusHandle,
delegate: D, delegate: D,
@ -57,6 +69,9 @@ pub struct Table<D: TableDelegate> {
selection_state: SelectionState, selection_state: SelectionState,
selected_row: Option<usize>, selected_row: Option<usize>,
selected_col: Option<usize>, selected_col: Option<usize>,
/// The column index that is being resized.
resizing_col: Option<usize>,
} }
#[allow(unused)] #[allow(unused)]
@ -76,10 +91,12 @@ pub trait TableDelegate: Sized + 'static {
/// Returns the width of the column at the given index. /// Returns the width of the column at the given index.
/// Return None, use auto width. /// Return None, use auto width.
fn col_width(&self, col_ix: usize) -> Option<f32>; ///
/// This is only called when the table initializes.
fn col_width(&self, col_ix: usize) -> Option<Pixels>;
/// Set the width of the column at the given index. /// When the column has resized, this method is called.
fn on_col_width_changed(&mut self, col_ix: usize, width: Option<f32>) {} 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. /// Render the header cell at the given column index, default to the column name.
fn render_th(&self, col_ix: usize) -> impl IntoElement { fn render_th(&self, col_ix: usize) -> impl IntoElement {
@ -114,9 +131,10 @@ where
selection_state: SelectionState::Row, selection_state: SelectionState::Row,
selected_row: None, selected_row: None,
selected_col: None, selected_col: None,
resizing_col: None,
}; };
this.update_col_groups(cx); this.prepare_col_groups(cx);
this this
} }
@ -128,38 +146,42 @@ where
&mut self.delegate &mut self.delegate
} }
fn update_col_groups(&mut self, cx: &mut ViewContext<Self>) { fn prepare_col_groups(&mut self, cx: &mut ViewContext<Self>) {
self.col_groups = (0..self.delegate.cols_count()) self.col_groups = (0..self.delegate.cols_count())
.map(|col_ix| ColGroup { .map(|col_ix| ColGroup {
width: self.delegate.col_width(col_ix), width: self.delegate.col_width(col_ix),
bounds: Bounds::default(),
}) })
.collect(); .collect();
cx.notify(); cx.notify();
} }
fn scroll_to_selected_row(&mut self, _cx: &mut ViewContext<Self>) { fn set_selected_row(&mut self, row_ix: usize, cx: &mut ViewContext<Self>) {
self.selection_state = SelectionState::Row;
self.selected_row = Some(row_ix);
if let Some(row_ix) = self.selected_row { if let Some(row_ix) = self.selected_row {
self.vertical_scroll_handle.scroll_to_item(row_ix); self.vertical_scroll_handle.scroll_to_item(row_ix);
} }
cx.emit(TableEvent::SelectRow(row_ix));
cx.notify();
} }
fn scroll_to_selected_column(&mut self, cx: &mut ViewContext<Self>) { fn set_selected_col(&mut self, col_ix: usize, cx: &mut ViewContext<Self>) {
self.selection_state = SelectionState::Column;
self.selected_col = Some(col_ix);
if let Some(col_ix) = self.selected_col { if let Some(col_ix) = self.selected_col {
self.horizontal_scroll_handle.scroll_to_item(col_ix); self.horizontal_scroll_handle.scroll_to_item(col_ix);
cx.notify();
} }
cx.emit(TableEvent::SelectCol(col_ix));
cx.notify();
} }
fn on_row_click(&mut self, row_ix: usize, cx: &mut ViewContext<Self>) { fn on_row_click(&mut self, row_ix: usize, cx: &mut ViewContext<Self>) {
self.selection_state = SelectionState::Row; self.set_selected_row(row_ix, cx)
self.selected_row = Some(row_ix);
self.scroll_to_selected_row(cx);
} }
fn on_col_head_click(&mut self, col_ix: usize, cx: &mut ViewContext<Self>) { fn on_col_head_click(&mut self, col_ix: usize, cx: &mut ViewContext<Self>) {
self.selection_state = SelectionState::Column; self.set_selected_col(col_ix, cx)
self.selected_col = Some(col_ix);
self.scroll_to_selected_column(cx);
} }
fn action_cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) { fn action_cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
@ -170,72 +192,63 @@ where
} }
fn action_select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) { fn action_select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
let selected_row = self.selected_row.unwrap_or(0); let mut selected_row = self.selected_row.unwrap_or(0);
let rows_count = self.delegate.rows_count(); let rows_count = self.delegate.rows_count();
if selected_row > 0 { if selected_row > 0 {
self.selected_row = Some(selected_row - 1); selected_row = selected_row - 1;
} else { } else {
if self.delegate.can_loop_select() { if self.delegate.can_loop_select() {
self.selected_row = Some(rows_count - 1); selected_row = rows_count - 1;
} }
} }
self.selection_state = SelectionState::Row; self.set_selected_row(selected_row, cx);
self.scroll_to_selected_row(cx);
cx.notify();
} }
fn action_select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) { fn action_select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
let selected_row = self.selected_row.unwrap_or(0); let mut selected_row = self.selected_row.unwrap_or(0);
if selected_row < self.delegate.rows_count() - 1 { if selected_row < self.delegate.rows_count() - 1 {
self.selected_row = Some(selected_row + 1); selected_row += 1;
} else { } else {
if self.delegate.can_loop_select() { if self.delegate.can_loop_select() {
self.selected_row = Some(0); selected_row = 0;
} }
} }
self.selection_state = SelectionState::Row; self.set_selected_row(selected_row, cx);
self.scroll_to_selected_row(cx);
cx.notify();
} }
fn action_select_prev_column(&mut self, _: &SelectPrevColumn, cx: &mut ViewContext<Self>) { fn action_select_prev_col(&mut self, _: &SelectPrevColumn, cx: &mut ViewContext<Self>) {
let selected_col = self.selected_col.unwrap_or(0); let mut selected_col = self.selected_col.unwrap_or(0);
let cols_count = self.delegate.cols_count(); let cols_count = self.delegate.cols_count();
if selected_col > 0 { if selected_col > 0 {
self.selected_col = Some(selected_col - 1); selected_col -= 1;
} else { } else {
if self.delegate.can_loop_select() { if self.delegate.can_loop_select() {
self.selected_col = Some(cols_count - 1); selected_col = cols_count - 1;
} }
} }
self.set_selected_col(selected_col, cx);
self.selection_state = SelectionState::Column;
self.scroll_to_selected_column(cx);
cx.notify();
} }
fn action_select_next_column(&mut self, _: &SelectNextColumn, cx: &mut ViewContext<Self>) { fn action_select_next_col(&mut self, _: &SelectNextColumn, cx: &mut ViewContext<Self>) {
let selected_col = self.selected_col.unwrap_or(0); let mut selected_col = self.selected_col.unwrap_or(0);
if selected_col < self.delegate.cols_count() - 1 { if selected_col < self.delegate.cols_count() - 1 {
self.selected_col = Some(selected_col + 1); selected_col += 1;
} else { } else {
if self.delegate.can_loop_select() { if self.delegate.can_loop_select() {
self.selected_col = Some(0); selected_col = 0;
} }
} }
self.selection_state = SelectionState::Column; self.set_selected_col(selected_col, cx);
self.scroll_to_selected_column(cx);
cx.notify();
} }
fn render_cell(&self, col_ix: usize, _cx: &mut ViewContext<Self>) -> Div { fn render_cell(&self, col_ix: usize, _cx: &mut ViewContext<Self>) -> Div {
let col_width = self.col_groups[col_ix].width; let col_width = self.col_groups[col_ix].width;
div() div()
.when_some(col_width, |this, width| this.w(px(width))) .when_some(col_width, |this, width| this.w(width))
.overflow_hidden() .overflow_hidden()
.whitespace_nowrap() .whitespace_nowrap()
.py_1() .py_1()
@ -245,9 +258,9 @@ where
/// Show Column selection style, when the column is selected and the selection state is Column. /// Show Column selection style, when the column is selected and the selection state is Column.
fn col_wrap(&self, col_ix: usize, cx: &mut ViewContext<Self>) -> Div { fn col_wrap(&self, col_ix: usize, cx: &mut ViewContext<Self>) -> Div {
if self.selected_col == Some(col_ix) && self.selection_state == SelectionState::Column { if self.selected_col == Some(col_ix) && self.selection_state == SelectionState::Column {
div().bg(cx.theme().accent.opacity(0.5)) h_flex().bg(cx.theme().table_active)
} else { } else {
div() h_flex()
} }
} }
@ -270,6 +283,142 @@ where
)), )),
) )
} }
fn render_resize_handle(&self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
const HANDLE_SIZE: Pixels = px(3.);
if !self.delegate.can_resize_col(ix) {
return div().into_any_element();
}
let group_id: SharedString = format!("resizable-handle-{}", ix).into();
let is_resizing = self.resizing_col == Some(ix);
h_flex()
.id(("resizable-handle", ix))
.group(group_id.clone())
.occlude()
.cursor_col_resize()
.h_full()
.w(HANDLE_SIZE)
.ml(-(HANDLE_SIZE))
.justify_end()
.items_center()
.child(
div()
.h_full()
.h_5()
.justify_center()
.bg(cx.theme().border)
.when(is_resizing, |this| this.bg(cx.theme().drag_border))
.group_hover(group_id, |this| this.bg(cx.theme().drag_border))
.w(px(1.)),
)
.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)) => {
if cx.entity_id() != *entity_id {
return;
}
// sync col widths into real widths
for (_, col_group) in view.col_groups.iter_mut().enumerate() {
col_group.width = Some(col_group.bounds.size.width);
}
let ix = *ix;
view.resizing_col = Some(ix);
let col_group = view.col_groups.get(ix).expect("BUG: invalid col index");
view.resize_cols(
ix,
e.event.position.x - HANDLE_SIZE - col_group.bounds.left(),
cx,
);
}
},
))
.on_drag(DragCol((cx.entity_id(), ix)), |drag, cx| {
cx.stop_propagation();
cx.new_view(|_| drag.clone())
})
.on_mouse_up_out(
MouseButton::Left,
cx.listener(|view, _, cx| {
if view.resizing_col.is_none() {
return;
}
view.resizing_col = None;
let new_widths = view.col_groups.iter().map(|g| g.width).collect();
cx.emit(TableEvent::ColWidthsChanged(new_widths));
cx.notify();
}),
)
.into_any_element()
}
/// 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);
if !self.delegate.can_resize_col(ix) {
return;
}
let size = size.floor();
let old_width = self.col_groups[ix].width.unwrap_or_default();
let new_width = size;
if new_width < MIN_WIDTH {
return;
}
let changed_width = new_width - old_width;
// If change size is less than 1px, do nothing.
if changed_width > px(-1.0) && changed_width < px(1.0) {
return;
}
self.col_groups[ix].width = Some(new_width);
// Resize next col, table not need to resize the right cols.
// let next_width = self.col_groups[ix + 1].width.unwrap_or_default();
// let next_width = (next_width - changed_width).max(MIN_WIDTH);
// self.col_groups[ix + 1].width = Some(next_width);
cx.notify();
}
/// Render the column header.
/// The children must be one by one items.
/// 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 {
self.col_wrap(col_ix, cx)
.child(
self.render_cell(col_ix, cx)
.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)),
)
// resize handle
.child(self.render_resize_handle(col_ix, cx))
// to save the bounds of this col.
.child({
let view = cx.view().clone();
canvas(
move |bounds, cx| view.update(cx, |r, _| r.col_groups[col_ix].bounds = bounds),
|_, _, _| {},
)
.absolute()
.size_full()
})
}
} }
impl<D> FocusableView for Table<D> impl<D> FocusableView for Table<D>
@ -281,6 +430,8 @@ where
} }
} }
impl<D> EventEmitter<TableEvent> for Table<D> where D: TableDelegate {}
impl<D> Render for Table<D> impl<D> Render for Table<D>
where where
D: TableDelegate, D: TableDelegate,
@ -291,11 +442,13 @@ where
let horizontal_scroll_handle = self.horizontal_scroll_handle.clone(); let horizontal_scroll_handle = self.horizontal_scroll_handle.clone();
let cols_count: usize = self.delegate.cols_count(); let cols_count: usize = self.delegate.cols_count();
let rows_count = self.delegate.rows_count(); let rows_count = self.delegate.rows_count();
let selected_bg = cx.theme().accent.opacity(0.8);
let hover_bg = cx.theme().accent.opacity(0.5);
fn tr(cx: &mut WindowContext) -> Div { fn last_empty_col(_: &mut WindowContext) -> Div {
h_flex().gap_1().border_color(cx.theme().border) h_flex().w(px(100.)).h_full().flex_shrink_0()
}
fn tr(_: &mut WindowContext) -> Div {
h_flex()
} }
let inner_table = v_flex() let inner_table = v_flex()
@ -305,8 +458,8 @@ where
.on_action(cx.listener(Self::action_cancel)) .on_action(cx.listener(Self::action_cancel))
.on_action(cx.listener(Self::action_select_next)) .on_action(cx.listener(Self::action_select_next))
.on_action(cx.listener(Self::action_select_prev)) .on_action(cx.listener(Self::action_select_prev))
.on_action(cx.listener(Self::action_select_next_column)) .on_action(cx.listener(Self::action_select_next_col))
.on_action(cx.listener(Self::action_select_prev_column)) .on_action(cx.listener(Self::action_select_prev_col))
.size_full() .size_full()
.overflow_hidden() .overflow_hidden()
.child( .child(
@ -314,7 +467,6 @@ where
.flex_grow() .flex_grow()
.h_10() .h_10()
.w_full() .w_full()
.shadow_sm()
.border_b_1() .border_b_1()
.border_color(cx.theme().border) .border_color(cx.theme().border)
.child( .child(
@ -322,30 +474,22 @@ where
let horizontal_scroll_handle = horizontal_scroll_handle.clone(); let horizontal_scroll_handle = horizontal_scroll_handle.clone();
move |table, _, cx| { move |table, _, cx| {
// Columns // Columns
vec![tr(cx) tr(cx)
.id("table-head") .id("table-head")
.w_full() .w_full()
.h_10()
.overflow_scroll() .overflow_scroll()
.track_scroll(&horizontal_scroll_handle) .track_scroll(&horizontal_scroll_handle)
// The children must be one by one items. .bg(cx.theme().table_head)
// Becuase the horizontal scroll handle will use the child_item_bounds to .children(
// calculate the item position for itself's `scroll_to_item` method. table
.children(table.col_groups.iter().enumerate().map( .col_groups
|(col_ix, _)| { .iter()
table.col_wrap(col_ix, cx).child( .enumerate()
table .map(|(col_ix, _)| table.render_th(col_ix, cx)),
.render_cell(col_ix, cx) )
.on_mouse_down( .child(last_empty_col(cx))
MouseButton::Left, .map(|this| vec![this])
cx.listener(move |this, _, cx| {
this.on_col_head_click(col_ix, cx);
}),
)
.child(table.delegate.render_th(col_ix)),
)
},
))
.flex_1()]
} }
}) })
.size_full(), .size_full(),
@ -361,6 +505,17 @@ where
tr(cx) tr(cx)
.id(("table-row", row_ix)) .id(("table-row", row_ix))
.w_full() .w_full()
.when(row_ix > 0, |this| this.border_t_1())
.when(row_ix % 2 == 0, |this| {
this.bg(cx.theme().table_even)
})
.hover(|this| {
if table.selected_row.is_some() {
this
} else {
this.bg(cx.theme().table_hover)
}
})
.children((0..cols_count).map(|col_ix| { .children((0..cols_count).map(|col_ix| {
table table
.col_wrap(col_ix, cx) // Make the row scroll sync with the horizontal_scroll_handle to support horizontal scrolling. .col_wrap(col_ix, cx) // Make the row scroll sync with the horizontal_scroll_handle to support horizontal scrolling.
@ -376,20 +531,13 @@ where
), ),
) )
})) }))
.when(row_ix > 0, |this| this.border_t_1()) .child(last_empty_col(cx))
.hover(|this| {
if table.selected_row.is_some() {
this
} else {
this.bg(hover_bg)
}
})
// Row selected style // Row selected style
.when_some(table.selected_row, |this, selected_row| { .when_some(table.selected_row, |this, selected_row| {
this.when( this.when(
row_ix == selected_row row_ix == selected_row
&& table.selection_state == SelectionState::Row, && table.selection_state == SelectionState::Row,
|this| this.bg(selected_bg), |this| this.bg(cx.theme().table_active),
) )
}) })
.on_mouse_down( .on_mouse_down(
@ -415,7 +563,7 @@ where
.rounded_md() .rounded_md()
.border_1() .border_1()
.border_color(cx.theme().border) .border_color(cx.theme().border)
.bg(cx.theme().card) .bg(cx.theme().table)
.child(inner_table) .child(inner_table)
.children(self.render_scrollbar(cx)) .children(self.render_scrollbar(cx))
.child(ScrollableMask::new( .child(ScrollableMask::new(

View file

@ -331,6 +331,11 @@ pub struct Theme {
pub slider_thumb: Hsla, pub slider_thumb: Hsla,
pub list_item_active: Hsla, pub list_item_active: Hsla,
pub list_item_hover: Hsla, pub list_item_hover: Hsla,
pub table: Hsla,
pub table_even: Hsla,
pub table_head: Hsla,
pub table_active: Hsla,
pub table_hover: Hsla,
} }
impl Global for Theme {} impl Global for Theme {}
@ -398,6 +403,11 @@ impl From<Colors> for Theme {
slider_thumb: colors.background, slider_thumb: colors.background,
list_item_active: colors.secondary_active, list_item_active: colors.secondary_active,
list_item_hover: colors.secondary, list_item_hover: colors.secondary,
table_head: colors.secondary.opacity(0.5),
table: colors.background,
table_even: colors.secondary.opacity(0.3),
table_active: colors.secondary_active,
table_hover: colors.secondary_active.opacity(0.7),
} }
} }
} }