use std::{ ops::Range, sync::LazyLock, time::{self, Duration}, }; use fake::Fake; use gpui::{ Action, AnyElement, App, AppContext, ClickEvent, Context, Entity, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, Timer, Window, div, prelude::FluentBuilder as _, }; use gpui_component::{ ActiveTheme as _, Selectable, Sizable as _, Size, StyleSized as _, StyledExt, button::Button, checkbox::Checkbox, h_flex, indicator::Indicator, input::{Input, InputEvent, InputState}, label::Label, menu::{DropdownMenu, PopupMenu}, table::{Column, ColumnFixed, ColumnSort, Table, TableDelegate, TableEvent}, v_flex, }; use serde::{Deserialize, Serialize}; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = table_story, no_json)] struct ChangeSize(Size); #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = table_story, no_json)] struct OpenDetail(usize); #[derive(Debug, Clone, Serialize, Deserialize, Default)] struct Counter { symbol: SharedString, market: SharedString, name: SharedString, } static ALL_COUNTERS: LazyLock> = LazyLock::new(|| serde_json::from_str(include_str!("./fixtures/counters.json")).unwrap()); impl Counter { fn random() -> Self { let len = ALL_COUNTERS.len(); let ix = rand::random::() % len; ALL_COUNTERS[ix].clone() } fn symbol_code(&self) -> SharedString { format!("{}.{}", self.symbol, self.market).into() } } #[derive(Clone, Debug, Default)] struct Stock { id: usize, counter: Counter, price: f64, change: f64, change_percent: f64, volume: f64, turnover: f64, market_cap: f64, ttm: f64, five_mins_ranking: f64, th60_days_ranking: f64, year_change_percent: f64, bid: f64, bid_volume: f64, ask: f64, ask_volume: f64, open: f64, prev_close: f64, high: f64, low: f64, turnover_rate: f64, rise_rate: f64, amplitude: f64, pe_status: f64, pb_status: f64, volume_ratio: f64, bid_ask_ratio: f64, latest_pre_close: f64, latest_post_close: f64, pre_market_cap: f64, pre_market_percent: f64, pre_market_change: f64, post_market_cap: f64, post_market_percent: f64, post_market_change: f64, float_cap: f64, shares: i64, shares_float: i64, day_5_ranking: f64, day_10_ranking: f64, day_30_ranking: f64, day_120_ranking: f64, day_250_ranking: f64, } impl Stock { fn random_update(&mut self) { self.price = (-300.0..999.999).fake::(); self.change = (-0.1..5.0).fake::(); self.change_percent = (-0.1..0.1).fake::(); self.volume = (-300.0..999.999).fake::(); self.turnover = (-300.0..999.999).fake::(); self.market_cap = (-1000.0..9999.999).fake::(); self.ttm = (-1000.0..9999.999).fake::(); self.five_mins_ranking = self.five_mins_ranking * (1.0 + (-0.2..0.2).fake::()); self.bid = self.price * (1.0 + (-0.2..0.2).fake::()); self.bid_volume = (100.0..1000.0).fake::(); self.ask = self.price * (1.0 + (-0.2..0.2).fake::()); self.ask_volume = (100.0..1000.0).fake::(); self.bid_ask_ratio = self.bid / self.ask; self.volume_ratio = self.volume / self.turnover; self.high = self.price * (1.0 + (0.0..1.5).fake::()); self.low = self.price * (1.0 + (-1.5..0.0).fake::()); } } fn random_stocks(size: usize) -> Vec { (0..size) .map(|id| Stock { id, counter: Counter::random(), change: (-100.0..100.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(), ttm: (0.0..1000.0).fake(), five_mins_ranking: (0.0..1000.0).fake(), th60_days_ranking: (0.0..1000.0).fake(), year_change_percent: (-1.0..1.0).fake(), bid: (0.0..1000.0).fake(), bid_volume: (0.0..1000.0).fake(), ask: (0.0..1000.0).fake(), ask_volume: (0.0..1000.0).fake(), open: (0.0..1000.0).fake(), prev_close: (0.0..1000.0).fake(), high: (0.0..1000.0).fake(), low: (0.0..1000.0).fake(), turnover_rate: (0.0..1.0).fake(), rise_rate: (0.0..1.0).fake(), amplitude: (0.0..1000.0).fake(), pe_status: (0.0..1000.0).fake(), pb_status: (0.0..1000.0).fake(), volume_ratio: (0.0..1.0).fake(), bid_ask_ratio: (0.0..1.0).fake(), latest_pre_close: (0.0..1000.0).fake(), latest_post_close: (0.0..1000.0).fake(), pre_market_cap: (0.0..1000.0).fake(), pre_market_percent: (-1.0..1.0).fake(), pre_market_change: (-100.0..100.0).fake(), post_market_cap: (0.0..1000.0).fake(), post_market_percent: (-1.0..1.0).fake(), post_market_change: (-100.0..100.0).fake(), float_cap: (0.0..1000.0).fake(), shares: (100000..9999999).fake(), shares_float: (100000..9999999).fake(), day_5_ranking: (0.0..1000.0).fake(), day_10_ranking: (0.0..1000.0).fake(), day_30_ranking: (0.0..1000.0).fake(), day_120_ranking: (0.0..1000.0).fake(), day_250_ranking: (0.0..1000.0).fake(), ..Default::default() }) .collect() } struct StockTableDelegate { stocks: Vec, columns: Vec, size: Size, loading: bool, full_loading: bool, eof: bool, visible_rows: Range, visible_cols: Range, _load_task: Task<()>, } impl StockTableDelegate { fn new(size: usize) -> Self { Self { size: Size::default(), stocks: random_stocks(size), columns: vec![ Column::new("id", "ID") .width(60.) .fixed(ColumnFixed::Left) .resizable(false), 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%") .sortable() .text_right() .p_0(), Column::new("volume", "Volume").p_0(), Column::new("turnover", "Turnover").p_0(), Column::new("market_cap", "Market Cap").p_0(), Column::new("ttm", "TTM").p_0(), Column::new("five_mins_ranking", "5m Ranking") .text_right() .p_0(), Column::new("th60_days_ranking", "60d Ranking"), Column::new("year_change_percent", "Year Chg%"), Column::new("bid", "Bid").text_right().p_0(), Column::new("bid_volume", "Bid Vol").text_right().p_0(), Column::new("ask", "Ask").text_right().p_0(), Column::new("ask_volume", "Ask Vol").text_right().p_0(), Column::new("open", "Open").text_right().p_0(), Column::new("prev_close", "Prev Close").text_right().p_0(), Column::new("high", "High").text_right().p_0(), Column::new("low", "Low").text_right().p_0(), Column::new("turnover_rate", "Turnover Rate"), Column::new("rise_rate", "Rise Rate"), Column::new("amplitude", "Amplitude"), Column::new("pe_status", "P/E"), Column::new("pb_status", "P/B"), Column::new("volume_ratio", "Volume Ratio") .text_right() .p_0(), Column::new("bid_ask_ratio", "Bid Ask Ratio") .text_right() .p_0(), Column::new("latest_pre_close", "Latest Pre Close"), Column::new("latest_post_close", "Latest Post Close"), Column::new("pre_market_cap", "Pre Mkt Cap"), Column::new("pre_market_percent", "Pre Mkt%"), Column::new("pre_market_change", "Pre Mkt Chg"), Column::new("post_market_cap", "Post Mkt Cap"), Column::new("post_market_percent", "Post Mkt%"), Column::new("post_market_change", "Post Mkt Chg"), Column::new("float_cap", "Float Cap"), Column::new("shares", "Shares"), Column::new("shares_float", "Float Shares"), Column::new("day_5_ranking", "5d Ranking"), Column::new("day_10_ranking", "10d Ranking"), Column::new("day_30_ranking", "30d Ranking"), Column::new("day_120_ranking", "120d Ranking"), Column::new("day_250_ranking", "250d Ranking"), ], loading: false, full_loading: false, eof: false, visible_cols: Range::default(), visible_rows: Range::default(), _load_task: Task::ready(()), } } fn update_stocks(&mut self, size: usize) { self.stocks = random_stocks(size); self.eof = size <= 50; self.loading = false; 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, val: f64, cx: &mut Context>, ) -> AnyElement { let this = div() .h_full() .table_cell_size(self.size) .child(format!("{:.3}", val)); // Val is a 0.0 .. n.0 // 30% to red, 30% to green, others to default let right_num = ((val - val.floor()) * 1000.).floor() as i32; let 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 }; this.when(col.align == TextAlign::Right, |this| { this.h_flex().justify_end() }) .into_any_element() } } impl TableDelegate for StockTableDelegate { fn columns_count(&self, _: &App) -> usize { self.columns.len() } fn rows_count(&self, _: &App) -> usize { self.stocks.len() } fn column(&self, col_ix: usize, _cx: &App) -> &Column { &self.columns[col_ix] } fn render_th( &self, col_ix: usize, _: &mut Window, _: &mut Context>, ) -> impl IntoElement { let col = self.columns.get(col_ix).unwrap(); div() .child(col.name.clone()) .when(col_ix >= 3 && col_ix <= 10, |this| { this.table_cell_size(self.size) }) .when(col.align == TextAlign::Right, |this| { this.h_flex().w_full().justify_end() }) } fn context_menu( &self, row_ix: usize, menu: PopupMenu, _window: &Window, _cx: &App, ) -> PopupMenu { menu.menu( format!("Selected Row: {}", row_ix), Box::new(OpenDetail(row_ix)), ) .separator() .menu("Size Large", Box::new(ChangeSize(Size::Large))) .menu("Size Medium", Box::new(ChangeSize(Size::Medium))) .menu("Size Small", Box::new(ChangeSize(Size::Small))) .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() ) })) } /// NOTE: Performance metrics /// /// last render 561 cells total: 232.745µs, avg: 414ns /// frame duration: 8.825083ms /// /// This is means render the full table cells takes 232.745µs. Then 232.745µs / 8.82ms = 2.6% of the frame duration. /// /// If we improve the td rendering, we can reduce the time to render the full table cells. fn render_td( &self, row_ix: usize, col_ix: usize, _: &mut Window, cx: &mut Context>, ) -> impl IntoElement { let stock = self.stocks.get(row_ix).unwrap(); let col = self.columns.get(col_ix).unwrap(); match col.key.as_ref() { "id" => stock.id.to_string().into_any_element(), "market" => div() .map(|this| { if stock.counter.market == "US" { this.text_color(cx.theme().blue) } else { this.text_color(cx.theme().magenta) } }) .child(stock.counter.market.clone()) .into_any_element(), "symbol" => stock.counter.symbol_code().into_any_element(), "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_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), "ttm" => self.render_value_cell(&col, stock.ttm, cx), "five_mins_ranking" => self.render_value_cell(&col, stock.five_mins_ranking, cx), "th60_days_ranking" => stock .th60_days_ranking .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), "ask_volume" => self.render_value_cell(&col, stock.ask_volume, cx), "open" => self.render_value_cell(&col, stock.open, cx), "prev_close" => self.render_value_cell(&col, stock.prev_close, cx), "high" => self.render_value_cell(&col, stock.high, cx), "low" => self.render_value_cell(&col, stock.low, cx), "turnover_rate" => (stock.turnover_rate * 100.0) .floor() .to_string() .into_any_element(), "rise_rate" => (stock.rise_rate * 100.0) .floor() .to_string() .into_any_element(), "amplitude" => (stock.amplitude * 100.0) .floor() .to_string() .into_any_element(), "pe_status" => stock.pe_status.floor().to_string().into_any_element(), "pb_status" => stock.pb_status.floor().to_string().into_any_element(), "volume_ratio" => self.render_value_cell(&col, stock.volume_ratio, cx), "bid_ask_ratio" => self.render_value_cell(&col, stock.bid_ask_ratio, cx), "latest_pre_close" => stock .latest_pre_close .floor() .to_string() .into_any_element(), "latest_post_close" => stock .latest_post_close .floor() .to_string() .into_any_element(), "pre_market_cap" => stock.pre_market_cap.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" => self.render_percent(&col, stock.post_market_percent, cx), "post_market_change" => stock .post_market_change .floor() .to_string() .into_any_element(), "float_cap" => stock.float_cap.floor().to_string().into_any_element(), "shares" => stock.shares.to_string().into_any_element(), "shares_float" => stock.shares_float.to_string().into_any_element(), "day_5_ranking" => stock.day_5_ranking.floor().to_string().into_any_element(), "day_10_ranking" => stock.day_10_ranking.floor().to_string().into_any_element(), "day_30_ranking" => stock.day_30_ranking.floor().to_string().into_any_element(), "day_120_ranking" => stock.day_120_ranking.floor().to_string().into_any_element(), "day_250_ranking" => stock.day_250_ranking.floor().to_string().into_any_element(), _ => "--".to_string().into_any_element(), } } fn move_column( &mut self, col_ix: usize, to_ix: usize, _: &mut Window, _: &mut Context>, ) { let col = self.columns.remove(col_ix); self.columns.insert(to_ix, col); } fn perform_sort( &mut self, col_ix: usize, sort: ColumnSort, _: &mut Window, _: &mut Context>, ) { if let Some(col) = self.columns.get_mut(col_ix) { match col.key.as_ref() { "id" => self.stocks.sort_by(|a, b| match sort { ColumnSort::Descending => b.id.cmp(&a.id), _ => a.id.cmp(&b.id), }), "symbol" => self.stocks.sort_by(|a, b| match sort { 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 { ColumnSort::Descending => b .change .partial_cmp(&a.change) .unwrap_or(std::cmp::Ordering::Equal), _ => a.id.cmp(&b.id), }), _ => {} } } } fn loading(&self, _: &App) -> bool { self.full_loading } fn is_eof(&self, _: &App) -> bool { return !self.loading && !self.eof; } fn load_more_threshold(&self) -> usize { 150 } fn load_more(&mut self, _: &mut Window, cx: &mut Context>) { self.loading = true; self._load_task = cx.spawn(async move |view, cx| { // Simulate network request, delay 1s to load data. Timer::after(Duration::from_secs(1)).await; _ = cx.update(|cx| { let _ = view.update(cx, |view, _| { view.delegate_mut().stocks.extend(random_stocks(200)); view.delegate_mut().loading = false; view.delegate_mut().eof = view.delegate().stocks.len() >= 6000; }); }); }); } fn visible_rows_changed( &mut self, visible_range: Range, _: &mut Window, _: &mut Context>, ) { self.visible_rows = visible_range; } fn visible_columns_changed( &mut self, visible_range: Range, _: &mut Window, _: &mut Context>, ) { self.visible_cols = visible_range; } } pub struct TableStory { table: Entity>, num_stocks_input: Entity, stripe: bool, refresh_data: bool, size: Size, _subscriptions: Vec, _load_task: Task<()>, } impl super::Story for TableStory { fn title() -> &'static str { "Table" } fn description() -> &'static str { "A complex data table with selection, sorting, column moving, and loading more." } fn new_view(window: &mut Window, cx: &mut App) -> Entity { Self::view(window, cx) } fn closable() -> bool { false } } impl Focusable for TableStory { fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { self.table.focus_handle(cx) } } impl TableStory { pub fn view(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Self::new(window, cx)) } fn new(window: &mut Window, cx: &mut Context) -> Self { // Create the number input field with validation for positive integers let num_stocks_input = cx.new(|cx| { let mut input = InputState::new(window, cx) .placeholder("Enter number of Stocks to display") .validate(|s, _| s.parse::().is_ok()); input.set_value("5000", window, cx); input }); let delegate = StockTableDelegate::new(5000); let table = cx.new(|cx| Table::new(delegate, window, cx)); let _subscriptions = vec![ cx.subscribe_in(&table, window, Self::on_table_event), cx.subscribe_in(&num_stocks_input, window, Self::on_num_stocks_input_change), // Spawn a background to random refresh the list ]; let _load_task = cx.spawn(async move |this, cx| { loop { Timer::after(time::Duration::from_millis(33)).await; this.update(cx, |this, cx| { if !this.refresh_data { return; } this.table.update(cx, |table, _| { table.delegate_mut().stocks.iter_mut().enumerate().for_each( |(i, stock)| { let n = (3..10).fake::(); // update 30% of the stocks if i % n == 0 { stock.random_update(); } }, ); }); cx.notify(); }) .ok(); } }); Self { table, num_stocks_input, stripe: false, refresh_data: false, size: Size::default(), _subscriptions, _load_task, } } // Event handler for changes in the number input field fn on_num_stocks_input_change( &mut self, _: &Entity, event: &InputEvent, _: &mut Window, cx: &mut Context, ) { match event { // Update when the user presses Enter or the input loses focus InputEvent::PressEnter { .. } | InputEvent::Blur => { let text = self.num_stocks_input.read(cx).value().to_string(); if let Ok(total_count) = text.parse::() { if total_count == self.table.read(cx).delegate().stocks.len() { return; } self.table.update(cx, |table, _| { table.delegate_mut().update_stocks(total_count); }); cx.notify(); } } _ => {} } } fn toggle_loop_selection(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.table.update(cx, |table, cx| { table.loop_selection = *checked; cx.notify(); }); } fn toggle_col_resize(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.table.update(cx, |table, 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.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.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.col_selectable = *checked; cx.notify(); }); } 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(); }); } 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; }); } fn toggle_refresh_data(&mut self, checked: &bool, _: &mut Window, cx: &mut Context) { self.refresh_data = *checked; cx.notify(); } fn on_table_event( &mut self, _: &Entity>, event: &TableEvent, _window: &mut Window, _cx: &mut Context, ) { match event { TableEvent::ColumnWidthsChanged(col_widths) => { println!("Column widths changed: {:?}", col_widths) } TableEvent::SelectColumn(ix) => println!("Select col: {}", ix), TableEvent::DoubleClickedRow(ix) => println!("Double clicked row: {}", ix), TableEvent::SelectRow(ix) => println!("Select row: {}", ix), TableEvent::MoveColumn(origin_idx, target_idx) => { println!("Move col index: {} -> {}", origin_idx, target_idx); } } } } impl Render for TableStory { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { let table = &self.table.read(cx); let delegate = table.delegate(); let rows_count = delegate.rows_count(cx); let size = self.size; v_flex() .on_action(cx.listener(Self::on_change_size)) .size_full() .text_sm() .gap_4() .child( h_flex() .items_center() .gap_3() .flex_wrap() .child( Checkbox::new("loop-selection") .label("Loop Selection") .selected(table.loop_selection) .on_click(cx.listener(Self::toggle_loop_selection)), ) .child( Checkbox::new("col-resize") .label("Column Resize") .selected(table.col_resizable) .on_click(cx.listener(Self::toggle_col_resize)), ) .child( Checkbox::new("col-order") .label("Column Order") .selected(table.col_movable) .on_click(cx.listener(Self::toggle_col_order)), ) .child( Checkbox::new("col-sort") .label("Sortable") .selected(table.sortable) .on_click(cx.listener(Self::toggle_col_sort)), ) .child( Checkbox::new("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("loading") .label("Loading") .checked(self.table.read(cx).delegate().full_loading) .on_click(cx.listener(|this, check: &bool, _, cx| { this.table.update(cx, |this, cx| { this.delegate_mut().full_loading = *check; cx.notify(); }) })), ) .child( Checkbox::new("refresh-data") .label("Refresh Data") .selected(self.refresh_data) .on_click(cx.listener(Self::toggle_refresh_data)), ), ) .child( h_flex() .gap_2() .child( Button::new("size") .outline() .small() .label(format!("size: {:?}", self.size)) .dropdown_menu(move |menu, _, _| { menu.menu_with_check( "Large", size == Size::Large, Box::new(ChangeSize(Size::Large)), ) .menu_with_check( "Medium", size == Size::Medium, Box::new(ChangeSize(Size::Medium)), ) .menu_with_check( "Small", size == Size::Small, Box::new(ChangeSize(Size::Small)), ) .menu_with_check( "XSmall", size == Size::XSmall, Box::new(ChangeSize(Size::XSmall)), ) }), ) .child( Button::new("scroll-top") .outline() .small() .child("Scroll to Top") .on_click(cx.listener(|this, _, _, cx| { this.table.update(cx, |table, cx| { table.scroll_to_row(0, cx); }) })), ) .child( Button::new("scroll-bottom") .outline() .small() .child("Scroll to Bottom") .on_click(cx.listener(|this, _, _, cx| { this.table.update(cx, |table, cx| { table.scroll_to_row(table.delegate().rows_count(cx) - 1, cx); }) })), ), // .child( // Button::new("scroll-first-col") // .child("Scroll to First Column") // .small() // .on_click(cx.listener(|this, _, window, cx| { // this.table.update(cx, |table, cx| { // table.scroll_to_col(0, cx); // }) // })), // ) // .child( // Button::new("scroll-last-col") // .child("Scroll to Last Column") // .small() // .on_click(cx.listener(|this, _, window, cx| { // this.table.update(cx, |table, cx| { // table.scroll_to_col(table.delegate().columns_count(cx), cx); // }) // })), // ), ) .child( h_flex().items_center().gap_2().child( h_flex() .w_full() .items_center() .justify_between() .gap_2() .child( h_flex() .gap_2() .flex_1() .child(Label::new("Number of Stocks:")) .child( h_flex() .min_w_32() .child(Input::new(&self.num_stocks_input).small()) .into_any_element(), ) .when(delegate.loading, |this| { this.child( h_flex() .gap_1() .child(Indicator::new()) .child("Loading..."), ) }), ) .child( h_flex() .gap_2() .child(format!("Total Rows: {}", rows_count)) .child(format!("Visible Rows: {:?}", delegate.visible_rows)) .child(format!("Visible Cols: {:?}", delegate.visible_cols)) .when(delegate.eof, |this| this.child("All data loaded.")), ), ), ) .child(self.table.clone()) } }