use editor::Editor; use gpui::{ div, uniform_list, Component, Div, FocusEnabled, ParentElement, Render, StatefulInteractivity, StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, }; use std::cmp; use ui::{prelude::*, v_stack, Divider, Label, LabelColor}; pub struct Picker { pub delegate: D, scroll_handle: UniformListScrollHandle, editor: View, pending_update_matches: Option>>, } pub trait PickerDelegate: Sized + 'static { type ListItem: Component>; fn match_count(&self) -> usize; fn selected_index(&self) -> usize; fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>); fn placeholder_text(&self) -> Arc; fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>; fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>); fn dismissed(&mut self, cx: &mut ViewContext>); fn render_match( &self, ix: usize, selected: bool, cx: &mut ViewContext>, ) -> Self::ListItem; } impl Picker { pub fn new(delegate: D, cx: &mut ViewContext) -> Self { let editor = cx.build_view(|cx| { let mut editor = Editor::single_line(cx); editor.set_placeholder_text(delegate.placeholder_text(), cx); editor }); cx.subscribe(&editor, Self::on_input_editor_event).detach(); Self { delegate, scroll_handle: UniformListScrollHandle::new(), pending_update_matches: None, editor, } } pub fn focus(&self, cx: &mut WindowContext) { self.editor.update(cx, |editor, cx| editor.focus(cx)); } fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext) { let count = self.delegate.match_count(); if count > 0 { let index = self.delegate.selected_index(); let ix = cmp::min(index + 1, count - 1); self.delegate.set_selected_index(ix, cx); self.scroll_handle.scroll_to_item(ix); cx.notify(); } } fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext) { let count = self.delegate.match_count(); if count > 0 { let index = self.delegate.selected_index(); let ix = index.saturating_sub(1); self.delegate.set_selected_index(ix, cx); self.scroll_handle.scroll_to_item(ix); cx.notify(); } } fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext) { let count = self.delegate.match_count(); if count > 0 { self.delegate.set_selected_index(0, cx); self.scroll_handle.scroll_to_item(0); cx.notify(); } } fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext) { let count = self.delegate.match_count(); if count > 0 { self.delegate.set_selected_index(count - 1, cx); self.scroll_handle.scroll_to_item(count - 1); cx.notify(); } } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { self.delegate.dismissed(cx); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { self.delegate.confirm(false, cx); } fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { self.delegate.confirm(true, cx); } fn on_input_editor_event( &mut self, _: View, event: &editor::Event, cx: &mut ViewContext, ) { if let editor::Event::BufferEdited = event { let query = self.editor.read(cx).text(cx); self.update_matches(query, cx); } } pub fn update_matches(&mut self, query: String, cx: &mut ViewContext) { let update = self.delegate.update_matches(query, cx); self.matches_updated(cx); self.pending_update_matches = Some(cx.spawn(|this, mut cx| async move { update.await; this.update(&mut cx, |this, cx| { this.matches_updated(cx); }) .ok() })); } fn matches_updated(&mut self, cx: &mut ViewContext) { let index = self.delegate.selected_index(); self.scroll_handle.scroll_to_item(index); self.pending_update_matches = None; cx.notify(); } } impl Render for Picker { type Element = Div, FocusEnabled>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() .context("picker") .id("picker-container") .focusable() .size_full() .elevation_2(cx) .on_action(Self::select_next) .on_action(Self::select_prev) .on_action(Self::select_first) .on_action(Self::select_last) .on_action(Self::cancel) .on_action(Self::confirm) .on_action(Self::secondary_confirm) .child( v_stack() .py_0p5() .px_1() .child(div().px_1().py_0p5().child(self.editor.clone())), ) .child(Divider::horizontal()) .when(self.delegate.match_count() > 0, |el| { el.child( v_stack() .p_1() .grow() .child( uniform_list("candidates", self.delegate.match_count(), { move |this: &mut Self, visible_range, cx| { let selected_ix = this.delegate.selected_index(); visible_range .map(|ix| { this.delegate.render_match(ix, ix == selected_ix, cx) }) .collect() } }) .track_scroll(self.scroll_handle.clone()), ) .max_h_72() .overflow_hidden(), ) }) .when(self.delegate.match_count() == 0, |el| { el.child( v_stack() .p_1() .grow() .child(Label::new("No matches").color(LabelColor::Muted)), ) }) } }