picker.rs

  1mod head;
  2pub mod highlighted_match_with_paths;
  3pub mod popover_menu;
  4
  5use anyhow::Result;
  6use editor::{
  7    Editor, SelectionEffects,
  8    actions::{MoveDown, MoveUp},
  9    scroll::Autoscroll,
 10};
 11use gpui::{
 12    Action, AnyElement, App, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
 13    Focusable, Length, ListSizingBehavior, ListState, MouseButton, MouseUpEvent, Render,
 14    ScrollStrategy, Stateful, Task, UniformListScrollHandle, Window, actions, div, list,
 15    prelude::*, uniform_list,
 16};
 17use head::Head;
 18use schemars::JsonSchema;
 19use serde::Deserialize;
 20use std::{ops::Range, sync::Arc, time::Duration};
 21use ui::{
 22    Color, Divider, Label, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, prelude::*, v_flex,
 23};
 24use util::ResultExt;
 25use workspace::ModalView;
 26
 27enum ElementContainer {
 28    List(ListState),
 29    UniformList(UniformListScrollHandle),
 30}
 31
 32pub enum Direction {
 33    Up,
 34    Down,
 35}
 36
 37actions!(
 38    picker,
 39    [
 40        /// Confirms the selected completion in the picker.
 41        ConfirmCompletion
 42    ]
 43);
 44
 45/// ConfirmInput is an alternative editor action which - instead of selecting active picker entry - treats pickers editor input literally,
 46/// performing some kind of action on it.
 47#[derive(Clone, PartialEq, Deserialize, JsonSchema, Default, Action)]
 48#[action(namespace = picker)]
 49#[serde(deny_unknown_fields)]
 50pub struct ConfirmInput {
 51    pub secondary: bool,
 52}
 53
 54struct PendingUpdateMatches {
 55    delegate_update_matches: Option<Task<()>>,
 56    _task: Task<Result<()>>,
 57}
 58
 59pub struct Picker<D: PickerDelegate> {
 60    pub delegate: D,
 61    element_container: ElementContainer,
 62    head: Head,
 63    pending_update_matches: Option<PendingUpdateMatches>,
 64    confirm_on_update: Option<bool>,
 65    width: Option<Length>,
 66    widest_item: Option<usize>,
 67    max_height: Option<Length>,
 68    focus_handle: FocusHandle,
 69    /// An external control to display a scrollbar in the `Picker`.
 70    show_scrollbar: bool,
 71    /// An internal state that controls whether to show the scrollbar based on the user's focus.
 72    scrollbar_visibility: bool,
 73    scrollbar_state: ScrollbarState,
 74    hide_scrollbar_task: Option<Task<()>>,
 75    /// Whether the `Picker` is rendered as a self-contained modal.
 76    ///
 77    /// Set this to `false` when rendering the `Picker` as part of a larger modal.
 78    is_modal: bool,
 79}
 80
 81#[derive(Debug, Default, Clone, Copy, PartialEq)]
 82pub enum PickerEditorPosition {
 83    #[default]
 84    /// Render the editor at the start of the picker. Usually the top
 85    Start,
 86    /// Render the editor at the end of the picker. Usually the bottom
 87    End,
 88}
 89
 90pub trait PickerDelegate: Sized + 'static {
 91    type ListItem: IntoElement;
 92
 93    fn match_count(&self) -> usize;
 94    fn selected_index(&self) -> usize;
 95    fn separators_after_indices(&self) -> Vec<usize> {
 96        Vec::new()
 97    }
 98    fn set_selected_index(
 99        &mut self,
100        ix: usize,
101        window: &mut Window,
102        cx: &mut Context<Picker<Self>>,
103    );
104    fn can_select(
105        &mut self,
106        _ix: usize,
107        _window: &mut Window,
108        _cx: &mut Context<Picker<Self>>,
109    ) -> bool {
110        true
111    }
112
113    // Allows binding some optional effect to when the selection changes.
114    fn selected_index_changed(
115        &self,
116        _ix: usize,
117        _window: &mut Window,
118        _cx: &mut Context<Picker<Self>>,
119    ) -> Option<Box<dyn Fn(&mut Window, &mut App) + 'static>> {
120        None
121    }
122    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str>;
123    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
124        Some("No matches".into())
125    }
126    fn update_matches(
127        &mut self,
128        query: String,
129        window: &mut Window,
130        cx: &mut Context<Picker<Self>>,
131    ) -> Task<()>;
132
133    // Delegates that support this method (e.g. the CommandPalette) can chose to block on any background
134    // work for up to `duration` to try and get a result synchronously.
135    // This avoids a flash of an empty command-palette on cmd-shift-p, and lets workspace::SendKeystrokes
136    // mostly work when dismissing a palette.
137    fn finalize_update_matches(
138        &mut self,
139        _query: String,
140        _duration: Duration,
141        _window: &mut Window,
142        _cx: &mut Context<Picker<Self>>,
143    ) -> bool {
144        false
145    }
146
147    /// Override if you want to have <enter> update the query instead of confirming.
148    fn confirm_update_query(
149        &mut self,
150        _window: &mut Window,
151        _cx: &mut Context<Picker<Self>>,
152    ) -> Option<String> {
153        None
154    }
155    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>);
156    /// Instead of interacting with currently selected entry, treats editor input literally,
157    /// performing some kind of action on it.
158    fn confirm_input(
159        &mut self,
160        _secondary: bool,
161        _window: &mut Window,
162        _: &mut Context<Picker<Self>>,
163    ) {
164    }
165    fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>);
166    fn should_dismiss(&self) -> bool {
167        true
168    }
169    fn confirm_completion(
170        &mut self,
171        _query: String,
172        _window: &mut Window,
173        _: &mut Context<Picker<Self>>,
174    ) -> Option<String> {
175        None
176    }
177
178    fn editor_position(&self) -> PickerEditorPosition {
179        PickerEditorPosition::default()
180    }
181
182    fn render_editor(
183        &self,
184        editor: &Entity<Editor>,
185        _window: &mut Window,
186        _cx: &mut Context<Picker<Self>>,
187    ) -> Div {
188        v_flex()
189            .when(
190                self.editor_position() == PickerEditorPosition::End,
191                |this| this.child(Divider::horizontal()),
192            )
193            .child(
194                h_flex()
195                    .overflow_hidden()
196                    .flex_none()
197                    .h_9()
198                    .px_2p5()
199                    .child(editor.clone()),
200            )
201            .when(
202                self.editor_position() == PickerEditorPosition::Start,
203                |this| this.child(Divider::horizontal()),
204            )
205    }
206
207    fn render_match(
208        &self,
209        ix: usize,
210        selected: bool,
211        window: &mut Window,
212        cx: &mut Context<Picker<Self>>,
213    ) -> Option<Self::ListItem>;
214
215    fn render_header(
216        &self,
217        _window: &mut Window,
218        _: &mut Context<Picker<Self>>,
219    ) -> Option<AnyElement> {
220        None
221    }
222
223    fn render_footer(
224        &self,
225        _window: &mut Window,
226        _: &mut Context<Picker<Self>>,
227    ) -> Option<AnyElement> {
228        None
229    }
230}
231
232impl<D: PickerDelegate> Focusable for Picker<D> {
233    fn focus_handle(&self, cx: &App) -> FocusHandle {
234        match &self.head {
235            Head::Editor(editor) => editor.focus_handle(cx),
236            Head::Empty(head) => head.focus_handle(cx),
237        }
238    }
239}
240
241#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
242enum ContainerKind {
243    List,
244    UniformList,
245}
246
247impl<D: PickerDelegate> Picker<D> {
248    /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height.
249    /// The picker allows the user to perform search items by text.
250    /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`.
251    pub fn uniform_list(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
252        let head = Head::editor(
253            delegate.placeholder_text(window, cx),
254            Self::on_input_editor_event,
255            window,
256            cx,
257        );
258
259        Self::new(delegate, ContainerKind::UniformList, head, window, cx)
260    }
261
262    /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height.
263    /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`.
264    pub fn nonsearchable_uniform_list(
265        delegate: D,
266        window: &mut Window,
267        cx: &mut Context<Self>,
268    ) -> Self {
269        let head = Head::empty(Self::on_empty_head_blur, window, cx);
270
271        Self::new(delegate, ContainerKind::UniformList, head, window, cx)
272    }
273
274    /// A picker, which displays its matches using `gpui::list`, matches can have different heights.
275    /// The picker allows the user to perform search items by text.
276    /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that.
277    pub fn list(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
278        let head = Head::editor(
279            delegate.placeholder_text(window, cx),
280            Self::on_input_editor_event,
281            window,
282            cx,
283        );
284
285        Self::new(delegate, ContainerKind::List, head, window, cx)
286    }
287
288    fn new(
289        delegate: D,
290        container: ContainerKind,
291        head: Head,
292        window: &mut Window,
293        cx: &mut Context<Self>,
294    ) -> Self {
295        let element_container = Self::create_element_container(container, cx);
296        let scrollbar_state = match &element_container {
297            ElementContainer::UniformList(scroll_handle) => {
298                ScrollbarState::new(scroll_handle.clone())
299            }
300            ElementContainer::List(state) => ScrollbarState::new(state.clone()),
301        };
302        let focus_handle = cx.focus_handle();
303        let mut this = Self {
304            delegate,
305            head,
306            element_container,
307            pending_update_matches: None,
308            confirm_on_update: None,
309            width: None,
310            widest_item: None,
311            max_height: Some(rems(18.).into()),
312            focus_handle,
313            show_scrollbar: false,
314            scrollbar_visibility: true,
315            scrollbar_state,
316            is_modal: true,
317            hide_scrollbar_task: None,
318        };
319        this.update_matches("".to_string(), window, cx);
320        // give the delegate 4ms to render the first set of suggestions.
321        this.delegate
322            .finalize_update_matches("".to_string(), Duration::from_millis(4), window, cx);
323        this
324    }
325
326    fn create_element_container(
327        container: ContainerKind,
328        cx: &mut Context<Self>,
329    ) -> ElementContainer {
330        match container {
331            ContainerKind::UniformList => {
332                ElementContainer::UniformList(UniformListScrollHandle::new())
333            }
334            ContainerKind::List => {
335                let entity = cx.entity().downgrade();
336                ElementContainer::List(ListState::new(
337                    0,
338                    gpui::ListAlignment::Top,
339                    px(1000.),
340                    move |ix, window, cx| {
341                        entity
342                            .upgrade()
343                            .map(|entity| {
344                                entity.update(cx, |this, cx| {
345                                    this.render_element(window, cx, ix).into_any_element()
346                                })
347                            })
348                            .unwrap_or_else(|| div().into_any_element())
349                    },
350                ))
351            }
352        }
353    }
354
355    pub fn width(mut self, width: impl Into<gpui::Length>) -> Self {
356        self.width = Some(width.into());
357        self
358    }
359
360    pub fn widest_item(mut self, ix: Option<usize>) -> Self {
361        self.widest_item = ix;
362        self
363    }
364
365    pub fn max_height(mut self, max_height: Option<gpui::Length>) -> Self {
366        self.max_height = max_height;
367        self
368    }
369
370    pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self {
371        self.show_scrollbar = show_scrollbar;
372        self
373    }
374
375    pub fn modal(mut self, modal: bool) -> Self {
376        self.is_modal = modal;
377        self
378    }
379
380    pub fn focus(&self, window: &mut Window, cx: &mut App) {
381        self.focus_handle(cx).focus(window);
382    }
383
384    /// Handles the selecting an index, and passing the change to the delegate.
385    /// If `fallback_direction` is set to `None`, the index will not be selected
386    /// if the element at that index cannot be selected.
387    /// If `fallback_direction` is set to
388    /// `Some(..)`, the next selectable element will be selected in the
389    /// specified direction (Down or Up), cycling through all elements until
390    /// finding one that can be selected or returning if there are no selectable elements.
391    /// If `scroll_to_index` is true, the new selected index will be scrolled into
392    /// view.
393    ///
394    /// If some effect is bound to `selected_index_changed`, it will be executed.
395    pub fn set_selected_index(
396        &mut self,
397        mut ix: usize,
398        fallback_direction: Option<Direction>,
399        scroll_to_index: bool,
400        window: &mut Window,
401        cx: &mut Context<Self>,
402    ) {
403        let match_count = self.delegate.match_count();
404        if match_count == 0 {
405            return;
406        }
407
408        if let Some(bias) = fallback_direction {
409            let mut curr_ix = ix;
410            while !self.delegate.can_select(curr_ix, window, cx) {
411                curr_ix = match bias {
412                    Direction::Down => {
413                        if curr_ix == match_count - 1 {
414                            0
415                        } else {
416                            curr_ix + 1
417                        }
418                    }
419                    Direction::Up => {
420                        if curr_ix == 0 {
421                            match_count - 1
422                        } else {
423                            curr_ix - 1
424                        }
425                    }
426                };
427                // There is no item that can be selected
428                if ix == curr_ix {
429                    return;
430                }
431            }
432            ix = curr_ix;
433        } else if !self.delegate.can_select(ix, window, cx) {
434            return;
435        }
436
437        let previous_index = self.delegate.selected_index();
438        self.delegate.set_selected_index(ix, window, cx);
439        let current_index = self.delegate.selected_index();
440
441        if previous_index != current_index {
442            if let Some(action) = self.delegate.selected_index_changed(ix, window, cx) {
443                action(window, cx);
444            }
445            if scroll_to_index {
446                self.scroll_to_item_index(ix);
447            }
448        }
449    }
450
451    pub fn select_next(
452        &mut self,
453        _: &menu::SelectNext,
454        window: &mut Window,
455        cx: &mut Context<Self>,
456    ) {
457        let count = self.delegate.match_count();
458        if count > 0 {
459            let index = self.delegate.selected_index();
460            let ix = if index == count - 1 { 0 } else { index + 1 };
461            self.set_selected_index(ix, Some(Direction::Down), true, window, cx);
462            cx.notify();
463        }
464    }
465
466    pub fn editor_move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
467        self.select_previous(&Default::default(), window, cx);
468    }
469
470    fn select_previous(
471        &mut self,
472        _: &menu::SelectPrevious,
473        window: &mut Window,
474        cx: &mut Context<Self>,
475    ) {
476        let count = self.delegate.match_count();
477        if count > 0 {
478            let index = self.delegate.selected_index();
479            let ix = if index == 0 { count - 1 } else { index - 1 };
480            self.set_selected_index(ix, Some(Direction::Up), true, window, cx);
481            cx.notify();
482        }
483    }
484
485    pub fn editor_move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
486        self.select_next(&Default::default(), window, cx);
487    }
488
489    pub fn select_first(
490        &mut self,
491        _: &menu::SelectFirst,
492        window: &mut Window,
493        cx: &mut Context<Self>,
494    ) {
495        let count = self.delegate.match_count();
496        if count > 0 {
497            self.set_selected_index(0, Some(Direction::Down), true, window, cx);
498            cx.notify();
499        }
500    }
501
502    fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
503        let count = self.delegate.match_count();
504        if count > 0 {
505            self.set_selected_index(count - 1, Some(Direction::Up), true, window, cx);
506            cx.notify();
507        }
508    }
509
510    pub fn cycle_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
511        let count = self.delegate.match_count();
512        let index = self.delegate.selected_index();
513        let new_index = if index + 1 == count { 0 } else { index + 1 };
514        self.set_selected_index(new_index, Some(Direction::Down), true, window, cx);
515        cx.notify();
516    }
517
518    pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
519        if self.delegate.should_dismiss() {
520            self.delegate.dismissed(window, cx);
521            cx.emit(DismissEvent);
522        }
523    }
524
525    fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
526        if self.pending_update_matches.is_some()
527            && !self.delegate.finalize_update_matches(
528                self.query(cx),
529                Duration::from_millis(16),
530                window,
531                cx,
532            )
533        {
534            self.confirm_on_update = Some(false)
535        } else {
536            self.pending_update_matches.take();
537            self.do_confirm(false, window, cx);
538        }
539    }
540
541    fn secondary_confirm(
542        &mut self,
543        _: &menu::SecondaryConfirm,
544        window: &mut Window,
545        cx: &mut Context<Self>,
546    ) {
547        if self.pending_update_matches.is_some()
548            && !self.delegate.finalize_update_matches(
549                self.query(cx),
550                Duration::from_millis(16),
551                window,
552                cx,
553            )
554        {
555            self.confirm_on_update = Some(true)
556        } else {
557            self.do_confirm(true, window, cx);
558        }
559    }
560
561    fn confirm_input(&mut self, input: &ConfirmInput, window: &mut Window, cx: &mut Context<Self>) {
562        self.delegate.confirm_input(input.secondary, window, cx);
563    }
564
565    fn confirm_completion(
566        &mut self,
567        _: &ConfirmCompletion,
568        window: &mut Window,
569        cx: &mut Context<Self>,
570    ) {
571        if let Some(new_query) = self.delegate.confirm_completion(self.query(cx), window, cx) {
572            self.set_query(new_query, window, cx);
573        } else {
574            cx.propagate()
575        }
576    }
577
578    fn handle_click(
579        &mut self,
580        ix: usize,
581        secondary: bool,
582        window: &mut Window,
583        cx: &mut Context<Self>,
584    ) {
585        cx.stop_propagation();
586        window.prevent_default();
587        self.set_selected_index(ix, None, false, window, cx);
588        self.do_confirm(secondary, window, cx)
589    }
590
591    fn do_confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Self>) {
592        if let Some(update_query) = self.delegate.confirm_update_query(window, cx) {
593            self.set_query(update_query, window, cx);
594            self.set_selected_index(0, Some(Direction::Down), false, window, cx);
595        } else {
596            self.delegate.confirm(secondary, window, cx)
597        }
598    }
599
600    fn on_input_editor_event(
601        &mut self,
602        _: &Entity<Editor>,
603        event: &editor::EditorEvent,
604        window: &mut Window,
605        cx: &mut Context<Self>,
606    ) {
607        let Head::Editor(editor) = &self.head else {
608            panic!("unexpected call");
609        };
610        match event {
611            editor::EditorEvent::BufferEdited => {
612                let query = editor.read(cx).text(cx);
613                self.update_matches(query, window, cx);
614            }
615            editor::EditorEvent::Blurred => {
616                if self.is_modal {
617                    self.cancel(&menu::Cancel, window, cx);
618                }
619            }
620            _ => {}
621        }
622    }
623
624    fn on_empty_head_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
625        let Head::Empty(_) = &self.head else {
626            panic!("unexpected call");
627        };
628        self.cancel(&menu::Cancel, window, cx);
629    }
630
631    pub fn refresh_placeholder(&mut self, window: &mut Window, cx: &mut App) {
632        match &self.head {
633            Head::Editor(editor) => {
634                let placeholder = self.delegate.placeholder_text(window, cx);
635                editor.update(cx, |editor, cx| {
636                    editor.set_placeholder_text(placeholder, cx);
637                    cx.notify();
638                });
639            }
640            Head::Empty(_) => {}
641        }
642    }
643
644    pub fn refresh(&mut self, window: &mut Window, cx: &mut Context<Self>) {
645        let query = self.query(cx);
646        self.update_matches(query, window, cx);
647    }
648
649    pub fn update_matches(&mut self, query: String, window: &mut Window, cx: &mut Context<Self>) {
650        let delegate_pending_update_matches = self.delegate.update_matches(query, window, cx);
651
652        self.matches_updated(window, cx);
653        // This struct ensures that we can synchronously drop the task returned by the
654        // delegate's `update_matches` method and the task that the picker is spawning.
655        // If we simply capture the delegate's task into the picker's task, when the picker's
656        // task gets synchronously dropped, the delegate's task would keep running until
657        // the picker's task has a chance of being scheduled, because dropping a task happens
658        // asynchronously.
659        self.pending_update_matches = Some(PendingUpdateMatches {
660            delegate_update_matches: Some(delegate_pending_update_matches),
661            _task: cx.spawn_in(window, async move |this, cx| {
662                let delegate_pending_update_matches = this.update(cx, |this, _| {
663                    this.pending_update_matches
664                        .as_mut()
665                        .unwrap()
666                        .delegate_update_matches
667                        .take()
668                        .unwrap()
669                })?;
670                delegate_pending_update_matches.await;
671                this.update_in(cx, |this, window, cx| {
672                    this.matches_updated(window, cx);
673                })
674            }),
675        });
676    }
677
678    fn matches_updated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
679        if let ElementContainer::List(state) = &mut self.element_container {
680            state.reset(self.delegate.match_count());
681        }
682
683        let index = self.delegate.selected_index();
684        self.scroll_to_item_index(index);
685        self.pending_update_matches = None;
686        if let Some(secondary) = self.confirm_on_update.take() {
687            self.do_confirm(secondary, window, cx);
688        }
689        cx.notify();
690    }
691
692    pub fn query(&self, cx: &App) -> String {
693        match &self.head {
694            Head::Editor(editor) => editor.read(cx).text(cx),
695            Head::Empty(_) => "".to_string(),
696        }
697    }
698
699    pub fn set_query(&self, query: impl Into<Arc<str>>, window: &mut Window, cx: &mut App) {
700        if let Head::Editor(editor) = &self.head {
701            editor.update(cx, |editor, cx| {
702                editor.set_text(query, window, cx);
703                let editor_offset = editor.buffer().read(cx).len(cx);
704                editor.change_selections(
705                    SelectionEffects::scroll(Autoscroll::Next),
706                    window,
707                    cx,
708                    |s| s.select_ranges(Some(editor_offset..editor_offset)),
709                );
710            });
711        }
712    }
713
714    fn scroll_to_item_index(&mut self, ix: usize) {
715        match &mut self.element_container {
716            ElementContainer::List(state) => state.scroll_to_reveal_item(ix),
717            ElementContainer::UniformList(scroll_handle) => {
718                scroll_handle.scroll_to_item(ix, ScrollStrategy::Top)
719            }
720        }
721    }
722
723    fn render_element(
724        &self,
725        window: &mut Window,
726        cx: &mut Context<Self>,
727        ix: usize,
728    ) -> impl IntoElement + use<D> {
729        div()
730            .id(("item", ix))
731            .cursor_pointer()
732            .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
733                this.handle_click(ix, event.modifiers().secondary(), window, cx)
734            }))
735            // As of this writing, GPUI intercepts `ctrl-[mouse-event]`s on macOS
736            // and produces right mouse button events. This matches platforms norms
737            // but means that UIs which depend on holding ctrl down (such as the tab
738            // switcher) can't be clicked on. Hence, this handler.
739            .on_mouse_up(
740                MouseButton::Right,
741                cx.listener(move |this, event: &MouseUpEvent, window, cx| {
742                    // We specifically want to use the platform key here, as
743                    // ctrl will already be held down for the tab switcher.
744                    this.handle_click(ix, event.modifiers.platform, window, cx)
745                }),
746            )
747            .children(self.delegate.render_match(
748                ix,
749                ix == self.delegate.selected_index(),
750                window,
751                cx,
752            ))
753            .when(
754                self.delegate.separators_after_indices().contains(&ix),
755                |picker| {
756                    picker
757                        .border_color(cx.theme().colors().border_variant)
758                        .border_b_1()
759                        .py(px(-1.0))
760                },
761            )
762    }
763
764    fn render_element_container(&self, cx: &mut Context<Self>) -> impl IntoElement {
765        let sizing_behavior = if self.max_height.is_some() {
766            ListSizingBehavior::Infer
767        } else {
768            ListSizingBehavior::Auto
769        };
770
771        match &self.element_container {
772            ElementContainer::UniformList(scroll_handle) => uniform_list(
773                "candidates",
774                self.delegate.match_count(),
775                cx.processor(move |picker, visible_range: Range<usize>, window, cx| {
776                    visible_range
777                        .map(|ix| picker.render_element(window, cx, ix))
778                        .collect()
779                }),
780            )
781            .with_sizing_behavior(sizing_behavior)
782            .when_some(self.widest_item, |el, widest_item| {
783                el.with_width_from_item(Some(widest_item))
784            })
785            .flex_grow()
786            .py_1()
787            .track_scroll(scroll_handle.clone())
788            .into_any_element(),
789            ElementContainer::List(state) => list(state.clone())
790                .with_sizing_behavior(sizing_behavior)
791                .flex_grow()
792                .py_2()
793                .into_any_element(),
794        }
795    }
796
797    #[cfg(any(test, feature = "test-support"))]
798    pub fn logical_scroll_top_index(&self) -> usize {
799        match &self.element_container {
800            ElementContainer::List(state) => state.logical_scroll_top().item_ix,
801            ElementContainer::UniformList(scroll_handle) => {
802                scroll_handle.logical_scroll_top_index()
803            }
804        }
805    }
806
807    fn hide_scrollbar(&mut self, cx: &mut Context<Self>) {
808        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
809        self.hide_scrollbar_task = Some(cx.spawn(async move |panel, cx| {
810            cx.background_executor()
811                .timer(SCROLLBAR_SHOW_INTERVAL)
812                .await;
813            panel
814                .update(cx, |panel, cx| {
815                    panel.scrollbar_visibility = false;
816                    cx.notify();
817                })
818                .log_err();
819        }))
820    }
821
822    fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
823        if !self.show_scrollbar
824            || !(self.scrollbar_visibility || self.scrollbar_state.is_dragging())
825        {
826            return None;
827        }
828        Some(
829            div()
830                .occlude()
831                .id("picker-scroll")
832                .h_full()
833                .absolute()
834                .right_1()
835                .top_1()
836                .bottom_0()
837                .w(px(12.))
838                .cursor_default()
839                .on_mouse_move(cx.listener(|_, _, _window, cx| {
840                    cx.notify();
841                    cx.stop_propagation()
842                }))
843                .on_hover(|_, _window, cx| {
844                    cx.stop_propagation();
845                })
846                .on_any_mouse_down(|_, _window, cx| {
847                    cx.stop_propagation();
848                })
849                .on_mouse_up(
850                    MouseButton::Left,
851                    cx.listener(|picker, _, window, cx| {
852                        if !picker.scrollbar_state.is_dragging()
853                            && !picker.focus_handle.contains_focused(window, cx)
854                        {
855                            picker.hide_scrollbar(cx);
856                            cx.notify();
857                        }
858                        cx.stop_propagation();
859                    }),
860                )
861                .on_scroll_wheel(cx.listener(|_, _, _window, cx| {
862                    cx.notify();
863                }))
864                .children(Scrollbar::vertical(self.scrollbar_state.clone())),
865        )
866    }
867}
868
869impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
870impl<D: PickerDelegate> ModalView for Picker<D> {}
871
872impl<D: PickerDelegate> Render for Picker<D> {
873    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
874        let editor_position = self.delegate.editor_position();
875        v_flex()
876            .key_context("Picker")
877            .size_full()
878            .when_some(self.width, |el, width| el.w(width))
879            .overflow_hidden()
880            // This is a bit of a hack to remove the modal styling when we're rendering the `Picker`
881            // as a part of a modal rather than the entire modal.
882            //
883            // We should revisit how the `Picker` is styled to make it more composable.
884            .when(self.is_modal, |this| this.elevation_3(cx))
885            .on_action(cx.listener(Self::select_next))
886            .on_action(cx.listener(Self::select_previous))
887            .on_action(cx.listener(Self::editor_move_down))
888            .on_action(cx.listener(Self::editor_move_up))
889            .on_action(cx.listener(Self::select_first))
890            .on_action(cx.listener(Self::select_last))
891            .on_action(cx.listener(Self::cancel))
892            .on_action(cx.listener(Self::confirm))
893            .on_action(cx.listener(Self::secondary_confirm))
894            .on_action(cx.listener(Self::confirm_completion))
895            .on_action(cx.listener(Self::confirm_input))
896            .children(match &self.head {
897                Head::Editor(editor) => {
898                    if editor_position == PickerEditorPosition::Start {
899                        Some(self.delegate.render_editor(&editor.clone(), window, cx))
900                    } else {
901                        None
902                    }
903                }
904                Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
905            })
906            .when(self.delegate.match_count() > 0, |el| {
907                el.child(
908                    v_flex()
909                        .id("element-container")
910                        .relative()
911                        .flex_grow()
912                        .when_some(self.max_height, |div, max_h| div.max_h(max_h))
913                        .overflow_hidden()
914                        .children(self.delegate.render_header(window, cx))
915                        .child(self.render_element_container(cx))
916                        .on_hover(cx.listener(|this, hovered, window, cx| {
917                            if *hovered {
918                                this.scrollbar_visibility = true;
919                                this.hide_scrollbar_task.take();
920                                cx.notify();
921                            } else if !this.focus_handle.contains_focused(window, cx) {
922                                this.hide_scrollbar(cx);
923                            }
924                        }))
925                        .when_some(self.render_scrollbar(cx), |div, scrollbar| {
926                            div.child(scrollbar)
927                        }),
928                )
929            })
930            .when(self.delegate.match_count() == 0, |el| {
931                el.when_some(self.delegate.no_matches_text(window, cx), |el, text| {
932                    el.child(
933                        v_flex().flex_grow().py_2().child(
934                            ListItem::new("empty_state")
935                                .inset(true)
936                                .spacing(ListItemSpacing::Sparse)
937                                .disabled(true)
938                                .child(Label::new(text).color(Color::Muted)),
939                        ),
940                    )
941                })
942            })
943            .children(self.delegate.render_footer(window, cx))
944            .children(match &self.head {
945                Head::Editor(editor) => {
946                    if editor_position == PickerEditorPosition::End {
947                        Some(self.delegate.render_editor(&editor.clone(), window, cx))
948                    } else {
949                        None
950                    }
951                }
952                Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
953            })
954    }
955}