diff --git a/crates/story/src/drawer_story.rs b/crates/story/src/drawer_story.rs index 9eb2fc67..fb174c9a 100644 --- a/crates/story/src/drawer_story.rs +++ b/crates/story/src/drawer_story.rs @@ -15,7 +15,7 @@ use gpui_component::{ date_picker::{DatePicker, DatePickerState}, h_flex, input::{Input, InputState}, - list::{List, ListDelegate, ListItem}, + list::{ListDelegate, ListItem, ListState}, v_flex, webview::WebView, wry, @@ -43,7 +43,7 @@ impl ListDelegate for ListItemDeletegate { &mut self, query: &str, _: &mut Window, - cx: &mut Context>, + cx: &mut Context>, ) -> Task<()> { let query = query.to_string(); cx.spawn(async move |this, cx| { @@ -65,12 +65,7 @@ impl ListDelegate for ListItemDeletegate { }) } - fn render_item( - &self, - ix: IndexPath, - _: &mut Window, - _: &mut Context>, - ) -> Option { + fn render_item(&self, ix: IndexPath, _: &mut Window, _: &mut App) -> Option { let confirmed = Some(ix.row) == self.confirmed_index; if let Some(item) = self.matches.get(ix.row) { @@ -102,7 +97,7 @@ impl ListDelegate for ListItemDeletegate { } } - fn render_empty(&self, _: &mut Window, cx: &mut Context>) -> impl IntoElement { + fn render_empty(&self, _: &mut Window, cx: &mut App) -> impl IntoElement { v_flex() .size_full() .child( @@ -118,13 +113,13 @@ impl ListDelegate for ListItemDeletegate { .text_color(cx.theme().muted_foreground) } - fn cancel(&mut self, window: &mut Window, cx: &mut Context>) { + fn cancel(&mut self, window: &mut Window, cx: &mut Context>) { _ = self.story.update(cx, |this, cx| { this.close_drawer(window, cx); }); } - fn confirm(&mut self, _secondary: bool, _: &mut Window, cx: &mut Context>) { + fn confirm(&mut self, _secondary: bool, _: &mut Window, cx: &mut Context>) { _ = self.story.update(cx, |this, _| { self.confirmed_index = self.selected_index; if let Some(ix) = self.confirmed_index { @@ -139,7 +134,7 @@ impl ListDelegate for ListItemDeletegate { &mut self, ix: Option, _: &mut Window, - cx: &mut Context>, + cx: &mut Context>, ) { self.selected_index = ix.map(|ix| ix.row); @@ -153,7 +148,7 @@ pub struct DrawerStory { focus_handle: FocusHandle, drawer_placement: Option, selected_value: Option, - list: Entity>, + list: Entity>, input1: Entity, input2: Entity, date: Entity, @@ -248,7 +243,7 @@ impl DrawerStory { matches: items.clone(), }; let list = cx.new(|cx| { - let mut list = List::new(delegate, window, cx); + let mut list = ListState::new(delegate, window, cx); list.focus(window, cx); if let Some(query_input) = list.query_input() { query_input.update(cx, |input, cx| { diff --git a/crates/story/src/list_story.rs b/crates/story/src/list_story.rs index 2ff2c190..0cb0dd8e 100644 --- a/crates/story/src/list_story.rs +++ b/crates/story/src/list_story.rs @@ -2,18 +2,19 @@ use std::{rc::Rc, time::Duration}; use fake::Fake; use gpui::{ - actions, div, prelude::FluentBuilder as _, px, App, AppContext, Context, Edges, ElementId, - Entity, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, - RenderOnce, ScrollStrategy, SharedString, Styled, Subscription, Task, Timer, Window, + App, AppContext, Context, Edges, ElementId, Entity, FocusHandle, Focusable, InteractiveElement, + IntoElement, ParentElement, Render, RenderOnce, ScrollStrategy, SharedString, Styled, + Subscription, Task, Timer, Window, actions, div, prelude::FluentBuilder as _, px, }; use gpui_component::{ + ActiveTheme, Icon, IconName, IndexPath, Selectable, Sizable, button::Button, checkbox::Checkbox, h_flex, label::Label, - list::{List, ListDelegate, ListEvent, ListItem}, - v_flex, ActiveTheme, Icon, IconName, IndexPath, Selectable, Sizable, + list::{List, ListDelegate, ListEvent, ListItem, ListState}, + v_flex, }; actions!(list_story, [SelectedCompany]); @@ -221,13 +222,13 @@ impl ListDelegate for CompanyListDelegate { &mut self, query: &str, _: &mut Window, - _: &mut Context>, + _: &mut Context>, ) -> Task<()> { self.prepare(query.to_owned()); Task::ready(()) } - fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { + fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { println!("Confirmed with secondary: {}", secondary); window.dispatch_action(Box::new(SelectedCompany), cx); } @@ -236,7 +237,7 @@ impl ListDelegate for CompanyListDelegate { &mut self, ix: Option, _: &mut Window, - cx: &mut Context>, + cx: &mut Context>, ) { self.selected_index = ix; cx.notify(); @@ -246,7 +247,7 @@ impl ListDelegate for CompanyListDelegate { &self, section: usize, _: &mut Window, - cx: &mut Context>, + cx: &mut App, ) -> Option { let Some(industry) = self.industries.get(section) else { return None; @@ -268,7 +269,7 @@ impl ListDelegate for CompanyListDelegate { &self, section: usize, _: &mut Window, - cx: &mut Context>, + cx: &mut App, ) -> Option { let Some(_) = self.industries.get(section) else { return None; @@ -288,12 +289,7 @@ impl ListDelegate for CompanyListDelegate { ) } - fn render_item( - &self, - ix: IndexPath, - _: &mut Window, - _: &mut Context>, - ) -> Option { + fn render_item(&self, ix: IndexPath, _: &mut Window, _: &mut App) -> Option { let selected = Some(ix) == self.selected_index || Some(ix) == self.confirmed_index; if let Some(company) = self.matched_companies[ix.section].get(ix.row) { return Some(CompanyListItem::new(ix, company.clone(), ix, selected)); @@ -314,7 +310,7 @@ impl ListDelegate for CompanyListDelegate { 150 } - fn load_more(&mut self, window: &mut Window, cx: &mut Context>) { + fn load_more(&mut self, window: &mut Window, cx: &mut Context>) { // TODO: The load more here will broken the scroll position, // because the extends will creates some new industries to make some new sections. cx.spawn_in(window, async move |view, window| { @@ -334,7 +330,7 @@ impl ListDelegate for CompanyListDelegate { pub struct ListStory { focus_handle: FocusHandle, - company_list: Entity>, + company_list: Entity>, selected_company: Option>, _subscriptions: Vec, } @@ -371,8 +367,7 @@ impl ListStory { }; delegate.extend_more(100); - let company_list = - cx.new(|cx| List::new(delegate, window, cx).paddings(Edges::all(px(8.)))); + let company_list = cx.new(|cx| ListState::new(delegate, window, cx)); let _subscriptions = vec![ @@ -546,7 +541,7 @@ impl Render for ListStory { .border_1() .border_color(cx.theme().border) .rounded(cx.theme().radius) - .child(self.company_list.clone()), + .child(List::new(&self.company_list).paddings(Edges::all(px(8.)))), ) } } diff --git a/crates/story/src/table_story.rs b/crates/story/src/table_story.rs index e9d1425e..77d5d406 100644 --- a/crates/story/src/table_story.rs +++ b/crates/story/src/table_story.rs @@ -6,8 +6,8 @@ use std::{ use fake::Fake; use gpui::{ - Action, AnyElement, App, AppContext, ClickEvent, Context, Entity, Focusable, - InteractiveElement, IntoElement, ParentElement, Render, SharedString, + Action, AnyElement, App, AppContext, ClickEvent, Context, Div, Entity, Focusable, + InteractiveElement, IntoElement, ParentElement, Render, SharedString, Stateful, StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, Timer, Window, div, prelude::FluentBuilder as _, }; @@ -20,7 +20,7 @@ use gpui_component::{ input::{Input, InputEvent, InputState}, label::Label, menu::{DropdownMenu, PopupMenu}, - table::{Column, ColumnFixed, ColumnSort, Table, TableDelegate, TableEvent}, + table::{Column, ColumnFixed, ColumnSort, Table, TableDelegate, TableEvent, TableState}, v_flex, }; use serde::{Deserialize, Serialize}; @@ -274,7 +274,7 @@ impl StockTableDelegate { self.full_loading = false; } - fn render_percent(&self, col: &Column, val: f64, cx: &mut Context>) -> AnyElement { + fn render_percent(&self, col: &Column, val: f64, cx: &mut App) -> AnyElement { let right_num = ((val - val.floor()) * 1000.).floor() as i32; div() @@ -298,12 +298,7 @@ impl StockTableDelegate { .into_any_element() } - fn render_value_cell( - &self, - col: &Column, - val: f64, - cx: &mut Context>, - ) -> AnyElement { + fn render_value_cell(&self, col: &Column, val: f64, cx: &mut App) -> AnyElement { let this = div() .h_full() .table_cell_size(self.size) @@ -342,12 +337,7 @@ impl TableDelegate for StockTableDelegate { &self.columns[col_ix] } - fn render_th( - &self, - col_ix: usize, - _: &mut Window, - _: &mut Context>, - ) -> impl IntoElement { + fn render_th(&self, col_ix: usize, _: &mut Window, _: &mut App) -> impl IntoElement { let col = self.columns.get(col_ix).unwrap(); div() @@ -378,20 +368,13 @@ impl TableDelegate for StockTableDelegate { .menu("Size XSmall", Box::new(ChangeSize(Size::XSmall))) } - fn render_tr( - &self, - row_ix: usize, - _: &mut Window, - cx: &mut Context>, - ) -> gpui::Stateful { - div() - .id(row_ix) - .on_click(cx.listener(|_, ev: &ClickEvent, _, _| { - println!( - "You have clicked row with secondary: {}", - ev.modifiers().secondary() - ) - })) + fn render_tr(&self, row_ix: usize, _: &mut Window, _: &mut App) -> Stateful
{ + div().id(row_ix).on_click(|ev: &ClickEvent, _, _| { + println!( + "You have clicked row with secondary: {}", + ev.modifiers().secondary() + ) + }) } /// NOTE: Performance metrics @@ -407,7 +390,7 @@ impl TableDelegate for StockTableDelegate { row_ix: usize, col_ix: usize, _: &mut Window, - cx: &mut Context>, + cx: &mut App, ) -> impl IntoElement { let stock = self.stocks.get(row_ix).unwrap(); let col = self.columns.get(col_ix).unwrap(); @@ -505,7 +488,7 @@ impl TableDelegate for StockTableDelegate { col_ix: usize, to_ix: usize, _: &mut Window, - _: &mut Context>, + _: &mut Context>, ) { let col = self.columns.remove(col_ix); self.columns.insert(to_ix, col); @@ -516,7 +499,7 @@ impl TableDelegate for StockTableDelegate { col_ix: usize, sort: ColumnSort, _: &mut Window, - _: &mut Context>, + _: &mut Context>, ) { if let Some(col) = self.columns.get_mut(col_ix) { match col.key.as_ref() { @@ -552,7 +535,7 @@ impl TableDelegate for StockTableDelegate { 150 } - fn load_more(&mut self, _: &mut Window, cx: &mut Context>) { + fn load_more(&mut self, _: &mut Window, cx: &mut Context>) { self.loading = true; self._load_task = cx.spawn(async move |view, cx| { @@ -573,7 +556,7 @@ impl TableDelegate for StockTableDelegate { &mut self, visible_range: Range, _: &mut Window, - _: &mut Context>, + _: &mut Context>, ) { self.visible_rows = visible_range; } @@ -582,14 +565,14 @@ impl TableDelegate for StockTableDelegate { &mut self, visible_range: Range, _: &mut Window, - _: &mut Context>, + _: &mut Context>, ) { self.visible_cols = visible_range; } } pub struct TableStory { - table: Entity>, + table: Entity>, num_stocks_input: Entity, stripe: bool, refresh_data: bool, @@ -639,7 +622,7 @@ impl TableStory { }); let delegate = StockTableDelegate::new(5000); - let table = cx.new(|cx| Table::new(delegate, window, cx)); + let table = cx.new(|cx| TableState::new(delegate, window, cx)); let _subscriptions = vec![ cx.subscribe_in(&table, window, Self::on_table_event), @@ -755,19 +738,12 @@ impl TableStory { fn toggle_stripe(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.stripe = *checked; - let stripe = self.stripe; - self.table.update(cx, |table, cx| { - table.set_stripe(stripe, cx); - cx.notify(); - }); + 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| { - table.set_size(a.0, cx); - table.delegate_mut().size = a.0; - }); + cx.notify(); } fn toggle_refresh_data(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { @@ -777,7 +753,7 @@ impl TableStory { fn on_table_event( &mut self, - _: &Entity>, + _: &Entity>, event: &TableEvent, _window: &mut Window, _cx: &mut Context, @@ -983,6 +959,10 @@ impl Render for TableStory { ), ), ) - .child(self.table.clone()) + .child( + Table::new(&self.table) + .with_size(self.size) + .stripe(self.stripe), + ) } } diff --git a/crates/ui/src/button/button.rs b/crates/ui/src/button/button.rs index 50eecb61..42127e4d 100644 --- a/crates/ui/src/button/button.rs +++ b/crates/ui/src/button/button.rs @@ -136,9 +136,10 @@ impl ButtonCustomVariant { } /// The veriant of the Button. -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq, Default)] pub enum ButtonVariant { Primary, + #[default] Secondary, Danger, Info, @@ -150,12 +151,6 @@ pub enum ButtonVariant { Custom(ButtonCustomVariant), } -impl Default for ButtonVariant { - fn default() -> Self { - Self::Secondary - } -} - impl ButtonVariant { #[inline] pub fn is_link(&self) -> bool { diff --git a/crates/ui/src/input/popovers/code_action_menu.rs b/crates/ui/src/input/popovers/code_action_menu.rs index e1e31178..b8949940 100644 --- a/crates/ui/src/input/popovers/code_action_menu.rs +++ b/crates/ui/src/input/popovers/code_action_menu.rs @@ -14,7 +14,7 @@ const MAX_MENU_HEIGHT: Pixels = px(480.); use crate::{ actions, h_flex, input::{self, popovers::editor_popover, InputState}, - list::{List, ListDelegate, ListEvent}, + list::{List, ListDelegate, ListEvent, ListState}, ActiveTheme, IndexPath, Selectable, }; @@ -110,12 +110,7 @@ impl ListDelegate for MenuDelegate { self.items.len() } - fn render_item( - &self, - ix: crate::IndexPath, - _: &mut Window, - _: &mut Context>, - ) -> Option { + fn render_item(&self, ix: crate::IndexPath, _: &mut Window, _: &mut App) -> Option { let item = self.items.get(ix.row)?; Some(MenuItem::new(ix.row, item.clone())) } @@ -124,13 +119,13 @@ impl ListDelegate for MenuDelegate { &mut self, ix: Option, _: &mut Window, - cx: &mut Context>, + cx: &mut Context>, ) { self.selected_ix = ix.map(|i| i.row).unwrap_or(0); cx.notify(); } - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { + fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { let Some(item) = self.selected_item() else { return; }; @@ -145,7 +140,7 @@ impl ListDelegate for MenuDelegate { pub struct CodeActionMenu { offset: usize, state: Entity, - list: Entity>, + list: Entity>, open: bool, bounds: Bounds, @@ -169,11 +164,7 @@ impl CodeActionMenu { selected_ix: 0, }; - let list = cx.new(|cx| { - List::new(menu, window, cx) - .no_query() - .max_h(MAX_MENU_HEIGHT) - }); + let list = cx.new(|cx| ListState::new(menu, window, cx).no_query()); let _subscriptions = vec![ @@ -336,7 +327,7 @@ impl Render for CodeActionMenu { .top(pos.y) .max_w(max_width) .min_w(px(120.)) - .child(self.list.clone()) + .child(List::new(&self.list).max_h(MAX_MENU_HEIGHT)) .child( canvas( move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds), diff --git a/crates/ui/src/input/popovers/completion_menu.rs b/crates/ui/src/input/popovers/completion_menu.rs index 74c95c9b..01a1102f 100644 --- a/crates/ui/src/input/popovers/completion_menu.rs +++ b/crates/ui/src/input/popovers/completion_menu.rs @@ -20,7 +20,7 @@ use crate::{ InputState, RopeExt, }, label::Label, - list::{List, ListDelegate, ListEvent}, + list::{List, ListDelegate, ListEvent, ListState}, ActiveTheme, IndexPath, Selectable, }; @@ -137,12 +137,7 @@ impl ListDelegate for ContextMenuDelegate { self.items.len() } - fn render_item( - &self, - ix: crate::IndexPath, - _: &mut Window, - _: &mut Context>, - ) -> Option { + fn render_item(&self, ix: crate::IndexPath, _: &mut Window, _: &mut App) -> Option { let item = self.items.get(ix.row)?; Some(CompletionMenuItem::new(ix.row, item.clone()).highlight_prefix(self.query.clone())) } @@ -151,13 +146,13 @@ impl ListDelegate for ContextMenuDelegate { &mut self, ix: Option, _: &mut Window, - cx: &mut Context>, + cx: &mut Context>, ) { self.selected_ix = ix.map(|i| i.row).unwrap_or(0); cx.notify(); } - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { + fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { let Some(item) = self.selected_item() else { return; }; @@ -172,7 +167,7 @@ impl ListDelegate for ContextMenuDelegate { pub struct CompletionMenu { offset: usize, editor: Entity, - list: Entity>, + list: Entity>, open: bool, bounds: Bounds, @@ -200,11 +195,7 @@ impl CompletionMenu { selected_ix: 0, }; - let list = cx.new(|cx| { - List::new(menu, window, cx) - .no_query() - .max_h(MAX_MENU_HEIGHT) - }); + let list = cx.new(|cx| ListState::new(menu, window, cx).no_query()); let _subscriptions = vec![ @@ -437,7 +428,7 @@ impl Render for CompletionMenu { editor_popover("completion-menu", cx) .max_w(max_width) .min_w(px(120.)) - .child(self.list.clone()) + .child(List::new(&self.list).max_h(MAX_MENU_HEIGHT)) .child( canvas( move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds), diff --git a/crates/ui/src/list/delegate.rs b/crates/ui/src/list/delegate.rs index e58efce5..4979bbe8 100644 --- a/crates/ui/src/list/delegate.rs +++ b/crates/ui/src/list/delegate.rs @@ -2,7 +2,7 @@ use gpui::{AnyElement, App, Context, IntoElement, ParentElement as _, Styled as use crate::{ h_flex, - list::{loading::Loading, List}, + list::{loading::Loading, ListState}, ActiveTheme as _, Icon, IconName, IndexPath, Selectable, }; @@ -17,7 +17,7 @@ pub trait ListDelegate: Sized + 'static { &mut self, query: &str, window: &mut Window, - cx: &mut Context>, + cx: &mut Context>, ) -> Task<()> { Task::ready(()) } @@ -35,12 +35,7 @@ pub trait ListDelegate: Sized + 'static { /// Return None will skip the item. /// /// NOTE: Every item should have same height. - fn render_item( - &self, - ix: IndexPath, - window: &mut Window, - cx: &mut Context>, - ) -> Option; + fn render_item(&self, ix: IndexPath, window: &mut Window, cx: &mut App) -> Option; /// Render the section header at the given index, default is None. /// @@ -49,7 +44,7 @@ pub trait ListDelegate: Sized + 'static { &self, section: usize, window: &mut Window, - cx: &mut Context>, + cx: &mut App, ) -> Option { None:: } @@ -61,13 +56,13 @@ pub trait ListDelegate: Sized + 'static { &self, section: usize, window: &mut Window, - cx: &mut Context>, + cx: &mut App, ) -> Option { None:: } /// Return a Element to show when list is empty. - fn render_empty(&self, window: &mut Window, cx: &mut Context>) -> impl IntoElement { + fn render_empty(&self, window: &mut Window, cx: &mut App) -> impl IntoElement { h_flex() .size_full() .justify_center() @@ -84,11 +79,7 @@ pub trait ListDelegate: Sized + 'static { /// For example: The last search results, or the last selected item. /// /// Default is None, that means no initial state. - fn render_initial( - &self, - window: &mut Window, - cx: &mut Context>, - ) -> Option { + fn render_initial(&self, window: &mut Window, cx: &mut App) -> Option { None } @@ -99,11 +90,7 @@ pub trait ListDelegate: Sized + 'static { /// Returns a Element to show when loading, default is built-in Skeleton /// loading view. - fn render_loading( - &self, - window: &mut Window, - cx: &mut Context>, - ) -> impl IntoElement { + fn render_loading(&self, window: &mut Window, cx: &mut App) -> impl IntoElement { Loading } @@ -112,17 +99,18 @@ pub trait ListDelegate: Sized + 'static { &mut self, ix: Option, window: &mut Window, - cx: &mut Context>, + cx: &mut Context>, ); /// Set the confirm and give the selected index, /// this is means user have clicked the item or pressed Enter. /// /// This will always to `set_selected_index` before confirm. - fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) {} + fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { + } /// Cancel the selection, e.g.: Pressed ESC. - fn cancel(&mut self, window: &mut Window, cx: &mut Context>) {} + fn cancel(&mut self, window: &mut Window, cx: &mut Context>) {} /// Return true to enable load more data when scrolling to the bottom. /// @@ -149,5 +137,5 @@ pub trait ListDelegate: Sized + 'static { /// 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>) {} + fn load_more(&mut self, window: &mut Window, cx: &mut Context>) {} } diff --git a/crates/ui/src/list/list.rs b/crates/ui/src/list/list.rs index 0ff31a73..44191ce6 100644 --- a/crates/ui/src/list/list.rs +++ b/crates/ui/src/list/list.rs @@ -10,14 +10,14 @@ use crate::{ v_flex, ActiveTheme, IconName, Size, }; use crate::{list::ListDelegate, v_virtual_list, VirtualListScrollHandle}; -use crate::{Icon, IndexPath, Selectable, Sizable as _, StyledExt}; +use crate::{Icon, IndexPath, Selectable, Sizable, StyledExt}; use gpui::{ div, prelude::FluentBuilder, AppContext, Entity, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding, Length, MouseButton, ParentElement, Render, Styled, Task, Window, }; use gpui::{ px, size, App, AvailableSpace, ClickEvent, Context, Edges, EventEmitter, ListSizingBehavior, - Pixels, ScrollStrategy, SharedString, StatefulInteractiveElement, Subscription, + Pixels, RenderOnce, ScrollStrategy, SharedString, StatefulInteractiveElement, Subscription, }; use rust_i18n::t; use smol::Timer; @@ -43,19 +43,16 @@ pub enum ListEvent { Cancel, } -pub struct List { +/// The state for List. +pub struct ListState { focus_handle: FocusHandle, - delegate: D, - max_height: Option, - paddings: Edges, query_input: Option>, + delegate: D, last_query: Option, selectable: bool, querying: bool, - scrollbar_visible: bool, scroll_handle: VirtualListScrollHandle, scroll_state: ScrollbarState, - pub(crate) size: Size, rows_cache: RowsCache, selected_index: Option, item_to_measure_index: IndexPath, @@ -67,7 +64,7 @@ pub struct List { _query_input_subscription: Subscription, } -impl List +impl ListState where D: ListDelegate, { @@ -90,35 +87,15 @@ where mouse_right_clicked_index: None, scroll_handle: VirtualListScrollHandle::new(), scroll_state: ScrollbarState::default(), - max_height: None, - scrollbar_visible: true, selectable: true, querying: false, - size: Size::default(), reset_on_cancel: true, - paddings: Edges::default(), _search_task: Task::ready(()), _load_more_task: Task::ready(()), _query_input_subscription, } } - /// Set the size - pub fn set_size(&mut self, size: Size, _: &mut Window, _: &mut Context) { - self.size = size; - } - - pub fn max_h(mut self, height: impl Into) -> Self { - self.max_height = Some(height.into()); - self - } - - /// Set the visibility of the scrollbar, default is true. - pub fn scrollbar_visible(mut self, visible: bool) -> Self { - self.scrollbar_visible = visible; - self - } - pub fn no_query(mut self) -> Self { self.query_input = None; self @@ -198,17 +175,6 @@ where cx.notify(); } - fn render_scrollbar(&self, _: &mut Window, _: &mut Context) -> Option { - if !self.scrollbar_visible { - return None; - } - - Some(Scrollbar::uniform_scroll( - &self.scroll_state, - &self.scroll_handle, - )) - } - /// Scroll to the item at the given index. pub fn scroll_to_item( &mut self, @@ -241,12 +207,6 @@ where } } - /// Set paddings for the list. - pub fn paddings(mut self, paddings: Edges) -> Self { - self.paddings = paddings; - self - } - fn on_query_input_event( &mut self, state: &Entity, @@ -409,124 +369,6 @@ where self.select_item(next_ix, window, cx); } - fn render_list_item( - &self, - ix: IndexPath, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let selected = self.selected_index.map(|s| s.eq_row(ix)).unwrap_or(false); - let mouse_right_clicked = self - .mouse_right_clicked_index - .map(|s| s.eq_row(ix)) - .unwrap_or(false); - let id = SharedString::from(format!("list-item-{}", ix)); - - div() - .id(id) - .w_full() - .relative() - .children(self.delegate.render_item(ix, window, cx).map(|item| { - item.selected(selected) - .secondary_selected(mouse_right_clicked) - })) - .when(self.selectable, |this| { - this.on_click(cx.listener(move |this, e: &ClickEvent, window, cx| { - this.mouse_right_clicked_index = None; - this.selected_index = Some(ix); - this.on_action_confirm( - &Confirm { - secondary: e.modifiers().secondary(), - }, - window, - cx, - ); - })) - .on_mouse_down( - MouseButton::Right, - cx.listener(move |this, _, _, cx| { - this.mouse_right_clicked_index = Some(ix); - cx.notify(); - }), - ) - }) - } - - fn render_items( - &mut self, - items_count: usize, - entities_count: usize, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let measured_size = self.rows_cache.measured_size(); - - v_flex() - .flex_grow() - .relative() - .h_full() - .min_w(measured_size.item_size.width) - .when_some(self.max_height, |this, h| this.max_h(h)) - .overflow_hidden() - .when(items_count == 0, |this| { - this.child(self.delegate().render_empty(window, cx)) - }) - .when(items_count > 0, { - let rows_cache = self.rows_cache.clone(); - |this| { - this.child( - v_virtual_list( - cx.entity().clone(), - "virtual-list", - rows_cache.entries_sizes.clone(), - move |list, visible_range: Range, window, cx| { - list.load_more_if_need( - entities_count, - visible_range.end, - window, - cx, - ); - - // NOTE: Here the v_virtual_list would not able to have gap_y, - // because the section header, footer is always have rendered as a empty child item, - // even the delegate give a None result. - - visible_range - .map(|ix| { - let Some(entry) = rows_cache.get(ix) else { - return div(); - }; - - div().children(match entry { - RowEntry::Entry(index) => Some( - list.render_list_item(index, window, cx) - .into_any_element(), - ), - RowEntry::SectionHeader(section_ix) => list - .delegate() - .render_section_header(section_ix, window, cx) - .map(|r| r.into_any_element()), - RowEntry::SectionFooter(section_ix) => list - .delegate() - .render_section_footer(section_ix, window, cx) - .map(|r| r.into_any_element()), - }) - }) - .collect::>() - }, - ) - .paddings(self.paddings) - .when(self.max_height.is_some(), |this| { - this.with_sizing_behavior(ListSizingBehavior::Infer) - }) - .track_scroll(&self.scroll_handle) - .into_any_element(), - ) - } - }) - .children(self.render_scrollbar(window, cx)) - } - fn prepare_items_if_needed(&mut self, window: &mut Window, cx: &mut Context) { let sections_count = self.delegate.sections_count(cx); @@ -559,9 +401,54 @@ where self.delegate.items_count(section_ix, cx) }); } + + fn render_list_item( + &self, + ix: IndexPath, + window: &mut Window, + cx: &mut Context, + ) -> impl IntoElement { + let selectable = self.selectable; + + let selected = self.selected_index.map(|s| s.eq_row(ix)).unwrap_or(false); + let mouse_right_clicked = self + .mouse_right_clicked_index + .map(|s| s.eq_row(ix)) + .unwrap_or(false); + let id = SharedString::from(format!("list-item-{}", ix)); + + div() + .id(id) + .w_full() + .relative() + .children(self.delegate.render_item(ix, window, cx).map(|item| { + item.selected(selected) + .secondary_selected(mouse_right_clicked) + })) + .when(selectable, |this| { + this.on_click(cx.listener(move |this, e: &ClickEvent, window, cx| { + this.mouse_right_clicked_index = None; + this.selected_index = Some(ix); + this.on_action_confirm( + &Confirm { + secondary: e.modifiers().secondary(), + }, + window, + cx, + ); + })) + .on_mouse_down( + MouseButton::Right, + cx.listener(move |this, _, _, cx| { + this.mouse_right_clicked_index = Some(ix); + cx.notify(); + }), + ) + }) + } } -impl Focusable for List +impl Focusable for ListState where D: ListDelegate, { @@ -573,43 +460,204 @@ where } } } -impl EventEmitter for List where D: ListDelegate {} -impl Render for List +impl EventEmitter for ListState where D: ListDelegate {} +impl Render for ListState where D: ListDelegate, { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - self.prepare_items_if_needed(window, cx); + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div() + } +} - // Scroll to the selected item if it is set. - if let Some((ix, strategy)) = self.deferred_scroll_to_index.take() { - if let Some(item_ix) = self.rows_cache.position_of(&ix) { - self.scroll_handle.scroll_to_item(item_ix, strategy); - } +/// The List element. +#[derive(IntoElement)] +pub struct List { + state: Entity>, + + max_height: Option, + paddings: Edges, + scrollbar_visible: bool, + pub(crate) size: Size, +} + +impl List +where + D: ListDelegate + 'static, +{ + /// Create a new List element with the given ListState entity. + pub fn new(state: &Entity>) -> Self { + Self { + state: state.clone(), + max_height: None, + paddings: Edges::default(), + scrollbar_visible: true, + size: Size::default(), } + } - let items_count = self.rows_cache.items_count(); - let entities_count = self.rows_cache.len(); - let loading = self.delegate.loading(cx); + /// Set paddings for the list. + pub fn paddings(mut self, paddings: Edges) -> Self { + self.paddings = paddings; + self + } - let initial_view = if let Some(input) = &self.query_input { - if input.read(cx).value().is_empty() { - self.delegate().render_initial(window, cx) + pub fn max_h(mut self, max_height: impl Into) -> Self { + self.max_height = Some(max_height.into()); + self + } + + pub fn scrollbar_visible(mut self, visible: bool) -> Self { + self.scrollbar_visible = visible; + self + } + + fn render_items( + &self, + items_count: usize, + entities_count: usize, + window: &mut Window, + cx: &mut App, + ) -> impl IntoElement { + self.state.update(cx, |state, cx| { + let rows_cache = state.rows_cache.clone(); + + let scrollbar_visible = self.scrollbar_visible; + let scroll_handle = state.scroll_handle.clone(); + let scroll_state = state.scroll_state.clone(); + let measured_size = rows_cache.measured_size(); + + v_flex() + .flex_grow() + .relative() + .h_full() + .min_w(measured_size.item_size.width) + .when_some(self.max_height, |this, h| this.max_h(h)) + .overflow_hidden() + .when(items_count == 0, |this| { + this.child(state.delegate.render_empty(window, cx)) + }) + .when(items_count > 0, { + |this| { + this.child( + v_virtual_list( + self.state.clone(), + "virtual-list", + rows_cache.entries_sizes.clone(), + move |list, visible_range: Range, window, cx| { + list.load_more_if_need( + entities_count, + visible_range.end, + window, + cx, + ); + + // NOTE: Here the v_virtual_list would not able to have gap_y, + // because the section header, footer is always have rendered as a empty child item, + // even the delegate give a None result. + + visible_range + .map(|ix| { + let Some(entry) = rows_cache.get(ix) else { + return div(); + }; + + div().children(match entry { + RowEntry::Entry(index) => Some( + list.render_list_item(index, window, cx) + .into_any_element(), + ), + RowEntry::SectionHeader(section_ix) => list + .delegate() + .render_section_header(section_ix, window, cx) + .map(|r| r.into_any_element()), + RowEntry::SectionFooter(section_ix) => list + .delegate() + .render_section_footer(section_ix, window, cx) + .map(|r| r.into_any_element()), + }) + }) + .collect::>() + }, + ) + .paddings(self.paddings) + .when(self.max_height.is_some(), |this| { + this.with_sizing_behavior(ListSizingBehavior::Infer) + }) + .track_scroll(&scroll_handle) + .into_any_element(), + ) + } + }) + .when(scrollbar_visible, |this| { + this.child(Scrollbar::uniform_scroll(&scroll_state, &scroll_handle)) + }) + }) + } +} + +impl Sizable for List +where + D: ListDelegate + 'static, +{ + fn with_size(mut self, size: impl Into) -> Self { + self.size = size.into(); + self + } +} + +impl RenderOnce for List +where + D: ListDelegate + 'static, +{ + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let mut loading = false; + let mut query_input = None; + let mut loading_view = None; + let mut initial_view = None; + + self.state.update(cx, |state, cx| { + state.prepare_items_if_needed(window, cx); + + // Scroll to the selected item if it is set. + if let Some((ix, strategy)) = state.deferred_scroll_to_index.take() { + if let Some(item_ix) = state.rows_cache.position_of(&ix) { + state.scroll_handle.scroll_to_item(item_ix, strategy); + } + } + + loading = state.delegate().loading(cx); + query_input = state.query_input.clone(); + loading_view = if loading { + Some(state.delegate.render_loading(window, cx).into_any_element()) } else { None - } - } else { - None - }; + }; + initial_view = if let Some(input) = &query_input { + if input.read(cx).value().is_empty() { + state.delegate.render_initial(window, cx) + } else { + None + } + } else { + None + }; + }); + + let state = self.state.read(cx); + let focus_handle = state.focus_handle.clone(); + let items_count = state.rows_cache.items_count(); + let entities_count = state.rows_cache.len(); + let mouse_right_clicked_index = state.mouse_right_clicked_index; v_flex() .key_context("List") .id("list") - .track_focus(&self.focus_handle) + .track_focus(&focus_handle) .size_full() .relative() .overflow_hidden() - .when_some(self.query_input.clone(), |this, input| { + .when_some(query_input.clone(), |this, input| { this.child( div() .map(|this| match self.size { @@ -631,14 +679,12 @@ where ), ) }) - .when(loading, |this| { - this.child(self.delegate().render_loading(window, cx)) - }) + .children(loading_view) .when(!loading, |this| { - this.on_action(cx.listener(Self::on_action_cancel)) - .on_action(cx.listener(Self::on_action_confirm)) - .on_action(cx.listener(Self::on_action_select_next)) - .on_action(cx.listener(Self::on_action_select_prev)) + this.on_action(window.listener_for(&self.state, ListState::on_action_cancel)) + .on_action(window.listener_for(&self.state, ListState::on_action_confirm)) + .on_action(window.listener_for(&self.state, ListState::on_action_select_next)) + .on_action(window.listener_for(&self.state, ListState::on_action_select_prev)) .map(|this| { if let Some(view) = initial_view { this.child(view) @@ -647,11 +693,14 @@ where } }) // Click out to cancel right clicked row - .when(self.mouse_right_clicked_index.is_some(), |this| { - this.on_mouse_down_out(cx.listener(|this, _, _, cx| { - this.mouse_right_clicked_index = None; - cx.notify(); - })) + .when(mouse_right_clicked_index.is_some(), |this| { + this.on_mouse_down_out(window.listener_for( + &self.state, + |this, _, _, cx| { + this.mouse_right_clicked_index = None; + cx.notify(); + }, + )) }) }) } diff --git a/crates/ui/src/select.rs b/crates/ui/src/select.rs index 2afff7f9..d3d8abfc 100644 --- a/crates/ui/src/select.rs +++ b/crates/ui/src/select.rs @@ -11,7 +11,7 @@ use crate::{ actions::{Cancel, Confirm, SelectDown, SelectUp}, h_flex, input::clear_button, - list::{List, ListDelegate}, + list::{List, ListDelegate, ListState}, v_flex, ActiveTheme, Disableable, Icon, IconName, IndexPath, Selectable, Sizable, Size, StyleSized, StyledExt, }; @@ -113,7 +113,12 @@ pub trait SelectDelegate: Sized { false } - fn perform_search(&mut self, _query: &str, _window: &mut Window, _: &mut App) -> Task<()> { + fn perform_search( + &mut self, + _query: &str, + _window: &mut Window, + _: &mut Context>, + ) -> Task<()> { Task::ready(()) } } @@ -164,7 +169,7 @@ where &self, section: usize, _: &mut Window, - cx: &mut Context>, + cx: &mut App, ) -> Option { let state = self.state.upgrade()?.read(cx); let Some(item) = self.delegate.section(section) else { @@ -182,12 +187,7 @@ where ); } - fn render_item( - &self, - ix: IndexPath, - _: &mut Window, - cx: &mut Context>, - ) -> Option { + fn render_item(&self, ix: IndexPath, _: &mut Window, cx: &mut App) -> Option { let selected = self .selected_index .map_or(false, |selected_index| selected_index == ix); @@ -213,7 +213,7 @@ where } } - fn cancel(&mut self, window: &mut Window, cx: &mut Context>) { + fn cancel(&mut self, window: &mut Window, cx: &mut Context>) { let state = self.state.clone(); cx.defer_in(window, move |_, window, cx| { _ = state.update(cx, |this, cx| { @@ -223,7 +223,12 @@ where }); } - fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { + fn confirm( + &mut self, + _secondary: bool, + window: &mut Window, + cx: &mut Context>, + ) { let selected_value = self .selected_index .and_then(|ix| self.delegate.item(ix)) @@ -244,7 +249,7 @@ where &mut self, query: &str, window: &mut Window, - cx: &mut Context>, + cx: &mut Context>, ) -> Task<()> { self.state.upgrade().map_or(Task::ready(()), |state| { state.update(cx, |_, cx| self.delegate.perform_search(query, window, cx)) @@ -255,12 +260,12 @@ where &mut self, ix: Option, _: &mut Window, - _: &mut Context>, + _: &mut Context>, ) { self.selected_index = ix; } - fn render_empty(&self, window: &mut Window, cx: &mut Context>) -> impl IntoElement { + fn render_empty(&self, window: &mut Window, cx: &mut App) -> impl IntoElement { if let Some(empty) = self .state .upgrade() @@ -285,7 +290,7 @@ pub enum SelectEvent { /// State of the [`Select`]. pub struct SelectState { focus_handle: FocusHandle, - list: Entity>>, + list: Entity>>, size: Size, empty: Option AnyElement>>, /// Store the bounds of the input @@ -373,7 +378,12 @@ impl SelectDelegate for SearchableVec { true } - fn perform_search(&mut self, query: &str, _window: &mut Window, _: &mut App) -> Task<()> { + fn perform_search( + &mut self, + query: &str, + _window: &mut Window, + _: &mut Context>, + ) -> Task<()> { self.matched_items = self .items .iter() @@ -434,7 +444,12 @@ impl SelectDelegate for SearchableVec> { true } - fn perform_search(&mut self, query: &str, _window: &mut Window, _: &mut App) -> Task<()> { + fn perform_search( + &mut self, + query: &str, + _window: &mut Window, + _: &mut Context>, + ) -> Task<()> { self.matched_items = self .items .iter() @@ -519,10 +534,7 @@ where let searchable = delegate.delegate.searchable(); let list = cx.new(|cx| { - let mut list = List::new(delegate, window, cx) - .max_h(rems(20.)) - .paddings(Edges::all(px(4.))) - .reset_on_cancel(false); + let mut list = ListState::new(delegate, window, cx).reset_on_cancel(false); if !searchable { list = list.no_query(); } @@ -847,13 +859,8 @@ where let focus_handle = self.state.focus_handle(cx); let is_focused = focus_handle.is_focused(window); // If the size has change, set size to self.list, to change the QueryInput size. - let old_size = self.state.read(cx).list.read(cx).size; + let old_size = self.state.read(cx).size; if old_size != self.size { - self.state - .read(cx) - .list - .clone() - .update(cx, |this, cx| this.set_size(self.size, window, cx)); self.state.update(cx, |this, _| { this.size = self.size; }); @@ -984,7 +991,12 @@ where .border_color(cx.theme().border) .rounded(popup_radius) .shadow_md() - .child(state.list.clone()), + .child( + List::new(&state.list) + .with_size(self.size) + .max_h(rems(20.)) + .paddings(Edges::all(px(4.))), + ), ) .on_mouse_down_out(window.listener_for( &self.state, diff --git a/crates/ui/src/table/delegate.rs b/crates/ui/src/table/delegate.rs index 0917cbb3..05790551 100644 --- a/crates/ui/src/table/delegate.rs +++ b/crates/ui/src/table/delegate.rs @@ -8,7 +8,7 @@ use gpui::{ use crate::{ h_flex, menu::PopupMenu, - table::{loading::Loading, Column, ColumnSort, Table}, + table::{loading::Loading, Column, ColumnSort, TableState}, ActiveTheme as _, Icon, IconName, Size, }; @@ -30,29 +30,19 @@ pub trait TableDelegate: Sized + 'static { col_ix: usize, sort: ColumnSort, window: &mut Window, - cx: &mut Context>, + 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 { + fn render_th(&self, col_ix: usize, window: &mut Window, cx: &mut App) -> 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
{ + fn render_tr(&self, row_ix: usize, window: &mut Window, cx: &mut App) -> Stateful
{ h_flex().id(("row", row_ix)) } @@ -67,7 +57,7 @@ pub trait TableDelegate: Sized + 'static { row_ix: usize, col_ix: usize, window: &mut Window, - cx: &mut Context>, + cx: &mut App, ) -> impl IntoElement; /// Move the column at the given `col_ix` to insert before the column at the given `to_ix`. @@ -76,12 +66,12 @@ pub trait TableDelegate: Sized + 'static { col_ix: usize, to_ix: usize, window: &mut Window, - cx: &mut Context>, + cx: &mut Context>, ) { } /// Return a Element to show when table is empty. - fn render_empty(&self, window: &mut Window, cx: &mut Context>) -> impl IntoElement { + fn render_empty(&self, window: &mut Window, cx: &mut App) -> impl IntoElement { h_flex() .size_full() .justify_center() @@ -98,12 +88,7 @@ pub trait TableDelegate: Sized + 'static { /// 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 { + fn render_loading(&self, size: Size, window: &mut Window, cx: &mut App) -> impl IntoElement { Loading::new().size(size) } @@ -129,14 +114,10 @@ pub trait TableDelegate: Sized + 'static { /// /// 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>) {} + 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 { + fn render_last_empty_col(&self, window: &mut Window, cx: &mut App) -> impl IntoElement { h_flex().w_3().h_full().flex_shrink_0() } @@ -150,7 +131,7 @@ pub trait TableDelegate: Sized + 'static { &mut self, visible_range: Range, window: &mut Window, - cx: &mut Context>, + cx: &mut Context>, ) { } @@ -164,7 +145,7 @@ pub trait TableDelegate: Sized + 'static { &mut self, visible_range: Range, window: &mut Window, - cx: &mut Context>, + cx: &mut Context>, ) { } } diff --git a/crates/ui/src/table/mod.rs b/crates/ui/src/table/mod.rs index 0c453343..5f3ca2e3 100644 --- a/crates/ui/src/table/mod.rs +++ b/crates/ui/src/table/mod.rs @@ -10,10 +10,10 @@ use crate::{ }; use gpui::{ actions, canvas, div, prelude::FluentBuilder, px, uniform_list, App, AppContext, Axis, Bounds, - Context, Div, DragMoveEvent, Edges, EventEmitter, FocusHandle, Focusable, InteractiveElement, - IntoElement, KeyBinding, ListSizingBehavior, MouseButton, MouseDownEvent, ParentElement, - Pixels, Point, Render, ScrollStrategy, ScrollWheelEvent, SharedString, - StatefulInteractiveElement as _, Styled, Task, UniformListScrollHandle, Window, + Context, Div, DragMoveEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, + InteractiveElement, IntoElement, KeyBinding, ListSizingBehavior, MouseButton, MouseDownEvent, + ParentElement, Pixels, Point, Render, RenderOnce, ScrollStrategy, ScrollWheelEvent, + SharedString, StatefulInteractiveElement as _, Styled, Task, UniformListScrollHandle, Window, }; mod column; @@ -25,14 +25,14 @@ pub use delegate::*; actions!(table, [SelectPrevColumn, SelectNextColumn]); +const CONTEXT: &'static str = "Table"; pub(crate) fn init(cx: &mut App) { - let context = Some("Table"); cx.bind_keys([ - KeyBinding::new("escape", Cancel, context), - KeyBinding::new("up", SelectUp, context), - KeyBinding::new("down", SelectDown, context), - KeyBinding::new("left", SelectPrevColumn, context), - KeyBinding::new("right", SelectNextColumn, context), + KeyBinding::new("escape", Cancel, Some(CONTEXT)), + KeyBinding::new("up", SelectUp, Some(CONTEXT)), + KeyBinding::new("down", SelectDown, Some(CONTEXT)), + KeyBinding::new("left", SelectPrevColumn, Some(CONTEXT)), + KeyBinding::new("right", SelectNextColumn, Some(CONTEXT)), ]); } @@ -74,7 +74,8 @@ impl VisibleRangeState { } } -pub struct Table { +/// The state for Table. +pub struct TableState { focus_handle: FocusHandle, delegate: D, /// The bounds of the table container. @@ -106,7 +107,6 @@ pub struct Table { pub horizontal_scroll_handle: VirtualListScrollHandle, pub horizontal_scroll_state: ScrollbarState, - scrollbar_visible: Edges, selected_row: Option, selection_state: SelectionState, right_clicked_row: Option, @@ -115,20 +115,17 @@ pub struct Table { /// The column index that is being resized. resizing_col: Option, - /// Set stripe style of the table. - stripe: bool, - /// Set to use border style of the table. - border: bool, - /// The cell size of the table. - size: Size, /// The visible range of the rows and columns. visible_range: VisibleRangeState, + stripe: bool, + size: Size, + _measure: Vec, _load_more_task: Task<()>, } -impl Table +impl TableState where D: TableDelegate, { @@ -148,10 +145,6 @@ where resizing_col: None, bounds: Bounds::default(), fixed_head_cols_bounds: Bounds::default(), - stripe: false, - border: true, - size: Size::default(), - scrollbar_visible: Edges::all(true), visible_range: VisibleRangeState::default(), loop_selection: true, col_selectable: true, @@ -160,6 +153,8 @@ where col_movable: true, col_resizable: true, col_fixed: true, + stripe: false, + size: Size::default(), _load_more_task: Task::ready(()), _measure: Vec::new(), }; @@ -176,23 +171,6 @@ where &mut self.delegate } - /// Set to use stripe style of the table, default to false. - pub fn stripe(mut self, stripe: bool) -> Self { - self.stripe = stripe; - self - } - - pub fn set_stripe(&mut self, stripe: bool, cx: &mut Context) { - self.stripe = stripe; - cx.notify(); - } - - /// Set to use border style of the table, default to true. - pub fn border(mut self, border: bool) -> Self { - self.border = border; - self - } - /// Set to loop selection, default to true. pub fn loop_selection(mut self, loop_selection: bool) -> Self { self.loop_selection = loop_selection; @@ -229,27 +207,6 @@ where self } - /// Set the size to the table. - pub fn set_size(&mut self, size: Size, cx: &mut Context) { - self.size = size; - cx.notify(); - } - - /// Get the size of the table. - pub fn size(&self) -> Size { - self.size - } - - /// Set scrollbar visibility. - pub fn scrollbar_visible(mut self, vertical: bool, horizontal: bool) -> Self { - self.scrollbar_visible = Edges { - right: vertical, - bottom: horizontal, - ..Default::default() - }; - self - } - /// When we update columns or rows, we need to refresh the table. pub fn refresh(&mut self, cx: &mut Context) { self.prepare_col_groups(cx); @@ -673,51 +630,6 @@ where } } - fn render_vertical_scrollbar( - &self, - _: &mut Window, - cx: &mut Context, - ) -> Option { - let state = self.vertical_scroll_state.clone(); - - Some( - div() - .occlude() - .absolute() - .top(self.size.table_row_height()) - .right_0() - .bottom_0() - .w(Scrollbar::width()) - .on_scroll_wheel(cx.listener(|_, _: &ScrollWheelEvent, _, cx| { - cx.notify(); - })) - .child(Scrollbar::uniform_scroll(&state, &self.vertical_scroll_handle).max_fps(60)), - ) - } - - fn render_horizontal_scrollbar( - &self, - _: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let state = self.horizontal_scroll_state.clone(); - - div() - .occlude() - .absolute() - .left(self.fixed_head_cols_bounds.size.width) - .right_0() - .bottom_0() - .h(Scrollbar::width()) - .on_scroll_wheel(cx.listener(|_, _: &ScrollWheelEvent, _, cx| { - cx.notify(); - })) - .child(Scrollbar::horizontal( - &state, - &self.horizontal_scroll_handle, - )) - } - fn render_resize_handle( &self, ix: usize, @@ -1292,6 +1204,125 @@ where } } +impl Focusable for TableState +where + D: TableDelegate, +{ + fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle { + self.focus_handle.clone() + } +} +impl EventEmitter for TableState where D: TableDelegate {} + +impl Render for TableState +where + D: TableDelegate, +{ + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div() + } +} + +/// A table element. +#[derive(IntoElement)] +pub struct Table { + state: Entity>, + scrollbar_visible: Edges, + /// Set stripe style of the table. + stripe: bool, + /// Set to use border style of the table. + border: bool, + /// The cell size of the table. + size: Size, +} + +impl Table +where + D: TableDelegate, +{ + /// Create a new Table element. + pub fn new(state: &Entity>) -> Self { + Self { + state: state.clone(), + scrollbar_visible: Edges::all(true), + stripe: false, + border: true, + size: Size::default(), + } + } + + /// Set to use stripe style of the table, default to false. + pub fn stripe(mut self, stripe: bool) -> Self { + self.stripe = stripe; + self + } + + /// Set to use border style of the table, default to true. + pub fn border(mut self, border: bool) -> Self { + self.border = border; + self + } + + /// Set scrollbar visibility. + pub fn scrollbar_visible(mut self, vertical: bool, horizontal: bool) -> Self { + self.scrollbar_visible = Edges { + right: vertical, + bottom: horizontal, + ..Default::default() + }; + self + } + + fn render_vertical_scrollbar( + &self, + state: &Entity>, + scroll_state: &ScrollbarState, + scroll_handle: &UniformListScrollHandle, + window: &mut Window, + _: &mut App, + ) -> Option { + Some( + div() + .occlude() + .absolute() + .top(self.size.table_row_height()) + .right_0() + .bottom_0() + .w(Scrollbar::width()) + .on_scroll_wheel( + window.listener_for(&state, |_, _: &ScrollWheelEvent, _, cx| { + cx.notify(); + }), + ) + .child(Scrollbar::uniform_scroll(scroll_state, scroll_handle).max_fps(60)), + ) + } + + fn render_horizontal_scrollbar( + &self, + state: &Entity>, + fixed_head_cols_bounds: Bounds, + scroll_state: &ScrollbarState, + scroll_handle: &VirtualListScrollHandle, + window: &mut Window, + _: &mut App, + ) -> impl IntoElement { + div() + .occlude() + .absolute() + .left(fixed_head_cols_bounds.size.width) + .right_0() + .bottom_0() + .h(Scrollbar::width()) + .on_scroll_wheel( + window.listener_for(&state, |_, _: &ScrollWheelEvent, _, cx| { + cx.notify(); + }), + ) + .child(Scrollbar::horizontal(scroll_state, scroll_handle)) + } +} + impl Sizable for Table where D: TableDelegate, @@ -1301,148 +1332,173 @@ where self } } -impl Focusable for Table + +impl RenderOnce for Table where D: TableDelegate, { - fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle { - self.focus_handle.clone() - } -} -impl EventEmitter for Table where D: TableDelegate {} - -impl Render for Table -where - D: TableDelegate, -{ - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - self.measure(window, cx); - - let view = cx.entity().clone(); - let vertical_scroll_handle = self.vertical_scroll_handle.clone(); - let horizontal_scroll_handle = self.horizontal_scroll_handle.clone(); - let columns_count: usize = self.delegate.columns_count(cx); - let left_columns_count = self + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let state = self.state.read(cx); + let focus_handle = state.focus_handle.clone(); + let vertical_scroll_state = state.vertical_scroll_state.clone(); + let vertical_scroll_handle = state.vertical_scroll_handle.clone(); + let horizontal_scroll_state = state.horizontal_scroll_state.clone(); + let horizontal_scroll_handle = state.horizontal_scroll_handle.clone(); + let fixed_head_cols_bounds = state.fixed_head_cols_bounds; + let columns_count = state.delegate.columns_count(cx); + let left_columns_count = state .col_groups .iter() - .filter(|col| self.col_fixed && col.column.fixed == Some(ColumnFixed::Left)) + .filter(|col| state.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_count = self.calculate_extra_rows_needed(rows_count); + let rows_count = state.delegate.rows_count(cx); + let loading = state.delegate.loading(cx); + let extra_rows_count = state.calculate_extra_rows_needed(rows_count); let render_rows_count = if self.stripe { rows_count + extra_rows_count } else { rows_count }; + let right_clicked_row = state.right_clicked_row; - let inner_table = v_flex() - .key_context("Table") - .id("table") - .track_focus(&self.focus_handle) - .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_col)) - .on_action(cx.listener(Self::action_select_prev_col)) - .size_full() - .overflow_hidden() - .child(self.render_table_head(left_columns_count, window, cx)) - .context_menu({ - let view = view.clone(); - move |this, window: &mut Window, cx: &mut Context| { - if let Some(row_ix) = view.read(cx).right_clicked_row { - view.read(cx) - .delegate - .context_menu(row_ix, this, window, cx) - } else { - this - } - } - }) - .map(|this| { - if rows_count == 0 { - this.child( - div() - .size_full() - .child(self.delegate.render_empty(window, cx)), - ) - } else { - this.child( - h_flex().id("table-body").flex_grow().size_full().child( - uniform_list( - "table-uniform-list", - render_rows_count, - cx.processor( - move |table, visible_range: Range, window, cx| { - // We must calculate the col sizes here, because the col sizes - // need render_th first, then that method will set the bounds of each col. - let col_sizes: Rc>> = Rc::new( - table - .col_groups - .iter() - .skip(left_columns_count) - .map(|col| col.bounds.size) - .collect(), - ); + let mut empty_view = None; + let mut loading_view = None; + let mut inner_table = None; + self.state.update(cx, |state, cx| { + state.size = self.size; + state.stripe = self.stripe; + state.measure(window, cx); - table.load_more_if_need( - rows_count, - visible_range.end, - window, - cx, - ); - table.update_visible_range_if_need( - visible_range.clone(), - Axis::Vertical, - window, - cx, - ); + if loading { + loading_view = Some( + state + .delegate + .render_loading(self.size, window, cx) + .into_any_element(), + ) + } - if visible_range.end > rows_count { - table.scroll_to_row( - std::cmp::min( - visible_range.start, - rows_count.saturating_sub(1), - ), - cx, - ); - } + if rows_count == 0 { + empty_view = Some( + div() + .size_full() + .child(state.delegate.render_empty(window, cx)) + .into_any_element(), + ) + } - let mut items = Vec::with_capacity( - visible_range.end.saturating_sub(visible_range.start), - ); + inner_table = Some( + v_flex() + .id("table") + .key_context(CONTEXT) + .track_focus(&focus_handle) + .on_action(cx.listener(TableState::action_cancel)) + .on_action(cx.listener(TableState::action_select_next)) + .on_action(cx.listener(TableState::action_select_prev)) + .on_action(cx.listener(TableState::action_select_next_col)) + .on_action(cx.listener(TableState::action_select_prev_col)) + .size_full() + .overflow_hidden() + .child(state.render_table_head(left_columns_count, window, cx)) + .context_menu({ + let view = cx.entity().clone(); + move |this, window: &mut Window, cx: &mut Context| { + if let Some(row_ix) = view.read(cx).right_clicked_row { + view.read(cx) + .delegate + .context_menu(row_ix, this, window, cx) + } else { + this + } + } + }) + .map(|this| { + if rows_count == 0 { + this.children(empty_view) + } else { + this.child( + h_flex().id("table-body").flex_grow().size_full().child( + uniform_list( + "table-uniform-list", + render_rows_count, + cx.processor( + move |table, + visible_range: Range, + window, + cx| { + // We must calculate the col sizes here, because the col sizes + // need render_th first, then that method will set the bounds of each col. + let col_sizes: Rc>> = + Rc::new( + table + .col_groups + .iter() + .skip(left_columns_count) + .map(|col| col.bounds.size) + .collect(), + ); - // Render fake rows to fill the table - visible_range.for_each(|row_ix| { - // Render real rows for available data - items.push(table.render_table_row( - row_ix, - rows_count, - left_columns_count, - col_sizes.clone(), - columns_count, - extra_rows_count, - window, - cx, - )); - }); + table.load_more_if_need( + rows_count, + visible_range.end, + window, + cx, + ); + table.update_visible_range_if_need( + visible_range.clone(), + Axis::Vertical, + window, + cx, + ); - items - }, + if visible_range.end > rows_count { + table.scroll_to_row( + std::cmp::min( + visible_range.start, + rows_count.saturating_sub(1), + ), + cx, + ); + } + + let mut items = Vec::with_capacity( + visible_range + .end + .saturating_sub(visible_range.start), + ); + + // Render fake rows to fill the table + visible_range.for_each(|row_ix| { + // Render real rows for available data + items.push(table.render_table_row( + row_ix, + rows_count, + left_columns_count, + col_sizes.clone(), + columns_count, + extra_rows_count, + window, + cx, + )); + }); + + items + }, + ), + ) + .flex_grow() + .size_full() + .with_sizing_behavior(ListSizingBehavior::Auto) + .track_scroll(vertical_scroll_handle.clone()) + .into_any_element(), ), ) - .flex_grow() - .size_full() - .with_sizing_behavior(ListSizingBehavior::Auto) - .track_scroll(vertical_scroll_handle) - .into_any_element(), - ), - ) - } - }); + } + }), + ); + }); - let view = cx.entity().clone(); + let view = self.state.clone(); div() .size_full() .when(self.border, |this| { @@ -1451,21 +1507,22 @@ where .border_color(cx.theme().border) }) .bg(cx.theme().table) - .when(loading, |this| { - this.child(self.delegate().render_loading(self.size, window, cx)) - }) + .children(loading_view) .when(!loading, |this| { - this.child(inner_table) + this.children(inner_table) .child(ScrollableMask::new( - cx.entity().entity_id(), + self.state.entity_id(), Axis::Horizontal, &horizontal_scroll_handle, )) - .when(self.right_clicked_row.is_some(), |this| { - this.on_mouse_down_out(cx.listener(|this, _, _, cx| { - this.right_clicked_row = None; - cx.notify(); - })) + .when(right_clicked_row.is_some(), |this| { + this.on_mouse_down_out(window.listener_for( + &self.state, + |this, _, _, cx| { + this.right_clicked_row = None; + cx.notify(); + }, + )) }) }) .child(canvas( @@ -1479,10 +1536,23 @@ where .top_0() .size_full() .when(self.scrollbar_visible.bottom, |this| { - this.child(self.render_horizontal_scrollbar(window, cx)) + this.child(self.render_horizontal_scrollbar( + &self.state, + fixed_head_cols_bounds, + &horizontal_scroll_state, + &horizontal_scroll_handle, + window, + cx, + )) }) .when(self.scrollbar_visible.right && rows_count > 0, |this| { - this.children(self.render_vertical_scrollbar(window, cx)) + this.children(self.render_vertical_scrollbar( + &self.state, + &vertical_scroll_state, + &vertical_scroll_handle, + window, + cx, + )) }), ) }) diff --git a/crates/ui/src/theme/registry.rs b/crates/ui/src/theme/registry.rs index d0e54e7c..9d6ffd9c 100644 --- a/crates/ui/src/theme/registry.rs +++ b/crates/ui/src/theme/registry.rs @@ -11,7 +11,7 @@ use std::{ }; const DEFAULT_THEME: &str = include_str!("./default-theme.json"); -pub(crate) const DEFAULT_THEME_COLORS: LazyLock< +pub(crate) static DEFAULT_THEME_COLORS: LazyLock< HashMap, Arc)>, > = LazyLock::new(|| { let mut colors = HashMap::new(); diff --git a/docs/docs/components/list.md b/docs/docs/components/list.md index ffb97da7..2d65edae 100644 --- a/docs/docs/components/list.md +++ b/docs/docs/components/list.md @@ -10,7 +10,7 @@ A powerful List component that provides a virtualized, searchable list interface ## Import ```rust -use gpui_component::list::{List, ListDelegate, ListItem, ListEvent, ListSeparatorItem}; +use gpui_component::list::{List, ListState, ListDelegate, ListItem, ListEvent, ListSeparatorItem}; use gpui_component::IndexPath; ``` @@ -37,7 +37,7 @@ impl ListDelegate for MyListDelegate { &self, ix: IndexPath, _window: &mut Window, - _cx: &mut Context>, + _cx: &mut App, ) -> Option { self.items.get(ix.row).map(|item| { ListItem::new(ix) @@ -50,7 +50,7 @@ impl ListDelegate for MyListDelegate { &mut self, ix: Option, _window: &mut Window, - cx: &mut Context>, + cx: &mut Context>, ) { self.selected_index = ix; cx.notify(); @@ -63,7 +63,14 @@ let delegate = MyListDelegate { selected_index: None, }; -let list = cx.new(|cx| List::new(delegate, window, cx)); +/// Create a list state. +let state = cx.new(|cx| ListState::new(delegate, window, cx)); +``` + +Now use [List] to render list: + +```rs +div().child(List::new(&state)) ``` ### List with Sections @@ -89,7 +96,7 @@ impl ListDelegate for MyListDelegate { &self, section: usize, _window: &mut Window, - cx: &mut Context>, + cx: &mut App, ) -> Option { let title = match section { 0 => "Section 1", @@ -114,7 +121,7 @@ impl ListDelegate for MyListDelegate { &self, section: usize, _window: &mut Window, - cx: &mut Context>, + cx: &mut App, ) -> Option { Some( div() @@ -135,7 +142,7 @@ fn render_item( &self, ix: IndexPath, _window: &mut Window, - cx: &mut Context>, + cx: &mut App, ) -> Option { self.items.get(ix.row).map(|item| { ListItem::new(ix) @@ -170,7 +177,7 @@ impl ListDelegate for MyListDelegate { &mut self, query: &str, _window: &mut Window, - _cx: &mut Context>, + _cx: &mut Context>, ) -> Task<()> { // Filter items based on query self.filtered_items = self.all_items @@ -184,7 +191,7 @@ impl ListDelegate for MyListDelegate { } // Create list without search input -let list = cx.new(|cx| List::new(delegate, window, cx).no_query()); +let state = cx.new(|cx| ListState::new(delegate, window, cx).no_query()); ``` ### List with Loading State @@ -198,7 +205,7 @@ impl ListDelegate for MyListDelegate { fn render_loading( &self, _window: &mut Window, - _cx: &mut Context>, + _cx: &mut App, ) -> impl IntoElement { // Custom loading view v_flex() @@ -223,7 +230,7 @@ impl ListDelegate for MyListDelegate { 20 // Trigger when 20 items from bottom } - fn load_more(&mut self, window: &mut Window, cx: &mut Context>) { + fn load_more(&mut self, window: &mut Window, cx: &mut Context>) { if self.is_loading { return; } @@ -248,7 +255,7 @@ impl ListDelegate for MyListDelegate { ```rust // Subscribe to list events -let _subscription = cx.subscribe(&list, |_, _, event: &ListEvent, _| { +let _subscription = cx.subscribe(&state, |_, _, event: &ListEvent, _| { match event { ListEvent::Select(ix) => { println!("Item selected at: {:?}", ix); @@ -296,7 +303,7 @@ ListSeparatorItem::new() ```rust impl ListDelegate for MyListDelegate { - fn render_empty(&self, _window: &mut Window, cx: &mut Context>) -> impl IntoElement { + fn render_empty(&self, _window: &mut Window, cx: &mut App) -> impl IntoElement { v_flex() .size_full() .justify_center() @@ -321,11 +328,9 @@ impl ListDelegate for MyListDelegate { ### List Configuration ```rust -List::new(delegate, window, cx) +List::new(&state) .max_h(px(400.)) // Set maximum height .scrollbar_visible(false) // Hide scrollbar - .selectable(false) // Disable selection - .no_query() // Remove search input .paddings(Edges::all(px(8.))) // Set internal padding ``` @@ -333,8 +338,8 @@ List::new(delegate, window, cx) ```rust // Scroll to specific item -list.update(cx, |list, cx| { - list.scroll_to_item( +state.update(cx, |state, cx| { + state.scroll_to_item( IndexPath::new(0).section(1), // Row 0 of section 1 ScrollStrategy::Center, window, @@ -343,13 +348,13 @@ list.update(cx, |list, cx| { }); // Scroll to selected item -list.update(cx, |list, cx| { - list.scroll_to_selected_item(window, cx); +state.update(cx, |state, cx| { + state.scroll_to_selected_item(window, cx); }); // Set selected index without scrolling -list.update(cx, |list, cx| { - list.set_selected_index(Some(IndexPath::new(5)), window, cx); +state.update(cx, |state, cx| { + state.set_selected_index(Some(IndexPath::new(5)), window, cx); }); ``` @@ -373,7 +378,7 @@ struct FileInfo { impl ListDelegate for FileBrowserDelegate { type Item = ListItem; - fn render_item(&self, ix: IndexPath, window: &mut Window, cx: &mut Context>) -> Option { + fn render_item(&self, ix: IndexPath, window: &mut Window, cx: &mut App) -> Option { self.files.get(ix.row).map(|file| { let icon = if file.is_directory { IconName::Folder @@ -423,7 +428,7 @@ impl ListDelegate for ContactListDelegate { self.contacts_by_letter.len() } - fn render_section_header(&self, section: usize, _window: &mut Window, cx: &mut Context>) -> Option { + fn render_section_header(&self, section: usize, _window: &mut Window, cx: &mut App) -> Option { let letter = self.contacts_by_letter.keys().nth(section)?; Some( diff --git a/docs/docs/components/table.md b/docs/docs/components/table.md index c89bf63a..a97c1b16 100644 --- a/docs/docs/components/table.md +++ b/docs/docs/components/table.md @@ -10,14 +10,14 @@ A comprehensive data table component designed for handling large datasets with h ## Import ```rust -use gpui_component::table::{Table, TableDelegate, Column, ColumnSort, ColumnFixed, TableEvent}; +use gpui_component::table::{Table, TableState, TableDelegate, Column, ColumnSort, ColumnFixed, TableEvent}; ``` ## Usage ### Basic Table -To create a table, you need to implement the `TableDelegate` trait and provide column definitions: +To create a table, you need to implement the `TableDelegate` trait and provide column definitions, and use `TableState` to manage the table state. ```rust use std::ops::Range; @@ -66,7 +66,7 @@ impl TableDelegate for MyTableDelegate { &self.columns[col_ix] } - fn render_td(&self, row_ix: usize, col_ix: usize, _: &mut Window, _: &mut Context>) -> impl IntoElement { + fn render_td(&self, row_ix: usize, col_ix: usize, _: &mut Window, _: &mut App) -> impl IntoElement { let row = &self.data[row_ix]; let col = &self.columns[col_ix]; @@ -82,7 +82,7 @@ impl TableDelegate for MyTableDelegate { // Create the table let delegate = MyTableDelegate::new(); -let table = cx.new(|cx| Table::new(delegate, window, cx)); +let state = cx.new(|cx| TableState::new(delegate, window, cx)); ``` ### Column Configuration @@ -143,7 +143,7 @@ impl TableDelegate for LargeDataDelegate { } // Only visible rows are rendered - fn render_td(&self, row_ix: usize, col_ix: usize, _: &mut Window, _: &mut Context>) -> impl IntoElement { + fn render_td(&self, row_ix: usize, col_ix: usize, _: &mut Window, _: &mut Context>) -> impl IntoElement { // This is only called for visible rows // Efficiently render cell content let row = &self.data[row_ix]; @@ -151,7 +151,7 @@ impl TableDelegate for LargeDataDelegate { } // Track visible range for optimizations - fn visible_rows_changed(&mut self, visible_range: Range, _: &mut Window, _: &mut Context>) { + fn visible_rows_changed(&mut self, visible_range: Range, _: &mut Window, _: &mut Context>) { // Only update data for visible rows if needed // This is called when user scrolls } @@ -164,7 +164,7 @@ Implement sorting in your delegate: ```rust impl TableDelegate for MyTableDelegate { - fn perform_sort(&mut self, col_ix: usize, sort: ColumnSort, _: &mut Window, _: &mut Context>) { + fn perform_sort(&mut self, col_ix: usize, sort: ColumnSort, _: &mut Window, _: &mut Context>) { let col = &self.columns[col_ix]; match col.key.as_ref() { @@ -197,16 +197,16 @@ Handle row selection and interaction: ```rust impl TableDelegate for MyTableDelegate { - fn render_tr(&self, row_ix: usize, _: &mut Window, cx: &mut Context>) -> gpui::Stateful { + fn render_tr(&self, row_ix: usize, _: &mut Window, cx: &mut App) -> Stateful
{ div() .id(row_ix) - .on_click(cx.listener(move |_, ev, _, _| { + .on_click(move |ev, _, _| { if ev.modifiers().secondary() { println!("Right-clicked row {}", row_ix); } else { println!("Selected row {}", row_ix); } - })) + }) } // Context menu for right-click @@ -220,7 +220,7 @@ impl TableDelegate for MyTableDelegate { } // Handle table events -cx.subscribe_in(&table, window, |view, table, event, _, cx| { +cx.subscribe_in(&state, window, |view, table, event, _, cx| { match event { TableEvent::SelectRow(row_ix) => { println!("Row {} selected", row_ix); @@ -243,7 +243,7 @@ Create rich cell content with custom rendering: ```rust impl TableDelegate for MyTableDelegate { - fn render_td(&self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut Context>) -> impl IntoElement { + fn render_td(&self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut App) -> impl IntoElement { let row = &self.data[row_ix]; let col = &self.columns[col_ix]; @@ -316,8 +316,8 @@ Enable dynamic column management: ```rust // Configure table features -let table = cx.new(|cx| { - Table::new(delegate, window, cx) +let state = cx.new(|cx| { + TableState::new(delegate, window, cx) .col_resizable(true) // Allow column resizing .col_movable(true) // Allow column reordering .sortable(true) // Enable sorting @@ -326,7 +326,7 @@ let table = cx.new(|cx| { }); // Listen for column changes -cx.subscribe_in(&table, window, |view, table, event, _, cx| { +cx.subscribe_in(&state, window, |view, table, event, _, cx| { match event { TableEvent::ColumnWidthsChanged(widths) => { // Save column widths to user preferences @@ -355,7 +355,7 @@ impl TableDelegate for MyTableDelegate { 50 // Load more when 50 rows from bottom } - fn load_more(&mut self, _: &mut Window, cx: &mut Context>) { + fn load_more(&mut self, _: &mut Window, cx: &mut Context>) { if self.loading { return; // Prevent multiple loads } @@ -388,17 +388,15 @@ impl TableDelegate for MyTableDelegate { Customize table appearance: ```rust -let table = cx.new(|cx| { - Table::new(delegate, window, cx) - .stripe(true) // Alternating row colors - .border(true) // Border around table - .scrollbar_visible(true, true) // Vertical, horizontal scrollbars +let state = cx.new(|cx| { + TableState::new(delegate, window, cx) }); -// Set table size -table.update(cx, |table, cx| { - table.set_size(Size::Small, cx); -}); +// In render +Table::new(&state) + .stripe(true) // Alternating row colors + .border(true) // Border around table + .scrollbar_visible(true, true) // Vertical, horizontal scrollbars ``` ## Examples @@ -415,7 +413,7 @@ struct StockData { } impl TableDelegate for StockTableDelegate { - fn render_td(&self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut Context>) -> impl IntoElement { + fn render_td(&self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut App) -> impl IntoElement { let stock = &self.stocks[row_ix]; let col = &self.columns[col_ix];