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