list, table: Refactor List, Table to have ListState, TableState. (#1468)
## Break Change
- Like Select, Input API design, now `List`, `Table` also has ListState
and TableState.
```diff
- let table = cx.new(|_| Table::new(delegate, window, cx).stripe(true).border(true))
+ let table = cx.new(|_| TableState::new(delegate, window, cx))
+ Table::new(&table).stripe(true).border(true) // for render
- let list = cx.new(|_| List::new(delegate, window, cx))
+ let list = cx.new(|_| ListState::new(delegate, window, cx))
+ List::new(&list) // for render
```
- ListDelegate, TableDelegate methods has changed some argument type:
- If first argument is `&mut self`, the `cx` has been changed to `&mut
Context<ListState<Self>>` or `&mut Context<TableState<Self>>`.
```diff
- fn confirm(&mut self, _secondary: bool, _: &mut Window, cx: &mut
Context<List<Self>>)
+ fn confirm(&mut self, _secondary: bool, _: &mut Window, cx: &mut
Context<ListState<Self>>)
```
- If first argument is `&self`, the `cx` has been changed to `&mut App`.
```diff
- fn render_item(&self, ix: IndexPath, _: &mut Window, _: &mut
Context<List<Self>>)
+ fn render_item(&self, ix: IndexPath, _: &mut Window, _: &mut App)
```
This commit is contained in:
parent
123934237a
commit
9fa2669ff4
14 changed files with 746 additions and 696 deletions
|
|
@ -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<List<Self>>,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> 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<List<Self>>,
|
||||
) -> Option<Self::Item> {
|
||||
fn render_item(&self, ix: IndexPath, _: &mut Window, _: &mut App) -> Option<Self::Item> {
|
||||
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<List<Self>>) -> 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<List<Self>>) {
|
||||
fn cancel(&mut self, window: &mut Window, cx: &mut Context<ListState<Self>>) {
|
||||
_ = self.story.update(cx, |this, cx| {
|
||||
this.close_drawer(window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, _: &mut Window, cx: &mut Context<List<Self>>) {
|
||||
fn confirm(&mut self, _secondary: bool, _: &mut Window, cx: &mut Context<ListState<Self>>) {
|
||||
_ = 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<IndexPath>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) {
|
||||
self.selected_index = ix.map(|ix| ix.row);
|
||||
|
||||
|
|
@ -153,7 +148,7 @@ pub struct DrawerStory {
|
|||
focus_handle: FocusHandle,
|
||||
drawer_placement: Option<Placement>,
|
||||
selected_value: Option<SharedString>,
|
||||
list: Entity<List<ListItemDeletegate>>,
|
||||
list: Entity<ListState<ListItemDeletegate>>,
|
||||
input1: Entity<InputState>,
|
||||
input2: Entity<InputState>,
|
||||
date: Entity<DatePickerState>,
|
||||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -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<List<Self>>,
|
||||
_: &mut Context<ListState<Self>>,
|
||||
) -> Task<()> {
|
||||
self.prepare(query.to_owned());
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<List<Self>>) {
|
||||
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
|
||||
println!("Confirmed with secondary: {}", secondary);
|
||||
window.dispatch_action(Box::new(SelectedCompany), cx);
|
||||
}
|
||||
|
|
@ -236,7 +237,7 @@ impl ListDelegate for CompanyListDelegate {
|
|||
&mut self,
|
||||
ix: Option<IndexPath>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
cx.notify();
|
||||
|
|
@ -246,7 +247,7 @@ impl ListDelegate for CompanyListDelegate {
|
|||
&self,
|
||||
section: usize,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
cx: &mut App,
|
||||
) -> Option<impl IntoElement> {
|
||||
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<List<Self>>,
|
||||
cx: &mut App,
|
||||
) -> Option<impl IntoElement> {
|
||||
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<List<Self>>,
|
||||
) -> Option<Self::Item> {
|
||||
fn render_item(&self, ix: IndexPath, _: &mut Window, _: &mut App) -> Option<Self::Item> {
|
||||
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<List<Self>>) {
|
||||
fn load_more(&mut self, window: &mut Window, cx: &mut Context<ListState<Self>>) {
|
||||
// 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<List<CompanyListDelegate>>,
|
||||
company_list: Entity<ListState<CompanyListDelegate>>,
|
||||
selected_company: Option<Rc<Company>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
|
@ -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.)))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Table<Self>>) -> 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<Table<Self>>,
|
||||
) -> 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<Table<Self>>,
|
||||
) -> 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<Table<Self>>,
|
||||
) -> gpui::Stateful<gpui::Div> {
|
||||
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> {
|
||||
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<Table<Self>>,
|
||||
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<Table<Self>>,
|
||||
_: &mut Context<TableState<Self>>,
|
||||
) {
|
||||
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<Table<Self>>,
|
||||
_: &mut Context<TableState<Self>>,
|
||||
) {
|
||||
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<Table<Self>>) {
|
||||
fn load_more(&mut self, _: &mut Window, cx: &mut Context<TableState<Self>>) {
|
||||
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<usize>,
|
||||
_: &mut Window,
|
||||
_: &mut Context<Table<Self>>,
|
||||
_: &mut Context<TableState<Self>>,
|
||||
) {
|
||||
self.visible_rows = visible_range;
|
||||
}
|
||||
|
|
@ -582,14 +565,14 @@ impl TableDelegate for StockTableDelegate {
|
|||
&mut self,
|
||||
visible_range: Range<usize>,
|
||||
_: &mut Window,
|
||||
_: &mut Context<Table<Self>>,
|
||||
_: &mut Context<TableState<Self>>,
|
||||
) {
|
||||
self.visible_cols = visible_range;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TableStory {
|
||||
table: Entity<Table<StockTableDelegate>>,
|
||||
table: Entity<TableState<StockTableDelegate>>,
|
||||
num_stocks_input: Entity<InputState>,
|
||||
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>) {
|
||||
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>) {
|
||||
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<Self>) {
|
||||
|
|
@ -777,7 +753,7 @@ impl TableStory {
|
|||
|
||||
fn on_table_event(
|
||||
&mut self,
|
||||
_: &Entity<Table<StockTableDelegate>>,
|
||||
_: &Entity<TableState<StockTableDelegate>>,
|
||||
event: &TableEvent,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
|
|
@ -983,6 +959,10 @@ impl Render for TableStory {
|
|||
),
|
||||
),
|
||||
)
|
||||
.child(self.table.clone())
|
||||
.child(
|
||||
Table::new(&self.table)
|
||||
.with_size(self.size)
|
||||
.stripe(self.stripe),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<List<Self>>,
|
||||
) -> Option<Self::Item> {
|
||||
fn render_item(&self, ix: crate::IndexPath, _: &mut Window, _: &mut App) -> Option<Self::Item> {
|
||||
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<crate::IndexPath>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) {
|
||||
self.selected_ix = ix.map(|i| i.row).unwrap_or(0);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<List<Self>>) {
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
|
||||
let Some(item) = self.selected_item() else {
|
||||
return;
|
||||
};
|
||||
|
|
@ -145,7 +140,7 @@ impl ListDelegate for MenuDelegate {
|
|||
pub struct CodeActionMenu {
|
||||
offset: usize,
|
||||
state: Entity<InputState>,
|
||||
list: Entity<List<MenuDelegate>>,
|
||||
list: Entity<ListState<MenuDelegate>>,
|
||||
open: bool,
|
||||
bounds: Bounds<Pixels>,
|
||||
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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<List<Self>>,
|
||||
) -> Option<Self::Item> {
|
||||
fn render_item(&self, ix: crate::IndexPath, _: &mut Window, _: &mut App) -> Option<Self::Item> {
|
||||
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<crate::IndexPath>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) {
|
||||
self.selected_ix = ix.map(|i| i.row).unwrap_or(0);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<List<Self>>) {
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
|
||||
let Some(item) = self.selected_item() else {
|
||||
return;
|
||||
};
|
||||
|
|
@ -172,7 +167,7 @@ impl ListDelegate for ContextMenuDelegate {
|
|||
pub struct CompletionMenu {
|
||||
offset: usize,
|
||||
editor: Entity<InputState>,
|
||||
list: Entity<List<ContextMenuDelegate>>,
|
||||
list: Entity<ListState<ContextMenuDelegate>>,
|
||||
open: bool,
|
||||
bounds: Bounds<Pixels>,
|
||||
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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<List<Self>>,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> 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<List<Self>>,
|
||||
) -> Option<Self::Item>;
|
||||
fn render_item(&self, ix: IndexPath, window: &mut Window, cx: &mut App) -> Option<Self::Item>;
|
||||
|
||||
/// 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<List<Self>>,
|
||||
cx: &mut App,
|
||||
) -> Option<impl IntoElement> {
|
||||
None::<AnyElement>
|
||||
}
|
||||
|
|
@ -61,13 +56,13 @@ pub trait ListDelegate: Sized + 'static {
|
|||
&self,
|
||||
section: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
cx: &mut App,
|
||||
) -> Option<impl IntoElement> {
|
||||
None::<AnyElement>
|
||||
}
|
||||
|
||||
/// Return a Element to show when list is empty.
|
||||
fn render_empty(&self, window: &mut Window, cx: &mut Context<List<Self>>) -> 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<List<Self>>,
|
||||
) -> Option<AnyElement> {
|
||||
fn render_initial(&self, window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
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<List<Self>>,
|
||||
) -> 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<IndexPath>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
);
|
||||
|
||||
/// 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<List<Self>>) {}
|
||||
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
|
||||
}
|
||||
|
||||
/// Cancel the selection, e.g.: Pressed ESC.
|
||||
fn cancel(&mut self, window: &mut Window, cx: &mut Context<List<Self>>) {}
|
||||
fn cancel(&mut self, window: &mut Window, cx: &mut Context<ListState<Self>>) {}
|
||||
|
||||
/// 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<List<Self>>) {}
|
||||
fn load_more(&mut self, window: &mut Window, cx: &mut Context<ListState<Self>>) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<D: ListDelegate> {
|
||||
/// The state for List.
|
||||
pub struct ListState<D: ListDelegate> {
|
||||
focus_handle: FocusHandle,
|
||||
delegate: D,
|
||||
max_height: Option<Length>,
|
||||
paddings: Edges<Pixels>,
|
||||
query_input: Option<Entity<InputState>>,
|
||||
delegate: D,
|
||||
last_query: Option<String>,
|
||||
selectable: bool,
|
||||
querying: bool,
|
||||
scrollbar_visible: bool,
|
||||
scroll_handle: VirtualListScrollHandle,
|
||||
scroll_state: ScrollbarState,
|
||||
pub(crate) size: Size,
|
||||
rows_cache: RowsCache,
|
||||
selected_index: Option<IndexPath>,
|
||||
item_to_measure_index: IndexPath,
|
||||
|
|
@ -67,7 +64,7 @@ pub struct List<D: ListDelegate> {
|
|||
_query_input_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl<D> List<D>
|
||||
impl<D> ListState<D>
|
||||
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>) {
|
||||
self.size = size;
|
||||
}
|
||||
|
||||
pub fn max_h(mut self, height: impl Into<Length>) -> 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<Self>) -> Option<impl IntoElement> {
|
||||
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<Pixels>) -> Self {
|
||||
self.paddings = paddings;
|
||||
self
|
||||
}
|
||||
|
||||
fn on_query_input_event(
|
||||
&mut self,
|
||||
state: &Entity<InputState>,
|
||||
|
|
@ -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<Self>,
|
||||
) -> 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<Self>,
|
||||
) -> 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<usize>, 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::<Vec<_>>()
|
||||
},
|
||||
)
|
||||
.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<Self>) {
|
||||
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<Self>,
|
||||
) -> 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<D> Focusable for List<D>
|
||||
impl<D> Focusable for ListState<D>
|
||||
where
|
||||
D: ListDelegate,
|
||||
{
|
||||
|
|
@ -573,43 +460,204 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
impl<D> EventEmitter<ListEvent> for List<D> where D: ListDelegate {}
|
||||
impl<D> Render for List<D>
|
||||
impl<D> EventEmitter<ListEvent> for ListState<D> where D: ListDelegate {}
|
||||
impl<D> Render for ListState<D>
|
||||
where
|
||||
D: ListDelegate,
|
||||
{
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.prepare_items_if_needed(window, cx);
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> 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<D: ListDelegate + 'static> {
|
||||
state: Entity<ListState<D>>,
|
||||
|
||||
max_height: Option<Length>,
|
||||
paddings: Edges<Pixels>,
|
||||
scrollbar_visible: bool,
|
||||
pub(crate) size: Size,
|
||||
}
|
||||
|
||||
impl<D> List<D>
|
||||
where
|
||||
D: ListDelegate + 'static,
|
||||
{
|
||||
/// Create a new List element with the given ListState entity.
|
||||
pub fn new(state: &Entity<ListState<D>>) -> 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<Pixels>) -> 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<Length>) -> 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<usize>, 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::<Vec<_>>()
|
||||
},
|
||||
)
|
||||
.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<D> Sizable for List<D>
|
||||
where
|
||||
D: ListDelegate + 'static,
|
||||
{
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> RenderOnce for List<D>
|
||||
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();
|
||||
},
|
||||
))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SelectState<Self>>,
|
||||
) -> Task<()> {
|
||||
Task::ready(())
|
||||
}
|
||||
}
|
||||
|
|
@ -164,7 +169,7 @@ where
|
|||
&self,
|
||||
section: usize,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
cx: &mut App,
|
||||
) -> Option<impl IntoElement> {
|
||||
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<List<Self>>,
|
||||
) -> Option<Self::Item> {
|
||||
fn render_item(&self, ix: IndexPath, _: &mut Window, cx: &mut App) -> Option<Self::Item> {
|
||||
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<List<Self>>) {
|
||||
fn cancel(&mut self, window: &mut Window, cx: &mut Context<ListState<Self>>) {
|
||||
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<List<Self>>) {
|
||||
fn confirm(
|
||||
&mut self,
|
||||
_secondary: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) {
|
||||
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<List<Self>>,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) -> 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<IndexPath>,
|
||||
_: &mut Window,
|
||||
_: &mut Context<List<Self>>,
|
||||
_: &mut Context<ListState<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn render_empty(&self, window: &mut Window, cx: &mut Context<List<Self>>) -> 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<D: SelectDelegate + 'static> {
|
|||
/// State of the [`Select`].
|
||||
pub struct SelectState<D: SelectDelegate + 'static> {
|
||||
focus_handle: FocusHandle,
|
||||
list: Entity<List<SelectListDelegate<D>>>,
|
||||
list: Entity<ListState<SelectListDelegate<D>>>,
|
||||
size: Size,
|
||||
empty: Option<Box<dyn Fn(&Window, &App) -> AnyElement>>,
|
||||
/// Store the bounds of the input
|
||||
|
|
@ -373,7 +378,12 @@ impl<I: SelectItem> SelectDelegate for SearchableVec<I> {
|
|||
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<SelectState<Self>>,
|
||||
) -> Task<()> {
|
||||
self.matched_items = self
|
||||
.items
|
||||
.iter()
|
||||
|
|
@ -434,7 +444,12 @@ impl<I: SelectItem> SelectDelegate for SearchableVec<SelectGroup<I>> {
|
|||
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<SelectState<Self>>,
|
||||
) -> 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,
|
||||
|
|
|
|||
|
|
@ -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<Table<Self>>,
|
||||
cx: &mut Context<TableState<Self>>,
|
||||
) {
|
||||
}
|
||||
|
||||
/// 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<Table<Self>>,
|
||||
) -> 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<Table<Self>>,
|
||||
) -> Stateful<Div> {
|
||||
fn render_tr(&self, row_ix: usize, window: &mut Window, cx: &mut App) -> Stateful<Div> {
|
||||
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<Table<Self>>,
|
||||
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<Table<Self>>,
|
||||
cx: &mut Context<TableState<Self>>,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Return a Element to show when table is empty.
|
||||
fn render_empty(&self, window: &mut Window, cx: &mut Context<Table<Self>>) -> 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<Table<Self>>,
|
||||
) -> 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<Table<Self>>) {}
|
||||
fn load_more(&mut self, window: &mut Window, cx: &mut Context<TableState<Self>>) {}
|
||||
|
||||
/// Render the last empty column, default to empty.
|
||||
fn render_last_empty_col(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Table<Self>>,
|
||||
) -> 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<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Table<Self>>,
|
||||
cx: &mut Context<TableState<Self>>,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -164,7 +145,7 @@ pub trait TableDelegate: Sized + 'static {
|
|||
&mut self,
|
||||
visible_range: Range<usize>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Table<Self>>,
|
||||
cx: &mut Context<TableState<Self>>,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<D: TableDelegate> {
|
||||
/// The state for Table.
|
||||
pub struct TableState<D: TableDelegate> {
|
||||
focus_handle: FocusHandle,
|
||||
delegate: D,
|
||||
/// The bounds of the table container.
|
||||
|
|
@ -106,7 +107,6 @@ pub struct Table<D: TableDelegate> {
|
|||
pub horizontal_scroll_handle: VirtualListScrollHandle,
|
||||
pub horizontal_scroll_state: ScrollbarState,
|
||||
|
||||
scrollbar_visible: Edges<bool>,
|
||||
selected_row: Option<usize>,
|
||||
selection_state: SelectionState,
|
||||
right_clicked_row: Option<usize>,
|
||||
|
|
@ -115,20 +115,17 @@ pub struct Table<D: TableDelegate> {
|
|||
/// The column index that is being resized.
|
||||
resizing_col: Option<usize>,
|
||||
|
||||
/// 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<Duration>,
|
||||
_load_more_task: Task<()>,
|
||||
}
|
||||
|
||||
impl<D> Table<D>
|
||||
impl<D> TableState<D>
|
||||
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>) {
|
||||
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>) {
|
||||
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>) {
|
||||
self.prepare_col_groups(cx);
|
||||
|
|
@ -673,51 +630,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar(
|
||||
&self,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<impl IntoElement> {
|
||||
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<Self>,
|
||||
) -> 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<D> Focusable for TableState<D>
|
||||
where
|
||||
D: TableDelegate,
|
||||
{
|
||||
fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
impl<D> EventEmitter<TableEvent> for TableState<D> where D: TableDelegate {}
|
||||
|
||||
impl<D> Render for TableState<D>
|
||||
where
|
||||
D: TableDelegate,
|
||||
{
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
}
|
||||
}
|
||||
|
||||
/// A table element.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Table<D: TableDelegate> {
|
||||
state: Entity<TableState<D>>,
|
||||
scrollbar_visible: Edges<bool>,
|
||||
/// 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<D> Table<D>
|
||||
where
|
||||
D: TableDelegate,
|
||||
{
|
||||
/// Create a new Table element.
|
||||
pub fn new(state: &Entity<TableState<D>>) -> 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<TableState<D>>,
|
||||
scroll_state: &ScrollbarState,
|
||||
scroll_handle: &UniformListScrollHandle,
|
||||
window: &mut Window,
|
||||
_: &mut App,
|
||||
) -> Option<impl IntoElement> {
|
||||
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<TableState<D>>,
|
||||
fixed_head_cols_bounds: Bounds<Pixels>,
|
||||
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<D> Sizable for Table<D>
|
||||
where
|
||||
D: TableDelegate,
|
||||
|
|
@ -1301,148 +1332,173 @@ where
|
|||
self
|
||||
}
|
||||
}
|
||||
impl<D> Focusable for Table<D>
|
||||
|
||||
impl<D> RenderOnce for Table<D>
|
||||
where
|
||||
D: TableDelegate,
|
||||
{
|
||||
fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
impl<D> EventEmitter<TableEvent> for Table<D> where D: TableDelegate {}
|
||||
|
||||
impl<D> Render for Table<D>
|
||||
where
|
||||
D: TableDelegate,
|
||||
{
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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<PopupMenu>| {
|
||||
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<usize>, 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<Vec<gpui::Size<Pixels>>> = 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<PopupMenu>| {
|
||||
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<usize>,
|
||||
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<Vec<gpui::Size<Pixels>>> =
|
||||
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,
|
||||
))
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<ThemeMode, (Arc<ThemeColor>, Arc<HighlightTheme>)>,
|
||||
> = LazyLock::new(|| {
|
||||
let mut colors = HashMap::new();
|
||||
|
|
|
|||
|
|
@ -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<List<Self>>,
|
||||
_cx: &mut App,
|
||||
) -> Option<Self::Item> {
|
||||
self.items.get(ix.row).map(|item| {
|
||||
ListItem::new(ix)
|
||||
|
|
@ -50,7 +50,7 @@ impl ListDelegate for MyListDelegate {
|
|||
&mut self,
|
||||
ix: Option<IndexPath>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
cx: &mut Context<ListState<Self>>,
|
||||
) {
|
||||
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<List<Self>>,
|
||||
cx: &mut App,
|
||||
) -> Option<impl IntoElement> {
|
||||
let title = match section {
|
||||
0 => "Section 1",
|
||||
|
|
@ -114,7 +121,7 @@ impl ListDelegate for MyListDelegate {
|
|||
&self,
|
||||
section: usize,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
cx: &mut App,
|
||||
) -> Option<impl IntoElement> {
|
||||
Some(
|
||||
div()
|
||||
|
|
@ -135,7 +142,7 @@ fn render_item(
|
|||
&self,
|
||||
ix: IndexPath,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<List<Self>>,
|
||||
cx: &mut App,
|
||||
) -> Option<Self::Item> {
|
||||
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<List<Self>>,
|
||||
_cx: &mut Context<ListState<Self>>,
|
||||
) -> 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<List<Self>>,
|
||||
_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<List<Self>>) {
|
||||
fn load_more(&mut self, window: &mut Window, cx: &mut Context<ListState<Self>>) {
|
||||
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<List<Self>>) -> 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<List<Self>>) -> Option<Self::Item> {
|
||||
fn render_item(&self, ix: IndexPath, window: &mut Window, cx: &mut App) -> Option<Self::Item> {
|
||||
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<List<Self>>) -> Option<impl IntoElement> {
|
||||
fn render_section_header(&self, section: usize, _window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
|
||||
let letter = self.contacts_by_letter.keys().nth(section)?;
|
||||
|
||||
Some(
|
||||
|
|
|
|||
|
|
@ -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<Table<Self>>) -> 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<Table<Self>>) -> impl IntoElement {
|
||||
fn render_td(&self, row_ix: usize, col_ix: usize, _: &mut Window, _: &mut Context<TableState<Self>>) -> 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<usize>, _: &mut Window, _: &mut Context<Table<Self>>) {
|
||||
fn visible_rows_changed(&mut self, visible_range: Range<usize>, _: &mut Window, _: &mut Context<TableState<Self>>) {
|
||||
// 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<Table<Self>>) {
|
||||
fn perform_sort(&mut self, col_ix: usize, sort: ColumnSort, _: &mut Window, _: &mut Context<TableState<Self>>) {
|
||||
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<Table<Self>>) -> gpui::Stateful<gpui::Div> {
|
||||
fn render_tr(&self, row_ix: usize, _: &mut Window, cx: &mut App) -> Stateful<Div> {
|
||||
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<Table<Self>>) -> 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<Table<Self>>) {
|
||||
fn load_more(&mut self, _: &mut Window, cx: &mut Context<TableState<Self>>) {
|
||||
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<Table<Self>>) -> 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];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue