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 if self.is_modal {
592 self.cancel(&menu::Cancel, window, cx);
593 }
594 }
595 _ => {}
596 }
597 }
598
599 fn on_empty_head_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
600 let Head::Empty(_) = &self.head else {
601 panic!("unexpected call");
602 };
603 self.cancel(&menu::Cancel, window, cx);
604 }
605
606 pub fn refresh_placeholder(&mut self, window: &mut Window, cx: &mut App) {
607 match &self.head {
608 Head::Editor(editor) => {
609 let placeholder = self.delegate.placeholder_text(window, cx);
610 editor.update(cx, |editor, cx| {
611 editor.set_placeholder_text(placeholder, cx);
612 cx.notify();
613 });
614 }
615 Head::Empty(_) => {}
616 }
617 }
618
619 pub fn refresh(&mut self, window: &mut Window, cx: &mut Context<Self>) {
620 let query = self.query(cx);
621 self.update_matches(query, window, cx);
622 }
623
624 pub fn update_matches(&mut self, query: String, window: &mut Window, cx: &mut Context<Self>) {
625 let delegate_pending_update_matches = self.delegate.update_matches(query, window, cx);
626
627 self.matches_updated(window, cx);
628 // This struct ensures that we can synchronously drop the task returned by the
629 // delegate's `update_matches` method and the task that the picker is spawning.
630 // If we simply capture the delegate's task into the picker's task, when the picker's
631 // task gets synchronously dropped, the delegate's task would keep running until
632 // the picker's task has a chance of being scheduled, because dropping a task happens
633 // asynchronously.
634 self.pending_update_matches = Some(PendingUpdateMatches {
635 delegate_update_matches: Some(delegate_pending_update_matches),
636 _task: cx.spawn_in(window, async move |this, cx| {
637 let delegate_pending_update_matches = this.update(cx, |this, _| {
638 this.pending_update_matches
639 .as_mut()
640 .unwrap()
641 .delegate_update_matches
642 .take()
643 .unwrap()
644 })?;
645 delegate_pending_update_matches.await;
646 this.update_in(cx, |this, window, cx| {
647 this.matches_updated(window, cx);
648 })
649 }),
650 });
651 }
652
653 fn matches_updated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
654 if let ElementContainer::List(state) = &mut self.element_container {
655 state.reset(self.delegate.match_count());
656 }
657
658 let index = self.delegate.selected_index();
659 self.scroll_to_item_index(index);
660 self.pending_update_matches = None;
661 if let Some(secondary) = self.confirm_on_update.take() {
662 self.do_confirm(secondary, window, cx);
663 }
664 cx.notify();
665 }
666
667 pub fn query(&self, cx: &App) -> String {
668 match &self.head {
669 Head::Editor(editor) => editor.read(cx).text(cx),
670 Head::Empty(_) => "".to_string(),
671 }
672 }
673
674 pub fn set_query(&self, query: impl Into<Arc<str>>, window: &mut Window, cx: &mut App) {
675 if let Head::Editor(editor) = &self.head {
676 editor.update(cx, |editor, cx| {
677 editor.set_text(query, window, cx);
678 let editor_offset = editor.buffer().read(cx).len(cx);
679 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
680 s.select_ranges(Some(editor_offset..editor_offset))
681 });
682 });
683 }
684 }
685
686 fn scroll_to_item_index(&mut self, ix: usize) {
687 match &mut self.element_container {
688 ElementContainer::List(state) => state.scroll_to_reveal_item(ix),
689 ElementContainer::UniformList(scroll_handle) => {
690 scroll_handle.scroll_to_item(ix, ScrollStrategy::Top)
691 }
692 }
693 }
694
695 fn render_element(
696 &self,
697 window: &mut Window,
698 cx: &mut Context<Self>,
699 ix: usize,
700 ) -> impl IntoElement + use<D> {
701 div()
702 .id(("item", ix))
703 .cursor_pointer()
704 .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
705 this.handle_click(ix, event.modifiers().secondary(), window, cx)
706 }))
707 // As of this writing, GPUI intercepts `ctrl-[mouse-event]`s on macOS
708 // and produces right mouse button events. This matches platforms norms
709 // but means that UIs which depend on holding ctrl down (such as the tab
710 // switcher) can't be clicked on. Hence, this handler.
711 .on_mouse_up(
712 MouseButton::Right,
713 cx.listener(move |this, event: &MouseUpEvent, window, cx| {
714 // We specifically want to use the platform key here, as
715 // ctrl will already be held down for the tab switcher.
716 this.handle_click(ix, event.modifiers.platform, window, cx)
717 }),
718 )
719 .children(self.delegate.render_match(
720 ix,
721 ix == self.delegate.selected_index(),
722 window,
723 cx,
724 ))
725 .when(
726 self.delegate.separators_after_indices().contains(&ix),
727 |picker| {
728 picker
729 .border_color(cx.theme().colors().border_variant)
730 .border_b_1()
731 .py(px(-1.0))
732 },
733 )
734 }
735
736 fn render_element_container(&self, cx: &mut Context<Self>) -> impl IntoElement {
737 let sizing_behavior = if self.max_height.is_some() {
738 ListSizingBehavior::Infer
739 } else {
740 ListSizingBehavior::Auto
741 };
742
743 match &self.element_container {
744 ElementContainer::UniformList(scroll_handle) => uniform_list(
745 cx.entity().clone(),
746 "candidates",
747 self.delegate.match_count(),
748 move |picker, visible_range, window, cx| {
749 visible_range
750 .map(|ix| picker.render_element(window, cx, ix))
751 .collect()
752 },
753 )
754 .with_sizing_behavior(sizing_behavior)
755 .when_some(self.widest_item, |el, widest_item| {
756 el.with_width_from_item(Some(widest_item))
757 })
758 .flex_grow()
759 .py_1()
760 .track_scroll(scroll_handle.clone())
761 .into_any_element(),
762 ElementContainer::List(state) => list(state.clone())
763 .with_sizing_behavior(sizing_behavior)
764 .flex_grow()
765 .py_2()
766 .into_any_element(),
767 }
768 }
769
770 #[cfg(any(test, feature = "test-support"))]
771 pub fn logical_scroll_top_index(&self) -> usize {
772 match &self.element_container {
773 ElementContainer::List(state) => state.logical_scroll_top().item_ix,
774 ElementContainer::UniformList(scroll_handle) => {
775 scroll_handle.logical_scroll_top_index()
776 }
777 }
778 }
779
780 fn hide_scrollbar(&mut self, cx: &mut Context<Self>) {
781 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
782 self.hide_scrollbar_task = Some(cx.spawn(async move |panel, cx| {
783 cx.background_executor()
784 .timer(SCROLLBAR_SHOW_INTERVAL)
785 .await;
786 panel
787 .update(cx, |panel, cx| {
788 panel.scrollbar_visibility = false;
789 cx.notify();
790 })
791 .log_err();
792 }))
793 }
794
795 fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
796 if !self.show_scrollbar
797 || !(self.scrollbar_visibility || self.scrollbar_state.is_dragging())
798 {
799 return None;
800 }
801 Some(
802 div()
803 .occlude()
804 .id("picker-scroll")
805 .h_full()
806 .absolute()
807 .right_1()
808 .top_1()
809 .bottom_0()
810 .w(px(12.))
811 .cursor_default()
812 .on_mouse_move(cx.listener(|_, _, _window, cx| {
813 cx.notify();
814 cx.stop_propagation()
815 }))
816 .on_hover(|_, _window, cx| {
817 cx.stop_propagation();
818 })
819 .on_any_mouse_down(|_, _window, cx| {
820 cx.stop_propagation();
821 })
822 .on_mouse_up(
823 MouseButton::Left,
824 cx.listener(|picker, _, window, cx| {
825 if !picker.scrollbar_state.is_dragging()
826 && !picker.focus_handle.contains_focused(window, cx)
827 {
828 picker.hide_scrollbar(cx);
829 cx.notify();
830 }
831 cx.stop_propagation();
832 }),
833 )
834 .on_scroll_wheel(cx.listener(|_, _, _window, cx| {
835 cx.notify();
836 }))
837 .children(Scrollbar::vertical(self.scrollbar_state.clone())),
838 )
839 }
840}
841
842impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
843impl<D: PickerDelegate> ModalView for Picker<D> {}
844
845impl<D: PickerDelegate> Render for Picker<D> {
846 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
847 let editor_position = self.delegate.editor_position();
848 v_flex()
849 .key_context("Picker")
850 .size_full()
851 .when_some(self.width, |el, width| el.w(width))
852 .overflow_hidden()
853 // This is a bit of a hack to remove the modal styling when we're rendering the `Picker`
854 // as a part of a modal rather than the entire modal.
855 //
856 // We should revisit how the `Picker` is styled to make it more composable.
857 .when(self.is_modal, |this| this.elevation_3(cx))
858 .on_action(cx.listener(Self::select_next))
859 .on_action(cx.listener(Self::select_previous))
860 .on_action(cx.listener(Self::select_first))
861 .on_action(cx.listener(Self::select_last))
862 .on_action(cx.listener(Self::cancel))
863 .on_action(cx.listener(Self::confirm))
864 .on_action(cx.listener(Self::secondary_confirm))
865 .on_action(cx.listener(Self::confirm_completion))
866 .on_action(cx.listener(Self::confirm_input))
867 .children(match &self.head {
868 Head::Editor(editor) => {
869 if editor_position == PickerEditorPosition::Start {
870 Some(self.delegate.render_editor(&editor.clone(), window, cx))
871 } else {
872 None
873 }
874 }
875 Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
876 })
877 .when(self.delegate.match_count() > 0, |el| {
878 el.child(
879 v_flex()
880 .id("element-container")
881 .relative()
882 .flex_grow()
883 .when_some(self.max_height, |div, max_h| div.max_h(max_h))
884 .overflow_hidden()
885 .children(self.delegate.render_header(window, cx))
886 .child(self.render_element_container(cx))
887 .on_hover(cx.listener(|this, hovered, window, cx| {
888 if *hovered {
889 this.scrollbar_visibility = true;
890 this.hide_scrollbar_task.take();
891 cx.notify();
892 } else if !this.focus_handle.contains_focused(window, cx) {
893 this.hide_scrollbar(cx);
894 }
895 }))
896 .when_some(self.render_scrollbar(cx), |div, scrollbar| {
897 div.child(scrollbar)
898 }),
899 )
900 })
901 .when(self.delegate.match_count() == 0, |el| {
902 el.when_some(self.delegate.no_matches_text(window, cx), |el, text| {
903 el.child(
904 v_flex().flex_grow().py_2().child(
905 ListItem::new("empty_state")
906 .inset(true)
907 .spacing(ListItemSpacing::Sparse)
908 .disabled(true)
909 .child(Label::new(text).color(Color::Muted)),
910 ),
911 )
912 })
913 })
914 .children(self.delegate.render_footer(window, cx))
915 .children(match &self.head {
916 Head::Editor(editor) => {
917 if editor_position == PickerEditorPosition::End {
918 Some(self.delegate.render_editor(&editor.clone(), window, cx))
919 } else {
920 None
921 }
922 }
923 Head::Empty(empty_head) => Some(div().child(empty_head.clone())),
924 })
925 }
926}