picker.rs

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