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 gpui::{
ParentElement, Render, SharedString, Styled, View, ViewContext,
VisualContext as _, WindowContext,
ParentElement, Pixels, Render, SharedString, Styled, View, ViewContext, VisualContext as _,
WindowContext,
};
use ui::{
checkbox::Checkbox,
h_flex,
table::{Table, TableDelegate},
table::{Table, TableDelegate, TableEvent},
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 {
0 => Some(50.0),
1 => Some(220.0),
@ -115,6 +114,15 @@ impl TableDelegate for CustomerTableDelegate {
13 => Some(90.0),
_ => 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 {
@ -156,6 +164,9 @@ impl TableStory {
fn new(cx: &mut ViewContext<Self>) -> Self {
let delegate = CustomerTableDelegate::new(2000);
let table = cx.new_view(|cx| Table::new(delegate, cx));
cx.subscribe(&table, Self::on_table_event).detach();
Self { table }
}
@ -166,6 +177,21 @@ impl TableStory {
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 {

View file

@ -3,14 +3,15 @@ use std::{cell::Cell, rc::Rc};
use crate::{
h_flex,
scroll::{ScrollableAxis, ScrollableMask, Scrollbar, ScrollbarState},
theme::{ActiveTheme, Colorize},
theme::ActiveTheme,
v_flex,
};
use gpui::{
actions, div, prelude::FluentBuilder as _, px, uniform_list, AppContext, Div, FocusHandle,
FocusableView, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
ParentElement as _, Render, ScrollHandle, SharedString, StatefulInteractiveElement as _,
Styled, UniformListScrollHandle, ViewContext, WindowContext,
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,
SharedString, StatefulInteractiveElement as _, Styled, UniformListScrollHandle, ViewContext,
VisualContext as _, WindowContext,
};
actions!(
@ -36,15 +37,26 @@ pub fn init(cx: &mut AppContext) {
}
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)]
enum SelectionState {
Column,
Row,
}
#[derive(Clone)]
pub enum TableEvent {
SelectRow(usize),
SelectCol(usize),
ColWidthsChanged(Vec<Option<Pixels>>),
}
pub struct Table<D: TableDelegate> {
focus_handle: FocusHandle,
delegate: D,
@ -57,6 +69,9 @@ pub struct Table<D: TableDelegate> {
selection_state: SelectionState,
selected_row: Option<usize>,
selected_col: Option<usize>,
/// The column index that is being resized.
resizing_col: Option<usize>,
}
#[allow(unused)]
@ -76,10 +91,12 @@ pub trait TableDelegate: Sized + 'static {
/// Returns the width of the column at the given index.
/// 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.
fn on_col_width_changed(&mut self, col_ix: usize, width: Option<f32>) {}
/// 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 {
@ -114,9 +131,10 @@ where
selection_state: SelectionState::Row,
selected_row: None,
selected_col: None,
resizing_col: None,
};
this.update_col_groups(cx);
this.prepare_col_groups(cx);
this
}
@ -128,38 +146,42 @@ where
&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())
.map(|col_ix| ColGroup {
width: self.delegate.col_width(col_ix),
bounds: Bounds::default(),
})
.collect();
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 {
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 {
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>) {
self.selection_state = SelectionState::Row;
self.selected_row = Some(row_ix);
self.scroll_to_selected_row(cx);
self.set_selected_row(row_ix, cx)
}
fn on_col_head_click(&mut self, col_ix: usize, cx: &mut ViewContext<Self>) {
self.selection_state = SelectionState::Column;
self.selected_col = Some(col_ix);
self.scroll_to_selected_column(cx);
self.set_selected_col(col_ix, cx)
}
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>) {
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();
if selected_row > 0 {
self.selected_row = Some(selected_row - 1);
selected_row = selected_row - 1;
} else {
if self.delegate.can_loop_select() {
self.selected_row = Some(rows_count - 1);
selected_row = rows_count - 1;
}
}
self.selection_state = SelectionState::Row;
self.scroll_to_selected_row(cx);
cx.notify();
self.set_selected_row(selected_row, cx);
}
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 {
self.selected_row = Some(selected_row + 1);
selected_row += 1;
} else {
if self.delegate.can_loop_select() {
self.selected_row = Some(0);
selected_row = 0;
}
}
self.selection_state = SelectionState::Row;
self.scroll_to_selected_row(cx);
cx.notify();
self.set_selected_row(selected_row, cx);
}
fn action_select_prev_column(&mut self, _: &SelectPrevColumn, cx: &mut ViewContext<Self>) {
let selected_col = self.selected_col.unwrap_or(0);
fn action_select_prev_col(&mut self, _: &SelectPrevColumn, cx: &mut ViewContext<Self>) {
let mut selected_col = self.selected_col.unwrap_or(0);
let cols_count = self.delegate.cols_count();
if selected_col > 0 {
self.selected_col = Some(selected_col - 1);
selected_col -= 1;
} else {
if self.delegate.can_loop_select() {
self.selected_col = Some(cols_count - 1);
selected_col = cols_count - 1;
}
}
self.selection_state = SelectionState::Column;
self.scroll_to_selected_column(cx);
cx.notify();
self.set_selected_col(selected_col, cx);
}
fn action_select_next_column(&mut self, _: &SelectNextColumn, cx: &mut ViewContext<Self>) {
let selected_col = self.selected_col.unwrap_or(0);
fn action_select_next_col(&mut self, _: &SelectNextColumn, cx: &mut ViewContext<Self>) {
let mut selected_col = self.selected_col.unwrap_or(0);
if selected_col < self.delegate.cols_count() - 1 {
self.selected_col = Some(selected_col + 1);
selected_col += 1;
} else {
if self.delegate.can_loop_select() {
self.selected_col = Some(0);
selected_col = 0;
}
}
self.selection_state = SelectionState::Column;
self.scroll_to_selected_column(cx);
cx.notify();
self.set_selected_col(selected_col, cx);
}
fn render_cell(&self, col_ix: usize, _cx: &mut ViewContext<Self>) -> Div {
let col_width = self.col_groups[col_ix].width;
div()
.when_some(col_width, |this, width| this.w(px(width)))
.when_some(col_width, |this, width| this.w(width))
.overflow_hidden()
.whitespace_nowrap()
.py_1()
@ -245,9 +258,9 @@ where
/// 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 {
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 {
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>
@ -281,6 +430,8 @@ where
}
}
impl<D> EventEmitter<TableEvent> for Table<D> where D: TableDelegate {}
impl<D> Render for Table<D>
where
D: TableDelegate,
@ -291,11 +442,13 @@ where
let horizontal_scroll_handle = self.horizontal_scroll_handle.clone();
let cols_count: usize = self.delegate.cols_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 {
h_flex().gap_1().border_color(cx.theme().border)
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 inner_table = v_flex()
@ -305,8 +458,8 @@ where
.on_action(cx.listener(Self::action_cancel))
.on_action(cx.listener(Self::action_select_next))
.on_action(cx.listener(Self::action_select_prev))
.on_action(cx.listener(Self::action_select_next_column))
.on_action(cx.listener(Self::action_select_prev_column))
.on_action(cx.listener(Self::action_select_next_col))
.on_action(cx.listener(Self::action_select_prev_col))
.size_full()
.overflow_hidden()
.child(
@ -314,7 +467,6 @@ where
.flex_grow()
.h_10()
.w_full()
.shadow_sm()
.border_b_1()
.border_color(cx.theme().border)
.child(
@ -322,30 +474,22 @@ where
let horizontal_scroll_handle = horizontal_scroll_handle.clone();
move |table, _, cx| {
// Columns
vec![tr(cx)
tr(cx)
.id("table-head")
.w_full()
.h_10()
.overflow_scroll()
.track_scroll(&horizontal_scroll_handle)
// 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.
.children(table.col_groups.iter().enumerate().map(
|(col_ix, _)| {
table.col_wrap(col_ix, cx).child(
table
.render_cell(col_ix, cx)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, _, cx| {
this.on_col_head_click(col_ix, cx);
}),
)
.child(table.delegate.render_th(col_ix)),
)
},
))
.flex_1()]
.bg(cx.theme().table_head)
.children(
table
.col_groups
.iter()
.enumerate()
.map(|(col_ix, _)| table.render_th(col_ix, cx)),
)
.child(last_empty_col(cx))
.map(|this| vec![this])
}
})
.size_full(),
@ -361,6 +505,17 @@ where
tr(cx)
.id(("table-row", row_ix))
.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| {
table
.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())
.hover(|this| {
if table.selected_row.is_some() {
this
} else {
this.bg(hover_bg)
}
})
.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(selected_bg),
|this| this.bg(cx.theme().table_active),
)
})
.on_mouse_down(
@ -415,7 +563,7 @@ where
.rounded_md()
.border_1()
.border_color(cx.theme().border)
.bg(cx.theme().card)
.bg(cx.theme().table)
.child(inner_table)
.children(self.render_scrollbar(cx))
.child(ScrollableMask::new(

View file

@ -331,6 +331,11 @@ pub struct Theme {
pub slider_thumb: Hsla,
pub list_item_active: 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 {}
@ -398,6 +403,11 @@ impl From<Colors> for Theme {
slider_thumb: colors.background,
list_item_active: colors.secondary_active,
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),
}
}
}