list: Add ListEvent to List. (#552)

- Add `ListEvent` and emit event for List with `Select`, `Confirm`,
`Cancel` events.

## Break Changes

- Removed useless `ListDelegate::confirmed_index` method.
- Updated `ListDelegate::confirm` method, to change `ix: Option<usize>`
to `ix: usize`.
This commit is contained in:
Jason Lee 2025-01-17 16:00:28 +08:00 committed by GitHub
parent 569a4e7262
commit f41edfd318
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 104 additions and 68 deletions

View file

@ -4,8 +4,8 @@ use std::time::Duration;
use fake::Fake; use fake::Fake;
use gpui::{ use gpui::{
actions, div, px, AppContext, ElementId, FocusHandle, FocusableView, InteractiveElement, actions, div, px, AppContext, ElementId, FocusHandle, FocusableView, InteractiveElement,
IntoElement, ParentElement, Render, RenderOnce, Styled, Task, Timer, View, ViewContext, IntoElement, ParentElement, Render, RenderOnce, Styled, Subscription, Task, Timer, View,
VisualContext, WindowContext, ViewContext, VisualContext, WindowContext,
}; };
use ui::{ use ui::{
@ -13,7 +13,7 @@ use ui::{
checkbox::Checkbox, checkbox::Checkbox,
h_flex, h_flex,
label::Label, label::Label,
list::{List, ListDelegate, ListItem}, list::{List, ListDelegate, ListEvent, ListItem},
theme::{hsl, ActiveTheme}, theme::{hsl, ActiveTheme},
v_flex, Sizable, v_flex, Sizable,
}; };
@ -150,10 +150,6 @@ impl ListDelegate for CompanyListDelegate {
self.matched_companies.len() self.matched_companies.len()
} }
fn confirmed_index(&self, _: &AppContext) -> Option<usize> {
self.confirmed_index
}
fn perform_search(&mut self, query: &str, _: &mut ViewContext<List<Self>>) -> Task<()> { fn perform_search(&mut self, query: &str, _: &mut ViewContext<List<Self>>) -> Task<()> {
self.query = query.to_string(); self.query = query.to_string();
self.matched_companies = self self.matched_companies = self
@ -166,11 +162,9 @@ impl ListDelegate for CompanyListDelegate {
Task::ready(()) Task::ready(())
} }
fn confirm(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) { fn confirm(&mut self, ix: usize, cx: &mut ViewContext<List<Self>>) {
self.confirmed_index = ix; self.confirmed_index = Some(ix);
if let Some(_) = ix { cx.dispatch_action(Box::new(SelectedCompany));
cx.dispatch_action(Box::new(SelectedCompany));
}
} }
fn set_selected_index(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) { fn set_selected_index(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) {
@ -230,6 +224,7 @@ pub struct ListStory {
focus_handle: FocusHandle, focus_handle: FocusHandle,
company_list: View<List<CompanyListDelegate>>, company_list: View<List<CompanyListDelegate>>,
selected_company: Option<Company>, selected_company: Option<Company>,
_subscriptions: Vec<Subscription>,
} }
impl super::Story for ListStory { impl super::Story for ListStory {
@ -256,20 +251,34 @@ impl ListStory {
.map(|_| random_company()) .map(|_| random_company())
.collect::<Vec<Company>>(); .collect::<Vec<Company>>();
let company_list = cx.new_view(|cx| { let delegate = CompanyListDelegate {
List::new( matched_companies: companies.clone(),
CompanyListDelegate { companies,
matched_companies: companies.clone(), selected_index: 3,
companies, confirmed_index: None,
selected_index: 0, query: "".to_string(),
confirmed_index: None, loading: false,
query: "".to_string(), is_eof: false,
loading: false, };
is_eof: false,
}, let company_list = cx.new_view(|cx| List::new(delegate, cx));
cx, company_list.update(cx, |list, cx| {
) list.set_selected_index(Some(3), cx);
}); });
let _subscriptions =
vec![
cx.subscribe(&company_list, |_, _, ev: &ListEvent, _| match ev {
ListEvent::Select(ix) => {
println!("List Selected: {:?}", ix);
}
ListEvent::Confirm(ix) => {
println!("List Confirmed: {:?}", ix);
}
ListEvent::Cancel => {
println!("List Cancelled");
}
}),
];
// Spawn a background to random refresh the list // Spawn a background to random refresh the list
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
@ -296,6 +305,7 @@ impl ListStory {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
company_list, company_list,
selected_company: None, selected_company: None,
_subscriptions,
} }
} }
@ -334,6 +344,7 @@ impl Render for ListStory {
.child( .child(
h_flex() h_flex()
.gap_2() .gap_2()
.flex_wrap()
.child( .child(
Button::new("scroll-top") Button::new("scroll-top")
.child("Scroll to Top") .child("Scroll to Top")
@ -354,6 +365,18 @@ impl Render for ListStory {
}) })
})), })),
) )
.child(
Button::new("scroll-to-selected")
.child("Scroll to Selected")
.small()
.on_click(cx.listener(|this, _, cx| {
this.company_list.update(cx, |list, cx| {
if let Some(selected) = list.selected_index() {
list.scroll_to_item(selected, cx);
}
})
})),
)
.child( .child(
Checkbox::new("loading") Checkbox::new("loading")
.label("Loading") .label("Loading")

View file

@ -39,10 +39,6 @@ impl ListDelegate for ListItemDeletegate {
self.matches.len() self.matches.len()
} }
fn confirmed_index(&self, _: &AppContext) -> Option<usize> {
self.confirmed_index
}
fn perform_search(&mut self, query: &str, cx: &mut ViewContext<List<Self>>) -> Task<()> { fn perform_search(&mut self, query: &str, cx: &mut ViewContext<List<Self>>) -> Task<()> {
let query = query.to_string(); let query = query.to_string();
cx.spawn(move |this, mut cx| async move { cx.spawn(move |this, mut cx| async move {
@ -121,15 +117,14 @@ impl ListDelegate for ListItemDeletegate {
} }
} }
fn confirm(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) { fn confirm(&mut self, ix: usize, cx: &mut ViewContext<List<Self>>) {
if let Some(story) = self.story.upgrade() { if let Some(story) = self.story.upgrade() {
cx.update_view(&story, |story, cx| { cx.update_view(&story, |story, cx| {
if let Some(ix) = ix { self.confirmed_index = Some(ix);
self.confirmed_index = Some(ix); if let Some(item) = self.matches.get(ix) {
if let Some(item) = self.matches.get(ix) { story.selected_value = Some(SharedString::from(item.to_string()));
story.selected_value = Some(SharedString::from(item.to_string()));
}
} }
cx.close_drawer(); cx.close_drawer();
}); });
} }

View file

@ -17,6 +17,16 @@ use crate::{
actions!(dropdown, [Up, Down, Enter, Escape]); actions!(dropdown, [Up, Down, Enter, Escape]);
#[derive(Clone)]
pub enum ListEvent {
/// Single click or move to selected row.
SelectItem(usize),
/// Double click on the row.
ConfirmItem(usize),
// Cancel the selection.
Cancel,
}
const CONTEXT: &str = "Dropdown"; const CONTEXT: &str = "Dropdown";
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.bind_keys([ cx.bind_keys([
@ -122,10 +132,6 @@ where
self.delegate.len() self.delegate.len()
} }
fn confirmed_index(&self, _: &AppContext) -> Option<usize> {
self.selected_index
}
fn render_item(&self, ix: usize, cx: &mut gpui::ViewContext<List<Self>>) -> Option<Self::Item> { fn render_item(&self, ix: usize, cx: &mut gpui::ViewContext<List<Self>>) -> Option<Self::Item> {
let selected = self let selected = self
.selected_index .selected_index
@ -159,8 +165,8 @@ where
}); });
} }
fn confirm(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) { fn confirm(&mut self, ix: usize, cx: &mut ViewContext<List<Self>>) {
self.selected_index = ix; self.selected_index = Some(ix);
let selected_value = self let selected_value = self
.selected_index .selected_index

View file

@ -14,7 +14,7 @@ use gpui::{
ListSizingBehavior, MouseButton, ParentElement, Render, SharedString, Styled, Task, ListSizingBehavior, MouseButton, ParentElement, Render, SharedString, Styled, Task,
UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext,
}; };
use gpui::{px, ScrollStrategy}; use gpui::{px, EventEmitter, ScrollStrategy};
use smol::Timer; use smol::Timer;
use super::loading::Loading; use super::loading::Loading;
@ -31,6 +31,16 @@ pub fn init(cx: &mut AppContext) {
]); ]);
} }
#[derive(Clone)]
pub enum ListEvent {
/// Move to select item.
Select(usize),
/// Click on item or pressed Enter.
Confirm(usize),
/// Pressed ESC to deselect the item.
Cancel,
}
/// A delegate for the List. /// A delegate for the List.
#[allow(unused)] #[allow(unused)]
pub trait ListDelegate: Sized + 'static { pub trait ListDelegate: Sized + 'static {
@ -71,16 +81,11 @@ pub trait ListDelegate: Sized + 'static {
Loading Loading
} }
/// Return the confirmed index of the selected item.
fn confirmed_index(&self, cx: &AppContext) -> Option<usize> {
None
}
/// Set the selected index, just store the ix, don't confirm. /// Set the selected index, just store the ix, don't confirm.
fn set_selected_index(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>); fn set_selected_index(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>);
/// Set the confirm and give the selected index, this is means user have clicked the item or pressed Enter. /// Set the confirm and give the selected index, this is means user have clicked the item or pressed Enter.
fn confirm(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) {} fn confirm(&mut self, ix: usize, cx: &mut ViewContext<List<Self>>) {}
/// Cancel the selection, e.g.: Pressed ESC. /// Cancel the selection, e.g.: Pressed ESC.
fn cancel(&mut self, cx: &mut ViewContext<List<Self>>) {} fn cancel(&mut self, cx: &mut ViewContext<List<Self>>) {}
@ -226,9 +231,11 @@ where
self.focus_handle(cx).focus(cx); self.focus_handle(cx).focus(cx);
} }
/// Set the selected index of the list, this will also scroll to the selected item.
pub fn set_selected_index(&mut self, ix: Option<usize>, cx: &mut ViewContext<Self>) { pub fn set_selected_index(&mut self, ix: Option<usize>, cx: &mut ViewContext<Self>) {
self.selected_index = ix; self.selected_index = ix;
self.delegate.set_selected_index(ix, cx); self.delegate.set_selected_index(ix, cx);
self.scroll_to_selected_item(cx);
} }
pub fn selected_index(&self) -> Option<usize> { pub fn selected_index(&self) -> Option<usize> {
@ -351,6 +358,7 @@ where
fn on_action_cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) { fn on_action_cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
self.set_selected_index(None, cx); self.set_selected_index(None, cx);
self.delegate.cancel(cx); self.delegate.cancel(cx);
cx.emit(ListEvent::Cancel);
cx.notify(); cx.notify();
} }
@ -359,7 +367,20 @@ where
return; return;
} }
self.delegate.confirm(self.selected_index, cx); let Some(ix) = self.selected_index else {
return;
};
self.delegate.confirm(ix, cx);
cx.emit(ListEvent::Confirm(ix));
cx.notify();
}
fn select_item(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
self.selected_index = Some(ix);
self.delegate.set_selected_index(Some(ix), cx);
self.scroll_to_selected_item(cx);
cx.emit(ListEvent::Select(ix));
cx.notify(); cx.notify();
} }
@ -369,16 +390,13 @@ where
return; return;
} }
let selected_index = self.selected_index.unwrap_or(0); let mut selected_index = self.selected_index.unwrap_or(0);
if selected_index > 0 { if selected_index > 0 {
self.selected_index = Some(selected_index - 1); selected_index = selected_index - 1;
} else { } else {
self.selected_index = Some(items_count - 1); selected_index = items_count - 1;
} }
self.select_item(selected_index, cx);
self.delegate.set_selected_index(self.selected_index, cx);
self.scroll_to_selected_item(cx);
cx.notify();
} }
fn on_action_select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) { fn on_action_select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
@ -387,19 +405,13 @@ where
return; return;
} }
if let Some(selected_index) = self.selected_index { let mut selected_index = self.selected_index.unwrap_or(0);
if selected_index < items_count - 1 { if selected_index < items_count - 1 {
self.selected_index = Some(selected_index + 1); selected_index = selected_index + 1;
} else {
self.selected_index = Some(0);
}
} else { } else {
self.selected_index = Some(0); selected_index = 0;
} }
self.select_item(selected_index, cx);
self.delegate.set_selected_index(self.selected_index, cx);
self.scroll_to_selected_item(cx);
cx.notify();
} }
fn render_list_item(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_list_item(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> impl IntoElement {
@ -456,7 +468,7 @@ where
} }
} }
} }
impl<D> EventEmitter<ListEvent> for List<D> where D: ListDelegate {}
impl<D> Render for List<D> impl<D> Render for List<D>
where where
D: ListDelegate, D: ListDelegate,