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 gpui::{
actions, div, px, AppContext, ElementId, FocusHandle, FocusableView, InteractiveElement,
IntoElement, ParentElement, Render, RenderOnce, Styled, Task, Timer, View, ViewContext,
VisualContext, WindowContext,
IntoElement, ParentElement, Render, RenderOnce, Styled, Subscription, Task, Timer, View,
ViewContext, VisualContext, WindowContext,
};
use ui::{
@ -13,7 +13,7 @@ use ui::{
checkbox::Checkbox,
h_flex,
label::Label,
list::{List, ListDelegate, ListItem},
list::{List, ListDelegate, ListEvent, ListItem},
theme::{hsl, ActiveTheme},
v_flex, Sizable,
};
@ -150,10 +150,6 @@ impl ListDelegate for CompanyListDelegate {
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<()> {
self.query = query.to_string();
self.matched_companies = self
@ -166,11 +162,9 @@ impl ListDelegate for CompanyListDelegate {
Task::ready(())
}
fn confirm(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) {
self.confirmed_index = ix;
if let Some(_) = ix {
cx.dispatch_action(Box::new(SelectedCompany));
}
fn confirm(&mut self, ix: usize, cx: &mut ViewContext<List<Self>>) {
self.confirmed_index = Some(ix);
cx.dispatch_action(Box::new(SelectedCompany));
}
fn set_selected_index(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) {
@ -230,6 +224,7 @@ pub struct ListStory {
focus_handle: FocusHandle,
company_list: View<List<CompanyListDelegate>>,
selected_company: Option<Company>,
_subscriptions: Vec<Subscription>,
}
impl super::Story for ListStory {
@ -256,20 +251,34 @@ impl ListStory {
.map(|_| random_company())
.collect::<Vec<Company>>();
let company_list = cx.new_view(|cx| {
List::new(
CompanyListDelegate {
matched_companies: companies.clone(),
companies,
selected_index: 0,
confirmed_index: None,
query: "".to_string(),
loading: false,
is_eof: false,
},
cx,
)
let delegate = CompanyListDelegate {
matched_companies: companies.clone(),
companies,
selected_index: 3,
confirmed_index: None,
query: "".to_string(),
loading: false,
is_eof: false,
};
let company_list = cx.new_view(|cx| List::new(delegate, 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
cx.spawn(move |this, mut cx| async move {
@ -296,6 +305,7 @@ impl ListStory {
focus_handle: cx.focus_handle(),
company_list,
selected_company: None,
_subscriptions,
}
}
@ -334,6 +344,7 @@ impl Render for ListStory {
.child(
h_flex()
.gap_2()
.flex_wrap()
.child(
Button::new("scroll-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(
Checkbox::new("loading")
.label("Loading")

View file

@ -39,10 +39,6 @@ impl ListDelegate for ListItemDeletegate {
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<()> {
let query = query.to_string();
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() {
cx.update_view(&story, |story, cx| {
if let Some(ix) = ix {
self.confirmed_index = Some(ix);
if let Some(item) = self.matches.get(ix) {
story.selected_value = Some(SharedString::from(item.to_string()));
}
self.confirmed_index = Some(ix);
if let Some(item) = self.matches.get(ix) {
story.selected_value = Some(SharedString::from(item.to_string()));
}
cx.close_drawer();
});
}

View file

@ -17,6 +17,16 @@ use crate::{
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";
pub fn init(cx: &mut AppContext) {
cx.bind_keys([
@ -122,10 +132,6 @@ where
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> {
let selected = self
.selected_index
@ -159,8 +165,8 @@ where
});
}
fn confirm(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) {
self.selected_index = ix;
fn confirm(&mut self, ix: usize, cx: &mut ViewContext<List<Self>>) {
self.selected_index = Some(ix);
let selected_value = self
.selected_index

View file

@ -14,7 +14,7 @@ use gpui::{
ListSizingBehavior, MouseButton, ParentElement, Render, SharedString, Styled, Task,
UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext,
};
use gpui::{px, ScrollStrategy};
use gpui::{px, EventEmitter, ScrollStrategy};
use smol::Timer;
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.
#[allow(unused)]
pub trait ListDelegate: Sized + 'static {
@ -71,16 +81,11 @@ pub trait ListDelegate: Sized + 'static {
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.
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.
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.
fn cancel(&mut self, cx: &mut ViewContext<List<Self>>) {}
@ -226,9 +231,11 @@ where
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>) {
self.selected_index = ix;
self.delegate.set_selected_index(ix, cx);
self.scroll_to_selected_item(cx);
}
pub fn selected_index(&self) -> Option<usize> {
@ -351,6 +358,7 @@ where
fn on_action_cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
self.set_selected_index(None, cx);
self.delegate.cancel(cx);
cx.emit(ListEvent::Cancel);
cx.notify();
}
@ -359,7 +367,20 @@ where
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();
}
@ -369,16 +390,13 @@ where
return;
}
let selected_index = self.selected_index.unwrap_or(0);
let mut selected_index = self.selected_index.unwrap_or(0);
if selected_index > 0 {
self.selected_index = Some(selected_index - 1);
selected_index = selected_index - 1;
} else {
self.selected_index = Some(items_count - 1);
selected_index = items_count - 1;
}
self.delegate.set_selected_index(self.selected_index, cx);
self.scroll_to_selected_item(cx);
cx.notify();
self.select_item(selected_index, cx);
}
fn on_action_select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
@ -387,19 +405,13 @@ where
return;
}
if let Some(selected_index) = self.selected_index {
if selected_index < items_count - 1 {
self.selected_index = Some(selected_index + 1);
} else {
self.selected_index = Some(0);
}
let mut selected_index = self.selected_index.unwrap_or(0);
if selected_index < items_count - 1 {
selected_index = selected_index + 1;
} else {
self.selected_index = Some(0);
selected_index = 0;
}
self.delegate.set_selected_index(self.selected_index, cx);
self.scroll_to_selected_item(cx);
cx.notify();
self.select_item(selected_index, cx);
}
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>
where
D: ListDelegate,