diff --git a/crates/story/src/table_story.rs b/crates/story/src/table_story.rs index 5d209a05..34e68bc2 100644 --- a/crates/story/src/table_story.rs +++ b/crates/story/src/table_story.rs @@ -6,9 +6,9 @@ use std::{ use fake::Fake; use gpui::{ - div, prelude::FluentBuilder as _, px, Action, AnyElement, App, AppContext, ClickEvent, Context, - Edges, Entity, Focusable, InteractiveElement, IntoElement, ParentElement, Pixels, Render, - SharedString, StatefulInteractiveElement, Styled, TextAlign, Timer, Window, + div, prelude::FluentBuilder as _, Action, AnyElement, App, AppContext, ClickEvent, Context, + Entity, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, + StatefulInteractiveElement, Styled, TextAlign, Timer, Window, }; use gpui_component::{ button::Button, @@ -18,7 +18,7 @@ use gpui_component::{ input::{InputEvent, InputState, TextInput}, label::Label, popup_menu::{PopupMenu, PopupMenuExt}, - table::{self, ColFixed, ColSort, Table, TableDelegate, TableEvent}, + table::{Column, ColumnFixed, ColumnSort, Table, TableDelegate, TableEvent}, v_flex, ActiveTheme as _, Selectable, Sizable as _, Size, StyleSized as _, StyledExt, }; use serde::{Deserialize, Serialize}; @@ -127,7 +127,7 @@ fn random_stocks(size: usize) -> Vec { id, counter: Counter::random(), change: (-100.0..100.0).fake(), - change_percent: (-1.0..1.0).fake(), + change_percent: (-0.1..0.1).fake(), volume: (0.0..1000.0).fake(), turnover: (0.0..1000.0).fake(), market_cap: (0.0..1000.0).fake(), @@ -171,73 +171,12 @@ fn random_stocks(size: usize) -> Vec { .collect() } -struct Column { - id: SharedString, - name: SharedString, - align: TextAlign, - sort: Option, - paddings: Option>, - width: Pixels, - fixed: bool, - resizable: bool, -} - -impl Column { - fn new(id: impl Into, name: impl Into) -> Self { - Self { - id: id.into(), - name: name.into(), - align: TextAlign::Left, - sort: None, - paddings: None, - width: px(100.), - fixed: false, - resizable: true, - } - } - - fn sortable(mut self) -> Self { - self.sort = Some(ColSort::Default); - self - } - - fn text_right(mut self) -> Self { - self.align = TextAlign::Right; - self - } - - fn p_0(mut self) -> Self { - self.paddings = Some(Edges::all(px(0.))); - self - } - - fn w(mut self, width: impl Into) -> Self { - self.width = width.into(); - self - } - - fn fixed(mut self) -> Self { - self.fixed = true; - self - } - - fn resizable(mut self, resizable: bool) -> Self { - self.resizable = resizable; - self - } -} - struct StockTableDelegate { stocks: Vec, columns: Vec, size: Size, - col_resize: bool, - col_order: bool, - col_sort: bool, - col_selection: bool, loading: bool, full_loading: bool, - fixed_cols: bool, eof: bool, visible_rows: Range, visible_cols: Range, @@ -249,13 +188,21 @@ impl StockTableDelegate { size: Size::default(), stocks: random_stocks(size), columns: vec![ - Column::new("id", "ID").w(60.).fixed().resizable(false), - Column::new("market", "Market") - .w(60.) - .fixed() + Column::new("id", "ID") + .width(60.) + .fixed(ColumnFixed::Left) .resizable(false), - Column::new("symbol", "Symbol").w(100.).fixed().sortable(), - Column::new("name", "Name").w(180.).fixed(), + Column::new("market", "Market") + .width(60.) + .fixed(ColumnFixed::Left) + .resizable(false), + Column::new("name", "Name") + .width(180.) + .fixed(ColumnFixed::Left), + Column::new("symbol", "Symbol") + .width(100.) + .fixed(ColumnFixed::Left) + .sortable(), Column::new("price", "Price").sortable().text_right().p_0(), Column::new("change", "Chg").sortable().text_right().p_0(), Column::new("change_percent", "Chg%") @@ -307,11 +254,6 @@ impl StockTableDelegate { Column::new("day_120_ranking", "120d Ranking"), Column::new("day_250_ranking", "250d Ranking"), ], - col_resize: true, - col_order: true, - col_sort: true, - col_selection: true, - fixed_cols: false, loading: false, full_loading: false, eof: false, @@ -327,6 +269,30 @@ impl StockTableDelegate { self.full_loading = false; } + fn render_percent(&self, col: &Column, val: f64, cx: &mut Context>) -> AnyElement { + let right_num = ((val - val.floor()) * 1000.).floor() as i32; + + div() + .h_full() + .table_cell_size(self.size) + .when(col.align == TextAlign::Right, |this| { + this.h_flex().justify_end() + }) + .map(|this| { + if right_num % 3 == 0 { + this.text_color(cx.theme().red) + .bg(cx.theme().red_light.alpha(0.05)) + } else if right_num % 3 == 1 { + this.text_color(cx.theme().green) + .bg(cx.theme().green_light.alpha(0.05)) + } else { + this + } + }) + .child(format!("{:.2}%", val * 100.)) + .into_any_element() + } + fn render_value_cell( &self, col: &Column, @@ -359,7 +325,7 @@ impl StockTableDelegate { } impl TableDelegate for StockTableDelegate { - fn cols_count(&self, _: &App) -> usize { + fn columns_count(&self, _: &App) -> usize { self.columns.len() } @@ -367,54 +333,8 @@ impl TableDelegate for StockTableDelegate { self.stocks.len() } - fn col_name(&self, col_ix: usize, _: &App) -> SharedString { - self.columns - .get(col_ix) - .map(|col| col.name.clone()) - .unwrap_or("--".into()) - } - - fn col_width(&self, col_ix: usize, _: &App) -> Pixels { - self.columns - .get(col_ix) - .map(|col| col.width) - .unwrap_or(px(100.)) - } - - fn col_paddings(&self, col_ix: usize, _: &App) -> Option> { - let Some(col) = self.columns.get(col_ix) else { - return None; - }; - - col.paddings - } - - fn col_fixed(&self, col_ix: usize, _: &App) -> Option { - if !self.fixed_cols { - return None; - } - - let Some(col) = self.columns.get(col_ix) else { - return None; - }; - - if col.fixed { - Some(ColFixed::Left) - } else { - None - } - } - - fn col_resizable(&self, col_ix: usize, _: &App) -> bool { - let Some(col) = self.columns.get(col_ix) else { - return false; - }; - - col.resizable - } - - fn col_selectable(&self, _: usize, _: &App) -> bool { - return self.col_selection; + fn column(&self, col_ix: usize, _cx: &App) -> &Column { + &self.columns[col_ix] } fn render_th( @@ -487,7 +407,7 @@ impl TableDelegate for StockTableDelegate { let stock = self.stocks.get(row_ix).unwrap(); let col = self.columns.get(col_ix).unwrap(); - match col.id.as_ref() { + match col.key.as_ref() { "id" => stock.id.to_string().into_any_element(), "market" => div() .map(|this| { @@ -503,7 +423,7 @@ impl TableDelegate for StockTableDelegate { "name" => stock.counter.name.clone().into_any_element(), "price" => self.render_value_cell(&col, stock.price, cx), "change" => self.render_value_cell(&col, stock.change, cx), - "change_percent" => self.render_value_cell(&col, stock.change_percent, cx), + "change_percent" => self.render_percent(&col, stock.change_percent, cx), "volume" => self.render_value_cell(&col, stock.volume, cx), "turnover" => self.render_value_cell(&col, stock.turnover, cx), "market_cap" => self.render_value_cell(&col, stock.market_cap, cx), @@ -514,10 +434,7 @@ impl TableDelegate for StockTableDelegate { .floor() .to_string() .into_any_element(), - "year_change_percent" => (stock.year_change_percent * 100.0) - .floor() - .to_string() - .into_any_element(), + "year_change_percent" => self.render_percent(&col, stock.year_change_percent, cx), "bid" => self.render_value_cell(&col, stock.bid, cx), "bid_volume" => self.render_value_cell(&col, stock.bid_volume, cx), "ask" => self.render_value_cell(&col, stock.ask, cx), @@ -553,20 +470,14 @@ impl TableDelegate for StockTableDelegate { .to_string() .into_any_element(), "pre_market_cap" => stock.pre_market_cap.floor().to_string().into_any_element(), - "pre_market_percent" => (stock.pre_market_percent * 100.0) - .floor() - .to_string() - .into_any_element(), + "pre_market_percent" => self.render_percent(&col, stock.pre_market_percent, cx), "pre_market_change" => stock .pre_market_change .floor() .to_string() .into_any_element(), "post_market_cap" => stock.post_market_cap.floor().to_string().into_any_element(), - "post_market_percent" => (stock.post_market_percent * 100.0) - .floor() - .to_string() - .into_any_element(), + "post_market_percent" => self.render_percent(&col, stock.post_market_percent, cx), "post_market_change" => stock .post_market_change .floor() @@ -584,11 +495,7 @@ impl TableDelegate for StockTableDelegate { } } - fn col_movable(&self, _: usize, _: &App) -> bool { - self.col_order - } - - fn move_col( + fn move_column( &mut self, col_ix: usize, to_ix: usize, @@ -599,37 +506,25 @@ impl TableDelegate for StockTableDelegate { self.columns.insert(to_ix, col); } - fn col_sort(&self, col_ix: usize, _: &App) -> Option { - if !self.col_sort { - return None; - } - - self.columns.get(col_ix).and_then(|c| c.sort) - } - fn perform_sort( &mut self, col_ix: usize, - sort: ColSort, + sort: ColumnSort, _: &mut Window, _: &mut Context>, ) { - if !self.col_sort { - return; - } - if let Some(col) = self.columns.get_mut(col_ix) { - match col.id.as_ref() { + match col.key.as_ref() { "id" => self.stocks.sort_by(|a, b| match sort { - ColSort::Descending => b.id.cmp(&a.id), + ColumnSort::Descending => b.id.cmp(&a.id), _ => a.id.cmp(&b.id), }), "symbol" => self.stocks.sort_by(|a, b| match sort { - ColSort::Descending => b.counter.symbol.cmp(&a.counter.symbol), + ColumnSort::Descending => b.counter.symbol.cmp(&a.counter.symbol), _ => a.id.cmp(&b.id), }), "change" | "change_percent" => self.stocks.sort_by(|a, b| match sort { - ColSort::Descending => b + ColumnSort::Descending => b .change .partial_cmp(&a.change) .unwrap_or(std::cmp::Ordering::Equal), @@ -679,7 +574,7 @@ impl TableDelegate for StockTableDelegate { self.visible_rows = visible_range; } - fn visible_cols_changed( + fn visible_columns_changed( &mut self, visible_range: Range, _: &mut Window, @@ -817,32 +712,35 @@ impl TableStory { fn toggle_col_resize(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.table.update(cx, |table, cx| { - table.delegate_mut().col_resize = *checked; - table.refresh(cx); + table.col_resizable = *checked; cx.notify(); }); } fn toggle_col_order(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.table.update(cx, |table, cx| { - table.delegate_mut().col_order = *checked; - table.refresh(cx); + table.col_movable = *checked; cx.notify(); }); } fn toggle_col_sort(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.table.update(cx, |table, cx| { - table.delegate_mut().col_sort = *checked; - table.refresh(cx); + table.sortable = *checked; + cx.notify(); + }); + } + + fn toggle_col_fixed(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { + self.table.update(cx, |table, cx| { + table.col_fixed = *checked; cx.notify(); }); } fn toggle_col_selection(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.table.update(cx, |table, cx| { - table.delegate_mut().col_selection = *checked; - table.refresh(cx); + table.col_selectable = *checked; cx.notify(); }); } @@ -856,14 +754,6 @@ impl TableStory { }); } - fn toggle_fixed_cols(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { - self.table.update(cx, |table, cx| { - table.delegate_mut().fixed_cols = *checked; - table.refresh(cx); - cx.notify(); - }); - } - fn on_change_size(&mut self, a: &ChangeSize, _: &mut Window, cx: &mut Context) { self.size = a.0; self.table.update(cx, |table, cx| { @@ -885,13 +775,13 @@ impl TableStory { _cx: &mut Context, ) { match event { - TableEvent::ColWidthsChanged(col_widths) => { - println!("Col widths changed: {:?}", col_widths) + TableEvent::ColumnWidthsChanged(col_widths) => { + println!("Column widths changed: {:?}", col_widths) } - TableEvent::SelectCol(ix) => println!("Select col: {}", ix), + TableEvent::SelectColumn(ix) => println!("Select col: {}", ix), TableEvent::DoubleClickedRow(ix) => println!("Double clicked row: {}", ix), TableEvent::SelectRow(ix) => println!("Select row: {}", ix), - TableEvent::MoveCol(origin_idx, target_idx) => { + TableEvent::MoveColumn(origin_idx, target_idx) => { println!("Move col index: {} -> {}", origin_idx, target_idx); } } @@ -900,8 +790,8 @@ impl TableStory { impl Render for TableStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { - let delegate = self.table.read(cx).delegate(); - let loop_selection = self.table.read(cx).loop_selection; + let table = &self.table.read(cx); + let delegate = table.delegate(); let rows_count = delegate.rows_count(cx); let size = self.size; @@ -918,45 +808,45 @@ impl Render for TableStory { .child( Checkbox::new("loop-selection") .label("Loop Selection") - .selected(loop_selection) + .selected(table.loop_selection) .on_click(cx.listener(Self::toggle_loop_selection)), ) .child( Checkbox::new("col-resize") .label("Column Resize") - .selected(delegate.col_resize) + .selected(table.col_resizable) .on_click(cx.listener(Self::toggle_col_resize)), ) .child( Checkbox::new("col-order") .label("Column Order") - .selected(delegate.col_order) + .selected(table.col_movable) .on_click(cx.listener(Self::toggle_col_order)), ) .child( Checkbox::new("col-sort") - .label("Column Sort") - .selected(delegate.col_sort) + .label("Sortable") + .selected(table.sortable) .on_click(cx.listener(Self::toggle_col_sort)), ) .child( Checkbox::new("col-selection") - .label("Column Selection") - .selected(delegate.col_selection) + .label("Column Selectable") + .selected(table.col_selectable) .on_click(cx.listener(Self::toggle_col_selection)), ) + .child( + Checkbox::new("fixed") + .label("Column Fixed") + .selected(table.col_fixed) + .on_click(cx.listener(Self::toggle_col_fixed)), + ) .child( Checkbox::new("stripe") .label("Stripe") .selected(self.stripe) .on_click(cx.listener(Self::toggle_stripe)), ) - .child( - Checkbox::new("fixed-cols") - .label("Fixed Columns") - .selected(delegate.fixed_cols) - .on_click(cx.listener(Self::toggle_fixed_cols)), - ) .child( Checkbox::new("loading") .label("Loading") @@ -1043,7 +933,7 @@ impl Render for TableStory { // .small() // .on_click(cx.listener(|this, _, window, cx| { // this.table.update(cx, |table, cx| { - // table.scroll_to_col(table.delegate().cols_count(cx), cx); + // table.scroll_to_col(table.delegate().columns_count(cx), cx); // }) // })), // ), diff --git a/crates/ui/src/table/column.rs b/crates/ui/src/table/column.rs new file mode 100644 index 00000000..0d86def1 --- /dev/null +++ b/crates/ui/src/table/column.rs @@ -0,0 +1,202 @@ +use gpui::{ + div, prelude::FluentBuilder, px, Bounds, Context, Edges, Empty, EntityId, IntoElement, + ParentElement as _, Pixels, Render, SharedString, Styled as _, TextAlign, Window, +}; + +use crate::ActiveTheme as _; + +/// Represents a column in a table, used for initializing table columns. +#[derive(Debug, Clone)] +pub struct Column { + pub key: SharedString, + pub name: SharedString, + pub align: TextAlign, + pub sort: Option, + pub paddings: Option>, + pub width: Pixels, + pub fixed: Option, + pub resizable: bool, + pub movable: bool, + pub selectable: bool, +} + +impl Default for Column { + fn default() -> Self { + Self { + key: SharedString::new(""), + name: SharedString::new(""), + align: TextAlign::Left, + sort: None, + paddings: None, + width: px(100.), + fixed: None, + resizable: true, + movable: true, + selectable: true, + } + } +} + +impl Column { + /// Create a new column with the given key and name. + pub fn new(key: impl Into, name: impl Into) -> Self { + Self { + key: key.into(), + name: name.into(), + ..Default::default() + } + } + + /// Set the column to be sortable with custom sort function, default is None (not sortable). + /// + /// See also [`Column::sortable`] to enable sorting with default. + pub fn sort(mut self, sort: ColumnSort) -> Self { + self.sort = Some(sort); + self + } + + /// Set whether the column is sortable, default is true. + /// + /// See also [`Column::sort`]. + pub fn sortable(mut self) -> Self { + self.sort = Some(ColumnSort::Default); + self + } + + /// Set whether the column is sort with ascending order. + pub fn ascending(mut self) -> Self { + self.sort = Some(ColumnSort::Ascending); + self + } + + /// Set whether the column is sort with descending order. + pub fn descending(mut self) -> Self { + self.sort = Some(ColumnSort::Descending); + self + } + + /// Set the alignment of the column text, default is left. + /// + /// Only `text_left`, `text_right` is supported. + pub fn text_right(mut self) -> Self { + self.align = TextAlign::Right; + self + } + + /// Set the padding of the column, default is None. + pub fn paddings(mut self, paddings: impl Into>) -> Self { + self.paddings = Some(paddings.into()); + self + } + + pub fn p_0(mut self) -> Self { + self.paddings = Some(Edges::all(px(0.))); + self + } + + /// Set the width of the column, default is 100px. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Set whether the column is fixed, default is false. + pub fn fixed(mut self, fixed: impl Into) -> Self { + self.fixed = Some(fixed.into()); + self + } + + /// Set whether the column is fixed on left side, default is false. + pub fn fixed_left(mut self) -> Self { + self.fixed = Some(ColumnFixed::Left); + self + } + + /// Set whether the column is resizable, default is true. + pub fn resizable(mut self, resizable: bool) -> Self { + self.resizable = resizable; + self + } + + /// Set whether the column is movable, default is true. + pub fn movable(mut self, movable: bool) -> Self { + self.movable = movable; + self + } + + /// Set whether the column is selectable, default is true. + pub fn selectable(mut self, selectable: bool) -> Self { + self.selectable = selectable; + self + } +} + +impl FluentBuilder for Column {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ColumnFixed { + Left, +} + +/// Used to sort the column runtime info in Table internal. +#[derive(Debug, Clone)] +pub(crate) struct ColGroup { + pub(crate) column: Column, + /// This is the runtime width of the column, we may update it when the column is resized. + /// + /// Including the width with next columns by col_span. + pub(crate) width: Pixels, + /// The bounds of the column in the table after it renders. + pub(crate) bounds: Bounds, +} + +impl ColGroup { + pub(crate) fn is_resizable(&self) -> bool { + self.column.resizable + } +} + +#[derive(Clone)] +pub(crate) struct DragColumn { + pub(crate) entity_id: EntityId, + pub(crate) name: SharedString, + pub(crate) width: Pixels, + pub(crate) col_ix: usize, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] +pub enum ColumnSort { + /// No sorting. + #[default] + Default, + /// Sort in ascending order. + Ascending, + /// Sort in descending order. + Descending, +} + +impl Render for DragColumn { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .px_4() + .py_1() + .bg(cx.theme().table_head) + .text_color(cx.theme().muted_foreground) + .opacity(0.9) + .border_1() + .border_color(cx.theme().border) + .shadow_md() + .w(self.width) + .min_w(px(100.)) + .max_w(px(450.)) + .child(self.name.clone()) + } +} + +#[derive(Clone)] +pub(crate) struct ResizeColumn(pub (EntityId, usize)); +impl Render for ResizeColumn { + fn render(&mut self, _window: &mut Window, _: &mut Context) -> impl IntoElement { + Empty + } +} diff --git a/crates/ui/src/table/delegate.rs b/crates/ui/src/table/delegate.rs new file mode 100644 index 00000000..cd0a5e03 --- /dev/null +++ b/crates/ui/src/table/delegate.rs @@ -0,0 +1,170 @@ +use std::ops::Range; + +use gpui::{ + div, App, Context, Div, InteractiveElement as _, IntoElement, ParentElement as _, Stateful, + Styled as _, Window, +}; + +use crate::{ + h_flex, + popup_menu::PopupMenu, + table::{loading::Loading, Column, ColumnSort, Table}, + ActiveTheme as _, Icon, IconName, Size, +}; + +#[allow(unused)] +pub trait TableDelegate: Sized + 'static { + /// Return the number of columns in the table. + fn columns_count(&self, cx: &App) -> usize; + /// Return the number of rows in the table. + fn rows_count(&self, cx: &App) -> usize; + + /// Returns the table column at the given index. + /// + /// This only call on Table prepare or refresh. + fn column(&self, col_ix: usize, cx: &App) -> &Column; + + /// Perform sort on the column at the given index. + fn perform_sort( + &mut self, + col_ix: usize, + sort: ColumnSort, + window: &mut Window, + cx: &mut Context>, + ) { + } + + /// Render the header cell at the given column index, default to the column name. + fn render_th( + &self, + col_ix: usize, + window: &mut Window, + cx: &mut Context>, + ) -> impl IntoElement { + div() + .size_full() + .child(self.column(col_ix, cx).name.clone()) + } + + /// Render the row at the given row and column. + fn render_tr( + &self, + row_ix: usize, + window: &mut Window, + cx: &mut Context>, + ) -> Stateful
{ + h_flex().id(("row", row_ix)) + } + + /// Render the context menu for the row at the given row index. + fn context_menu(&self, row_ix: usize, menu: PopupMenu, window: &Window, cx: &App) -> PopupMenu { + menu + } + + /// Render cell at the given row and column. + fn render_td( + &self, + row_ix: usize, + col_ix: usize, + window: &mut Window, + cx: &mut Context>, + ) -> impl IntoElement; + + /// Move the column at the given `col_ix` to insert before the column at the given `to_ix`. + fn move_column( + &mut self, + col_ix: usize, + to_ix: usize, + window: &mut Window, + cx: &mut Context>, + ) { + } + + /// Return a Element to show when table is empty. + fn render_empty(&self, window: &mut Window, cx: &mut Context>) -> impl IntoElement { + h_flex() + .size_full() + .justify_center() + .text_color(cx.theme().muted_foreground.opacity(0.6)) + .child(Icon::new(IconName::Inbox).size_12()) + .into_any_element() + } + + /// Return true to show the loading view. + fn loading(&self, cx: &App) -> bool { + false + } + + /// Return a Element to show when table is loading, default is built-in Skeleton loading view. + /// + /// The size is the size of the Table. + fn render_loading( + &self, + size: Size, + window: &mut Window, + cx: &mut Context>, + ) -> impl IntoElement { + Loading::new().size(size) + } + + /// Return true to enable load more data when scrolling to the bottom. + /// + /// Default: true + fn can_load_more(&self, cx: &App) -> bool { + true + } + + /// Returns a threshold value (n rows), of course, when scrolling to the bottom, + /// the remaining number of rows triggers `load_more`. + /// This should smaller than the total number of first load rows. + /// + /// Default: 20 rows + fn load_more_threshold(&self) -> usize { + 20 + } + + /// Load more data when the table is scrolled to the bottom. + /// + /// This will performed in a background task. + /// + /// This is always called when the table is near the bottom, + /// so you must check if there is more data to load or lock the loading state. + fn load_more(&mut self, window: &mut Window, cx: &mut Context>) {} + + /// Render the last empty column, default to empty. + fn render_last_empty_col( + &mut self, + window: &mut Window, + cx: &mut Context>, + ) -> impl IntoElement { + h_flex().w_3().h_full().flex_shrink_0() + } + + /// Called when the visible range of the rows changed. + /// + /// NOTE: Make sure this method is fast, because it will be called frequently. + /// + /// This can used to handle some data update, to only update the visible rows. + /// Please ensure that the data is updated in the background task. + fn visible_rows_changed( + &mut self, + visible_range: Range, + window: &mut Window, + cx: &mut Context>, + ) { + } + + /// Called when the visible range of the columns changed. + /// + /// NOTE: Make sure this method is fast, because it will be called frequently. + /// + /// This can used to handle some data update, to only update the visible rows. + /// Please ensure that the data is updated in the background task. + fn visible_columns_changed( + &mut self, + visible_range: Range, + window: &mut Window, + cx: &mut Context>, + ) { + } +} diff --git a/crates/ui/src/table.rs b/crates/ui/src/table/mod.rs similarity index 77% rename from crates/ui/src/table.rs rename to crates/ui/src/table/mod.rs index 34a4a756..5812aa1a 100644 --- a/crates/ui/src/table.rs +++ b/crates/ui/src/table/mod.rs @@ -10,15 +10,19 @@ use crate::{ }; use gpui::{ actions, canvas, div, prelude::FluentBuilder, px, uniform_list, App, AppContext, Axis, Bounds, - Context, Div, DragMoveEvent, Edges, Empty, EntityId, EventEmitter, FocusHandle, Focusable, - InteractiveElement, IntoElement, KeyBinding, ListSizingBehavior, MouseButton, MouseDownEvent, - ParentElement, Pixels, Point, Render, ScrollHandle, ScrollStrategy, ScrollWheelEvent, - SharedString, Stateful, StatefulInteractiveElement as _, Styled, Task, UniformListScrollHandle, - Window, + Context, Div, DragMoveEvent, Edges, EventEmitter, FocusHandle, Focusable, InteractiveElement, + IntoElement, KeyBinding, ListSizingBehavior, MouseButton, MouseDownEvent, ParentElement, + Pixels, Point, Render, ScrollHandle, ScrollStrategy, ScrollWheelEvent, SharedString, + StatefulInteractiveElement as _, Styled, Task, UniformListScrollHandle, Window, }; +mod column; +mod delegate; mod loading; +pub use column::*; +pub use delegate::*; + actions!(table, [SelectPrevColumn, SelectNextColumn]); pub fn init(cx: &mut App) { @@ -32,64 +36,6 @@ pub fn init(cx: &mut App) { ]); } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ColFixed { - Left, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) struct ColGroup { - pub(crate) width: Pixels, - pub(crate) bounds: Bounds, - pub(crate) sort: Option, - pub(crate) fixed: Option, - pub(crate) paddings: Option>, -} - -#[derive(Clone)] -pub(crate) struct DragCol { - pub(crate) entity_id: EntityId, - pub(crate) name: SharedString, - pub(crate) width: Pixels, - pub(crate) col_ix: usize, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum ColSort { - /// No sorting. - Default, - /// Sort in ascending order. - Ascending, - /// Sort in descending order. - Descending, -} - -impl Render for DragCol { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .px_4() - .py_1() - .bg(cx.theme().table_head) - .text_color(cx.theme().muted_foreground) - .opacity(0.9) - .border_1() - .border_color(cx.theme().border) - .shadow_md() - .w(self.width) - .min_w(px(100.)) - .max_w(px(450.)) - .child(self.name.clone()) - } -} - -#[derive(Clone)] -pub struct ResizeCol(pub (EntityId, usize)); -impl Render for ResizeCol { - fn render(&mut self, _window: &mut Window, _: &mut Context) -> impl IntoElement { - Empty - } -} - #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum SelectionState { Column, @@ -102,14 +48,9 @@ pub enum TableEvent { SelectRow(usize), /// Double click on the row. DoubleClickedRow(usize), - SelectCol(usize), - ColWidthsChanged(Vec), - MoveCol(usize, usize), -} - -#[derive(Clone, Copy, Default)] -struct FixedCols { - left: usize, + SelectColumn(usize), + ColumnWidthsChanged(Vec), + MoveColumn(usize, usize), } /// The visible range of the rows and columns. @@ -142,12 +83,23 @@ pub struct Table { fixed_head_cols_bounds: Bounds, col_groups: Vec, - fixed_cols: FixedCols, /// Whether the table can loop selection, default is true. /// /// When the prev/next selection is out of the table bounds, the selection will loop to the other side. pub loop_selection: bool, + /// Whether the table can select column. + pub col_selectable: bool, + /// Whether the table can select row. + pub row_selectable: bool, + /// Whether the table can sort. + pub sortable: bool, + /// Whether the table can resize columns. + pub col_resizable: bool, + /// Whether the table can move columns. + pub col_movable: bool, + /// Enable/disable fixed columns feature. + pub col_fixed: bool, pub vertical_scroll_handle: UniformListScrollHandle, pub vertical_scroll_state: ScrollbarState, @@ -176,203 +128,6 @@ pub struct Table { _load_more_task: Task<()>, } -#[allow(unused)] -pub trait TableDelegate: Sized + 'static { - /// Return the number of columns in the table. - fn cols_count(&self, cx: &App) -> usize; - /// Return the number of rows in the table. - fn rows_count(&self, cx: &App) -> usize; - - /// Returns the name of the column at the given index. - fn col_name(&self, col_ix: usize, cx: &App) -> SharedString; - - /// Returns whether the column at the given index can be resized. Default: true - fn col_resizable(&self, col_ix: usize, cx: &App) -> bool { - true - } - - /// Returns whether the column at the given index can be selected. Default: false - fn col_selectable(&self, col_ix: usize, cx: &App) -> bool { - false - } - - /// Returns the width of the column at the given index. - /// Return None, use auto width. - /// - /// This is only called when the table initializes. - /// - /// Default: 100px - fn col_width(&self, col_ix: usize, cx: &App) -> Pixels { - px(100.) - } - - /// Return the sort state of the column at the given index. - /// - /// This is only called when the table initializes. - fn col_sort(&self, col_ix: usize, cx: &App) -> Option { - None - } - - /// Return the fixed side of the column at the given index. - fn col_fixed(&self, col_ix: usize, cx: &App) -> Option { - None - } - - /// Return the padding of the column at the given index to override the default padding. - /// - /// Return None, use the default padding. - fn col_paddings(&self, col_ix: usize, cx: &App) -> Option> { - None - } - - /// Return true to enable column order change. - fn col_movable(&self, col_ix: usize, cx: &App) -> bool { - false - } - - /// Perform sort on the column at the given index. - fn perform_sort( - &mut self, - col_ix: usize, - sort: ColSort, - window: &mut Window, - cx: &mut Context>, - ) { - } - - /// Render the header cell at the given column index, default to the column name. - fn render_th( - &self, - col_ix: usize, - window: &mut Window, - cx: &mut Context>, - ) -> impl IntoElement { - div().size_full().child(self.col_name(col_ix, cx)) - } - - /// Render the row at the given row and column. - fn render_tr( - &self, - row_ix: usize, - window: &mut Window, - cx: &mut Context>, - ) -> Stateful
{ - h_flex().id(("table-row", row_ix)) - } - - /// Render the context menu for the row at the given row index. - fn context_menu(&self, row_ix: usize, menu: PopupMenu, window: &Window, cx: &App) -> PopupMenu { - menu - } - - /// Render cell at the given row and column. - fn render_td( - &self, - row_ix: usize, - col_ix: usize, - window: &mut Window, - cx: &mut Context>, - ) -> impl IntoElement; - - /// 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, - window: &mut Window, - cx: &mut Context>, - ) { - } - - /// Return a Element to show when table is empty. - fn render_empty(&self, window: &mut Window, cx: &mut Context>) -> impl IntoElement { - h_flex() - .size_full() - .justify_center() - .text_color(cx.theme().muted_foreground.opacity(0.6)) - .child(Icon::new(IconName::Inbox).size_12()) - .into_any_element() - } - - /// Return true to show the loading view. - fn loading(&self, cx: &App) -> bool { - false - } - - /// Return a Element to show when table is loading, default is built-in Skeleton loading view. - /// - /// The size is the size of the Table. - fn render_loading( - &self, - size: Size, - window: &mut Window, - cx: &mut Context>, - ) -> impl IntoElement { - loading::Loading::new().size(size) - } - - /// Return true to enable load more data when scrolling to the bottom. - /// - /// Default: true - fn can_load_more(&self, cx: &App) -> bool { - true - } - - /// Returns a threshold value (n rows), of course, when scrolling to the bottom, - /// the remaining number of rows triggers `load_more`. - /// This should smaller than the total number of first load rows. - /// - /// Default: 20 rows - fn load_more_threshold(&self) -> usize { - 20 - } - - /// Load more data when the table is scrolled to the bottom. - /// - /// This will performed in a background task. - /// - /// This is always called when the table is near the bottom, - /// so you must check if there is more data to load or lock the loading state. - fn load_more(&mut self, window: &mut Window, cx: &mut Context>) {} - - /// Render the last empty column, default to empty. - fn render_last_empty_col( - &mut self, - window: &mut Window, - cx: &mut Context>, - ) -> impl IntoElement { - h_flex().w_3().h_full().flex_shrink_0() - } - - /// Called when the visible range of the rows changed. - /// - /// NOTE: Make sure this method is fast, because it will be called frequently. - /// - /// This can used to handle some data update, to only update the visible rows. - /// Please ensure that the data is updated in the background task. - fn visible_rows_changed( - &mut self, - visible_range: Range, - window: &mut Window, - cx: &mut Context>, - ) { - } - - /// Called when the visible range of the columns changed. - /// - /// NOTE: Make sure this method is fast, because it will be called frequently. - /// - /// This can used to handle some data update, to only update the visible rows. - /// Please ensure that the data is updated in the background task. - fn visible_cols_changed( - &mut self, - visible_range: Range, - window: &mut Window, - cx: &mut Context>, - ) { - } -} - impl Table where D: TableDelegate, @@ -382,7 +137,6 @@ where focus_handle: cx.focus_handle(), delegate, col_groups: Vec::new(), - fixed_cols: FixedCols::default(), horizontal_scroll_handle: ScrollHandle::new(), vertical_scroll_handle: UniformListScrollHandle::new(), vertical_scroll_state: ScrollbarState::default(), @@ -400,6 +154,12 @@ where scrollbar_visible: Edges::all(true), visible_range: VisibleRangeState::default(), loop_selection: true, + col_selectable: true, + row_selectable: true, + sortable: true, + col_movable: true, + col_resizable: true, + col_fixed: true, _load_more_task: Task::ready(()), _measure: Vec::new(), }; @@ -439,6 +199,36 @@ where self } + /// Set to enable/disable column movable, default to true. + pub fn col_movable(mut self, col_movable: bool) -> Self { + self.col_movable = col_movable; + self + } + + /// Set to enable/disable column resizable, default to true. + pub fn col_resizable(mut self, col_resizable: bool) -> Self { + self.col_resizable = col_resizable; + self + } + + /// Set to enable/disable column sortable, default true + pub fn sortable(mut self, sortable: bool) -> Self { + self.sortable = sortable; + self + } + + /// Set to enable/disable row selectable, default true + pub fn row_selectable(mut self, row_selectable: bool) -> Self { + self.row_selectable = row_selectable; + self + } + + /// Set to enable/disable column selectable, default true + pub fn col_selectable(mut self, col_selectable: bool) -> Self { + self.col_selectable = col_selectable; + self + } + /// Set the size to the table. pub fn set_size(&mut self, size: Size, cx: &mut Context) { self.size = size; @@ -466,20 +256,16 @@ where } fn prepare_col_groups(&mut self, cx: &mut Context) { - self.col_groups = (0..self.delegate.cols_count(cx)) - .map(|col_ix| ColGroup { - width: self.delegate.col_width(col_ix, cx), - paddings: self.delegate.col_paddings(col_ix, cx), - bounds: Bounds::default(), - sort: self.delegate.col_sort(col_ix, cx), - fixed: self.delegate.col_fixed(col_ix, cx), + self.col_groups = (0..self.delegate.columns_count(cx)) + .map(|col_ix| { + let column = self.delegate().column(col_ix, cx); + ColGroup { + width: column.width, + bounds: Bounds::default(), + column: column.clone(), + } }) .collect(); - self.fixed_cols.left = self - .col_groups - .iter() - .filter(|col| col.fixed == Some(ColFixed::Left)) - .count(); cx.notify(); } @@ -530,7 +316,7 @@ where // self.horizontal_scroll_handle.scroll_to_item(col_ix); // } } - cx.emit(TableEvent::SelectCol(col_ix)); + cx.emit(TableEvent::SelectColumn(col_ix)); cx.notify(); } @@ -566,7 +352,15 @@ where } fn on_col_head_click(&mut self, col_ix: usize, _: &mut Window, cx: &mut Context) { - if !self.delegate.col_selectable(col_ix, cx) { + if !self.col_selectable { + return; + } + + let Some(col_group) = self.col_groups.get(col_ix) else { + return; + }; + + if !col_group.column.selectable { return; } @@ -623,12 +417,12 @@ where cx: &mut Context, ) { let mut selected_col = self.selected_col.unwrap_or(0); - let cols_count = self.delegate.cols_count(cx); + let columns_count = self.delegate.columns_count(cx); if selected_col > 0 { selected_col = selected_col.saturating_sub(1); } else { if self.loop_selection { - selected_col = cols_count.saturating_sub(1); + selected_col = columns_count.saturating_sub(1); } } self.set_selected_col(selected_col, cx); @@ -641,7 +435,7 @@ where cx: &mut Context, ) { let mut selected_col = self.selected_col.unwrap_or(0); - if selected_col < self.delegate.cols_count(cx).saturating_sub(1) { + if selected_col < self.delegate.columns_count(cx).saturating_sub(1) { selected_col += 1; } else { if self.loop_selection { @@ -653,7 +447,11 @@ where } /// Scroll table when mouse position is near the edge of the table bounds. - fn scroll_table_by_col_resizing(&mut self, mouse_position: Point, col_group: ColGroup) { + fn scroll_table_by_col_resizing( + &mut self, + mouse_position: Point, + col_group: &ColGroup, + ) { // Do nothing if pos out of the table bounds right for avoid scroll to the right. if mouse_position.x > self.bounds.right() { return; @@ -678,15 +476,22 @@ where /// 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, _: &mut Window, cx: &mut Context) { + if !self.col_resizable { + return; + } + const MIN_WIDTH: Pixels = px(10.0); const MAX_WIDTH: Pixels = px(1200.0); + let Some(col_group) = self.col_groups.get_mut(ix) else { + return; + }; - if !self.delegate.col_resizable(ix, cx) { + if !col_group.is_resizable() { return; } let size = size.floor(); - let old_width = self.col_groups[ix].width; + let old_width = col_group.width; let new_width = size; if new_width < MIN_WIDTH { return; @@ -696,35 +501,34 @@ where if changed_width > px(-1.0) && changed_width < px(1.0) { return; } - self.col_groups[ix].width = 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(); - // let next_width = (next_width - changed_width).max(MIN_WIDTH); - // self.col_groups[ix + 1].width = Some(next_width); + col_group.width = new_width.min(MAX_WIDTH); cx.notify(); } fn perform_sort(&mut self, col_ix: usize, window: &mut Window, cx: &mut Context) { - let sort = self.col_groups.get(col_ix).and_then(|g| g.sort); + if !self.sortable { + return; + } + + let sort = self.col_groups.get(col_ix).and_then(|g| g.column.sort); if sort.is_none() { return; } let sort = sort.unwrap(); let sort = match sort { - ColSort::Ascending => ColSort::Default, - ColSort::Descending => ColSort::Ascending, - ColSort::Default => ColSort::Descending, + ColumnSort::Ascending => ColumnSort::Default, + ColumnSort::Descending => ColumnSort::Ascending, + ColumnSort::Default => ColumnSort::Descending, }; for (ix, col_group) in self.col_groups.iter_mut().enumerate() { if ix == col_ix { - col_group.sort = Some(sort); + col_group.column.sort = Some(sort); } else { - if col_group.sort.is_some() { - col_group.sort = Some(ColSort::Default); + if col_group.column.sort.is_some() { + col_group.column.sort = Some(ColumnSort::Default); } } } @@ -734,7 +538,7 @@ where cx.notify(); } - fn move_col( + fn move_column( &mut self, col_ix: usize, to_ix: usize, @@ -745,11 +549,11 @@ where return; } - self.delegate.move_col(col_ix, to_ix, window, cx); + self.delegate.move_column(col_ix, to_ix, window, cx); let col_group = self.col_groups.remove(col_ix); self.col_groups.insert(to_ix, col_group); - cx.emit(TableEvent::MoveCol(col_ix, to_ix)); + cx.emit(TableEvent::MoveColumn(col_ix, to_ix)); cx.notify(); } @@ -801,19 +605,18 @@ where return; } self.delegate_mut() - .visible_cols_changed(visible_range.clone(), window, cx); + .visible_columns_changed(visible_range.clone(), window, cx); self.visible_range.cols = visible_range; } } - #[inline] fn render_cell(&self, col_ix: usize, _window: &mut Window, _cx: &mut Context) -> Div { let Some(col_group) = self.col_groups.get(col_ix) else { return div(); }; let col_width = col_group.width; - let col_padding = col_group.paddings; + let col_padding = col_group.column.paddings; div() .w(col_width) @@ -835,8 +638,14 @@ where /// Show Column selection style, when the column is selected and the selection state is Column. fn render_col_wrap(&self, col_ix: usize, _: &mut Window, cx: &mut Context) -> Div { let el = h_flex().h_full(); + let selectable = self.col_selectable + && self + .col_groups + .get(col_ix) + .map(|col_group| col_group.column.selectable) + .unwrap_or(false); - if self.delegate().col_selectable(col_ix, cx) + if selectable && self.selected_col == Some(col_ix) && self.selection_state == SelectionState::Column { @@ -899,7 +708,13 @@ where ) -> impl IntoElement { const HANDLE_SIZE: Pixels = px(2.); - if !self.delegate.col_resizable(ix, cx) { + let resizable = self.col_resizable + && self + .col_groups + .get(ix) + .map(|col| col.is_resizable()) + .unwrap_or(false); + if !resizable { return div().into_any_element(); } @@ -924,23 +739,27 @@ where .w(px(1.)), ) .on_drag_move( - cx.listener(move |view, e: &DragMoveEvent, window, cx| { + cx.listener(move |view, e: &DragMoveEvent, window, cx| { match e.drag(cx) { - ResizeCol((entity_id, ix)) => { + ResizeColumn((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 = col_group.bounds.size.width; - } + // TODO: Consider to remove this, this may not need now. + // for (_, col_group) in view.col_groups.iter_mut().enumerate() { + // col_group.width = 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"); + let col_group = view + .col_groups + .get(ix) + .expect("BUG: invalid col index") + .clone(); view.resize_cols( ix, @@ -950,12 +769,12 @@ where ); // scroll the table if the drag is near the edge - view.scroll_table_by_col_resizing(e.event.position, col_group); + view.scroll_table_by_col_resizing(e.event.position, &col_group); } }; }), ) - .on_drag(ResizeCol((cx.entity_id(), ix)), |drag, _, _, cx| { + .on_drag(ResizeColumn((cx.entity_id(), ix)), |drag, _, _, cx| { cx.stop_propagation(); cx.new(|_| drag.clone()) }) @@ -969,7 +788,7 @@ where view.resizing_col = None; let new_widths = view.col_groups.iter().map(|g| g.width).collect(); - cx.emit(TableEvent::ColWidthsChanged(new_widths)); + cx.emit(TableEvent::ColumnWidthsChanged(new_widths)); cx.notify(); }), ) @@ -983,14 +802,18 @@ where _: &mut Window, cx: &mut Context, ) -> Option { - let Some(sort) = col_group.sort else { + if !self.sortable { + return None; + } + + let Some(sort) = col_group.column.sort else { return None; }; let (icon, is_on) = match sort { - ColSort::Ascending => (IconName::SortAscending, true), - ColSort::Descending => (IconName::SortDescending, true), - ColSort::Default => (IconName::ChevronsUpDown, false), + ColumnSort::Ascending => (IconName::SortAscending, true), + ColumnSort::Descending => (IconName::SortDescending, true), + ColumnSort::Default => (IconName::ChevronsUpDown, false), }; Some( @@ -1027,9 +850,10 @@ where ) -> impl IntoElement { let entity_id = cx.entity_id(); let col_group = self.col_groups.get(col_ix).expect("BUG: invalid col index"); - let movable = self.delegate.col_movable(col_ix, cx); - let paddings = self.delegate.col_paddings(col_ix, cx); - let name = self.delegate.col_name(col_ix, cx); + + let movable = self.col_movable && col_group.column.movable; + let paddings = col_group.column.paddings; + let name = col_group.column.name.clone(); h_flex() .child( @@ -1057,7 +881,7 @@ where ) .when(movable, |this| { this.on_drag( - DragCol { + DragColumn { entity_id, col_ix, name, @@ -1068,20 +892,20 @@ where cx.new(|_| drag.clone()) }, ) - .drag_over::(|this, _, _, cx| { + .drag_over::(|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, window, cx| { + move |table, drag: &DragColumn, window, 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, window, cx); + table.move_column(drag.col_ix, col_ix, window, cx); }, )) }), @@ -1104,7 +928,7 @@ where fn render_table_head( &mut self, - left_cols_count: usize, + left_columns_count: usize, window: &mut Window, cx: &mut Context, ) -> impl IntoElement { @@ -1112,7 +936,7 @@ where let horizontal_scroll_handle = self.horizontal_scroll_handle.clone(); // Reset fixed head columns bounds, if no fixed columns are present - if left_cols_count == 0 { + if left_columns_count == 0 { self.fixed_head_cols_bounds = Bounds::default(); } @@ -1123,7 +947,7 @@ where .border_b_1() .border_color(cx.theme().border) .text_color(cx.theme().table_head_foreground) - .when(left_cols_count > 0, |this| { + .when(left_columns_count > 0, |this| { let view = view.clone(); // Render left fixed columns this.child( @@ -1134,7 +958,7 @@ where .children( self.col_groups .iter() - .filter(|col| col.fixed == Some(ColFixed::Left)) + .filter(|col| col.column.fixed == Some(ColumnFixed::Left)) .enumerate() .map(|(col_ix, _)| self.render_th(col_ix, window, cx)), ) @@ -1177,10 +1001,10 @@ where .children( self.col_groups .iter() - .filter(|col| col.fixed == None) + .filter(|col| col.column.fixed == None) .enumerate() .map(|(col_ix, _)| { - self.render_th(left_cols_count + col_ix, window, cx) + self.render_th(left_columns_count + col_ix, window, cx) }), ) .child(self.delegate.render_last_empty_col(window, cx)), @@ -1193,9 +1017,9 @@ where &mut self, row_ix: usize, rows_count: usize, - left_cols_count: usize, + left_columns_count: usize, col_sizes: Rc>>, - cols_count: usize, + columns_count: usize, window: &mut Window, cx: &mut Context, ) -> impl IntoElement { @@ -1223,16 +1047,16 @@ where this.bg(cx.theme().table_hover) } }) - .when(left_cols_count > 0, |this| { + .when(left_columns_count > 0, |this| { // Left fixed columns this.child( h_flex() .relative() .h_full() .children({ - let mut items = Vec::with_capacity(left_cols_count); + let mut items = Vec::with_capacity(left_columns_count); - (0..left_cols_count).for_each(|col_ix| { + (0..left_columns_count).for_each(|col_ix| { items.push(self.render_col_wrap(col_ix, window, cx).child( self.render_cell(col_ix, window, cx).child( self.measure_render_td(row_ix, col_ix, window, cx), @@ -1282,7 +1106,7 @@ where ); visible_range.for_each(|col_ix| { - let col_ix = col_ix + left_cols_count; + let col_ix = col_ix + left_columns_count; let el = table.render_col_wrap(col_ix, window, cx).child( table.render_cell(col_ix, window, cx).child( @@ -1357,7 +1181,7 @@ where .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| { + .children((0..columns_count).map(|col_ix| { h_flex() .left(horizontal_scroll_handle.offset().x) .child(self.render_cell(col_ix, window, cx)) @@ -1467,8 +1291,12 @@ where let view = cx.entity().clone(); let vertical_scroll_handle = self.vertical_scroll_handle.clone(); let horizontal_scroll_handle = self.horizontal_scroll_handle.clone(); - let cols_count: usize = self.delegate.cols_count(cx); - let left_cols_count = self.fixed_cols.left; + let columns_count: usize = self.delegate.columns_count(cx); + let left_columns_count = self + .col_groups + .iter() + .filter(|col| self.col_fixed && col.column.fixed == Some(ColumnFixed::Left)) + .count(); let rows_count = self.delegate.rows_count(cx); let loading = self.delegate.loading(cx); let extra_rows_needed = self.calculate_extra_rows_needed(rows_count); @@ -1484,7 +1312,7 @@ where .on_action(cx.listener(Self::action_select_prev_col)) .size_full() .overflow_hidden() - .child(self.render_table_head(left_cols_count, window, cx)) + .child(self.render_table_head(left_columns_count, window, cx)) .context_menu({ let view = view.clone(); move |this, window: &mut Window, cx: &mut Context| { @@ -1518,7 +1346,7 @@ where table .col_groups .iter() - .skip(left_cols_count) + .skip(left_columns_count) .map(|col| col.bounds.size) .collect(), ); @@ -1556,9 +1384,9 @@ where items.push(table.render_table_row( row_ix, rows_count, - left_cols_count, + left_columns_count, col_sizes.clone(), - cols_count, + columns_count, window, cx, )); @@ -1608,17 +1436,19 @@ where move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds), |_, _, _, _| {}, )) - .child( - div() - .absolute() - .top_0() - .size_full() - .when(self.scrollbar_visible.bottom, |this| { - this.child(self.render_horizontal_scrollbar(window, cx)) - }) - .when(self.scrollbar_visible.right && rows_count > 0, |this| { - this.children(self.render_vertical_scrollbar(window, cx)) - }), - ) + .when(!window.is_inspector_picking(cx), |this| { + this.child( + div() + .absolute() + .top_0() + .size_full() + .when(self.scrollbar_visible.bottom, |this| { + this.child(self.render_horizontal_scrollbar(window, cx)) + }) + .when(self.scrollbar_visible.right && rows_count > 0, |this| { + this.children(self.render_vertical_scrollbar(window, cx)) + }), + ) + }) } } diff --git a/crates/ui/src/text/element.rs b/crates/ui/src/text/element.rs index 4bd5d2a5..b45f89ec 100644 --- a/crates/ui/src/text/element.rs +++ b/crates/ui/src/text/element.rs @@ -100,30 +100,30 @@ impl From for Paragraph { #[derive(Debug, Default, Clone, PartialEq)] pub struct Table { pub children: Vec, - pub column_aligns: Vec, + pub column_aligns: Vec, } impl Table { - pub(crate) fn column_align(&self, index: usize) -> TableColumnAlign { + pub(crate) fn column_align(&self, index: usize) -> ColumnumnAlign { self.column_aligns.get(index).copied().unwrap_or_default() } } #[derive(Debug, Default, Copy, Clone, PartialEq)] -pub enum TableColumnAlign { +pub enum ColumnumnAlign { #[default] Left, Center, Right, } -impl From for TableColumnAlign { +impl From for ColumnumnAlign { fn from(value: mdast::AlignKind) -> Self { match value { - mdast::AlignKind::None => TableColumnAlign::Left, - mdast::AlignKind::Left => TableColumnAlign::Left, - mdast::AlignKind::Center => TableColumnAlign::Center, - mdast::AlignKind::Right => TableColumnAlign::Right, + mdast::AlignKind::None => ColumnumnAlign::Left, + mdast::AlignKind::Left => ColumnumnAlign::Left, + mdast::AlignKind::Center => ColumnumnAlign::Center, + mdast::AlignKind::Right => ColumnumnAlign::Right, } } } @@ -571,10 +571,10 @@ impl Node { div() .id("cell") .flex() - .when(align == TableColumnAlign::Center, |this| { + .when(align == ColumnumnAlign::Center, |this| { this.justify_center() }) - .when(align == TableColumnAlign::Right, |this| { + .when(align == ColumnumnAlign::Right, |this| { this.justify_end() }) .w(Length::Definite(relative(len as f32))) @@ -863,9 +863,9 @@ impl Node { .iter() .map(|align| { match align { - TableColumnAlign::Left => ":--", - TableColumnAlign::Center => ":-:", - TableColumnAlign::Right => "--:", + ColumnumnAlign::Left => ":--", + ColumnumnAlign::Center => ":-:", + ColumnumnAlign::Right => "--:", } .to_string() })