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