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