diff --git a/crates/story/src/list_story.rs b/crates/story/src/list_story.rs index 7ca25db3..7bc83b7b 100644 --- a/crates/story/src/list_story.rs +++ b/crates/story/src/list_story.rs @@ -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 { - self.confirmed_index - } - fn perform_search(&mut self, query: &str, _: &mut ViewContext>) -> 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, cx: &mut ViewContext>) { - self.confirmed_index = ix; - if let Some(_) = ix { - cx.dispatch_action(Box::new(SelectedCompany)); - } + fn confirm(&mut self, ix: usize, cx: &mut ViewContext>) { + self.confirmed_index = Some(ix); + cx.dispatch_action(Box::new(SelectedCompany)); } fn set_selected_index(&mut self, ix: Option, cx: &mut ViewContext>) { @@ -230,6 +224,7 @@ pub struct ListStory { focus_handle: FocusHandle, company_list: View>, selected_company: Option, + _subscriptions: Vec, } impl super::Story for ListStory { @@ -256,20 +251,34 @@ impl ListStory { .map(|_| random_company()) .collect::>(); - 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") diff --git a/crates/story/src/modal_story.rs b/crates/story/src/modal_story.rs index 4545cd33..41efc089 100644 --- a/crates/story/src/modal_story.rs +++ b/crates/story/src/modal_story.rs @@ -39,10 +39,6 @@ impl ListDelegate for ListItemDeletegate { self.matches.len() } - fn confirmed_index(&self, _: &AppContext) -> Option { - self.confirmed_index - } - fn perform_search(&mut self, query: &str, cx: &mut ViewContext>) -> 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, cx: &mut ViewContext>) { + fn confirm(&mut self, ix: usize, cx: &mut ViewContext>) { 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(); }); } diff --git a/crates/ui/src/dropdown.rs b/crates/ui/src/dropdown.rs index 6e177a17..d03f9032 100644 --- a/crates/ui/src/dropdown.rs +++ b/crates/ui/src/dropdown.rs @@ -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 { - self.selected_index - } - fn render_item(&self, ix: usize, cx: &mut gpui::ViewContext>) -> Option { let selected = self .selected_index @@ -159,8 +165,8 @@ where }); } - fn confirm(&mut self, ix: Option, cx: &mut ViewContext>) { - self.selected_index = ix; + fn confirm(&mut self, ix: usize, cx: &mut ViewContext>) { + self.selected_index = Some(ix); let selected_value = self .selected_index diff --git a/crates/ui/src/list/list.rs b/crates/ui/src/list/list.rs index 1ee01987..56b49644 100644 --- a/crates/ui/src/list/list.rs +++ b/crates/ui/src/list/list.rs @@ -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 { - None - } - /// Set the selected index, just store the ix, don't confirm. fn set_selected_index(&mut self, ix: Option, cx: &mut ViewContext>); /// 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, cx: &mut ViewContext>) {} + fn confirm(&mut self, ix: usize, cx: &mut ViewContext>) {} /// Cancel the selection, e.g.: Pressed ESC. fn cancel(&mut self, cx: &mut ViewContext>) {} @@ -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, cx: &mut ViewContext) { self.selected_index = ix; self.delegate.set_selected_index(ix, cx); + self.scroll_to_selected_item(cx); } pub fn selected_index(&self) -> Option { @@ -351,6 +358,7 @@ where fn on_action_cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { 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.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) { @@ -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) -> impl IntoElement { @@ -456,7 +468,7 @@ where } } } - +impl EventEmitter for List where D: ListDelegate {} impl Render for List where D: ListDelegate,