Skip to repository content1940 lines · 68.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:38:32.327Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
picker.rs
1use anyhow::Result;
2use gpui::Action;
3use gpui::{
4 AnyElement, App, Bounds, ClickEvent, Context, DismissEvent, EventEmitter, FocusHandle,
5 Focusable, ListSizingBehavior, ListState, MouseButton, MouseUpEvent, Pixels, ScrollStrategy,
6 Task, UniformListScrollHandle, Window, actions, canvas, div, list, prelude::*, uniform_list,
7};
8use head::Head;
9use schemars::JsonSchema;
10use serde::Deserialize;
11use std::{
12 cell::Cell, cell::RefCell, collections::HashMap, ops::Range, rc::Rc, sync::Arc, time::Duration,
13};
14use ui::{
15 Checkbox, ContextMenu, Divider, DocumentationAside, KeyBinding, PopoverMenuHandle, Tooltip,
16 prelude::*,
17};
18use ui_input::ErasedEditorEvent;
19use util::ResultExt;
20use workspace::ModalView;
21use zed_actions::editor::{MoveDown, MoveUp};
22
23mod footer;
24mod head;
25pub mod highlighted_match_with_paths;
26pub mod parts;
27mod persistence;
28pub mod popover_menu;
29mod preview;
30mod render;
31mod shape;
32
33use crate::shape::RelativeHeight;
34use crate::shape::RelativeWidth;
35pub use footer::PickerAction;
36pub use language::{HighlightedText, HighlightedTextBuilder};
37pub use preview::Layout as PreviewLayout;
38pub use preview::MatchLocation;
39pub use preview::Preview;
40pub use preview::PreviewBackend;
41pub use preview::PreviewSource;
42pub use preview::Update as PreviewUpdate;
43pub use ui_input::ErasedEditor;
44
45pub const DEFAULT_MODAL_WIDTH: Rems = Rems(34.0);
46pub const DEFAULT_MODAL_MAX_HEIGHT: Rems = Rems(24.0);
47
48enum ElementContainer {
49 List(ListState),
50 UniformList(UniformListScrollHandle),
51}
52
53pub enum Direction {
54 Up,
55 Down,
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum ScrollBehavior {
60 RevealSelected,
61 PreserveOffset,
62}
63
64actions!(
65 picker,
66 [
67 /// Confirms the selected completion in the picker.
68 ConfirmCompletion,
69 /// Toggles the preview between hidden and visible.
70 TogglePreview,
71 /// Shows the preview to the right of the results.
72 SetPreviewRight,
73 /// Shows the preview below the results.
74 SetPreviewBelow,
75 /// Hides the preview.
76 SetPreviewHidden,
77 /// Opens the footer's actions menu.
78 ToggleActionsMenu,
79 /// Toggles multi-select mode, in which clicking items adds them to
80 /// the selection instead of opening them
81 ToggleMultiSelect,
82 /// Toggles the current item in the multi-selection and advances to
83 /// the next item, starting multi-select mode if it isn't already
84 /// active
85 MultiSelectNext,
86 ]
87);
88
89/// ConfirmInput is an alternative editor action which - instead of selecting active picker entry - treats pickers editor input literally,
90/// performing some kind of action on it.
91#[derive(Clone, PartialEq, Deserialize, JsonSchema, Default, Action)]
92#[action(namespace = picker)]
93#[serde(deny_unknown_fields)]
94pub struct ConfirmInput {
95 pub secondary: bool,
96}
97
98struct PendingUpdateMatches {
99 delegate_update_matches: Option<Task<()>>,
100 _task: Task<Result<()>>,
101}
102
103#[derive(Clone, Copy)]
104enum Presentation {
105 /// A self-contained modal: draws its own elevated background and dismisses
106 /// when it loses focus. May optionally be resized (persisting its size);
107 /// resizing only makes sense for modals.
108 Modal { resizable: bool },
109 /// A popover attached to a menu/trigger: draws its own elevated background
110 /// and dismisses when it loses focus, but is never resizable.
111 Popover,
112 /// Embedded inside a larger container (e.g. another modal) that provides its
113 /// own chrome and handles dismissal.
114 Embedded,
115}
116
117/// The default size for a given preview layout. With the preview hidden the
118/// picker uses its standard size; showing a preview expands it to the larger
119/// "telescope" size so the results pane isn't cramped beside the preview.
120fn default_shape_for_layout(hidden: shape::Centered, layout: preview::Layout) -> shape::Centered {
121 match layout {
122 preview::Layout::Hidden => hidden,
123 preview::Layout::Right | preview::Layout::Below => shape::Centered::default(),
124 }
125}
126
127pub struct Picker<D: PickerDelegate> {
128 pub delegate: D,
129 element_container: ElementContainer,
130 head: Head,
131 preview: Option<Preview>,
132 pending_update_matches: Option<PendingUpdateMatches>,
133 confirm_on_update: Option<bool>,
134 select_instead_of_open: bool,
135 shape: shape::Shape,
136 /// The size the picker opens at (and resets to). Defaults depend on whether
137 /// the picker has a preview; see [`Picker::initial_width`] / [`Picker::max_height`].
138 default_shape: shape::Centered,
139 size_bounds: shape::SizeBounds,
140 /// An external control to display a scrollbar in the `Picker`.
141 show_scrollbar: bool,
142 /// How the picker is presented, which controls its chrome and behavior.
143 presentation: Presentation,
144 /// Bounds tracking for the picker container (for aside positioning)
145 picker_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
146 /// Bounds tracking for items (for aside positioning) - maps item index to bounds
147 item_bounds: Rc<RefCell<HashMap<usize, Bounds<Pixels>>>>,
148 shape_loaded_from_persistence: bool,
149 /// Handle for the default footer's Actions popover menu. Used to keep the
150 /// picker open while that menu has focus.
151 actions_menu_handle: PopoverMenuHandle<ContextMenu>,
152 reopenable: bool,
153}
154
155#[derive(Debug, Default, Clone, Copy, PartialEq)]
156pub enum PickerEditorPosition {
157 #[default]
158 /// Render the editor at the start of the picker. Usually the top
159 Start,
160 /// Render the editor at the end of the picker. Usually the bottom
161 End,
162}
163
164pub trait PickerDelegate: Sized + 'static {
165 type ListItem: IntoElement;
166
167 /// Name of the picker, this is the key for serialization. We could use the
168 /// typename of the delegate but then a rename would break persistence.
169 fn name() -> &'static str;
170 fn match_count(&self) -> usize;
171 fn selected_index(&self) -> usize;
172 fn separators_after_indices(&self) -> Vec<usize> {
173 Vec::new()
174 }
175 fn set_selected_index(
176 &mut self,
177 ix: usize,
178 window: &mut Window,
179 cx: &mut Context<Picker<Self>>,
180 );
181
182 /// Called before the picker handles `SelectPrevious` or `SelectNext`. Return `Some(query)` to
183 /// set a new query and prevent the default selection behavior.
184 fn select_history(
185 &mut self,
186 _direction: Direction,
187 _query: &str,
188 _window: &mut Window,
189 _cx: &mut App,
190 ) -> Option<String> {
191 None
192 }
193 fn can_select(
194 &self,
195 _ix: usize,
196 _window: &mut Window,
197 _cx: &mut Context<Picker<Self>>,
198 ) -> bool {
199 true
200 }
201 fn select_on_hover(&self) -> bool {
202 true
203 }
204
205 // Allows binding some optional effect to when the selection changes.
206 fn selected_index_changed(
207 &self,
208 _ix: usize,
209 _window: &mut Window,
210 _cx: &mut Context<Picker<Self>>,
211 ) -> Option<Box<dyn Fn(&mut Window, &mut App) + 'static>> {
212 None
213 }
214 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str>;
215 fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
216 Some("No matches".into())
217 }
218 fn update_matches(
219 &mut self,
220 query: String,
221 window: &mut Window,
222 cx: &mut Context<Picker<Self>>,
223 ) -> Task<()>;
224
225 // Delegates that support this method (e.g. the CommandPalette) can chose to block on any background
226 // work for up to `duration` to try and get a result synchronously.
227 // This avoids a flash of an empty command-palette on cmd-shift-p, and lets workspace::SendKeystrokes
228 // mostly work when dismissing a palette.
229 fn finalize_update_matches(
230 &mut self,
231 _query: String,
232 _duration: Duration,
233 _window: &mut Window,
234 _cx: &mut Context<Picker<Self>>,
235 ) -> bool {
236 false
237 }
238
239 /// Override if you want to have <enter> update the query instead of confirming.
240 fn confirm_update_query(
241 &mut self,
242 _window: &mut Window,
243 _cx: &mut Context<Picker<Self>>,
244 ) -> Option<String> {
245 None
246 }
247 fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>);
248 /// Whether this delegate supports selecting multiple items at once. When
249 /// `true`, the delegate owns the multi-selection state; the picker only
250 /// drives the generic UX (toggle action, indicator, click routing).
251 fn supports_multi_select(&self) -> bool {
252 false
253 }
254 /// Whether the item at `ix` is part of the current multi-selection.
255 fn is_item_selected(&self, _ix: usize) -> bool {
256 false
257 }
258 /// Toggle whether the item at `ix` is part of the multi-selection. Items
259 /// that cannot participate in multi-selection (e.g. non-file entries)
260 /// should leave the selection unchanged.
261 fn toggle_item_selected(
262 &mut self,
263 _ix: usize,
264 _window: &mut Window,
265 _cx: &mut Context<Picker<Self>>,
266 ) {
267 }
268 /// Number of items currently in the multi-selection.
269 fn selected_item_count(&self) -> usize {
270 0
271 }
272 /// Clear the multi-selection.
273 fn clear_selection(&mut self, _cx: &mut Context<Picker<Self>>) {}
274 /// Open every item in the multi-selection. Called on confirm when the
275 /// selection is non-empty; implementations should clear the selection.
276 fn confirm_multi(
277 &mut self,
278 _secondary: bool,
279 _window: &mut Window,
280 _cx: &mut Context<Picker<Self>>,
281 ) {
282 }
283 /// Instead of interacting with currently selected entry, treats editor input literally,
284 /// performing some kind of action on it.
285 fn confirm_input(
286 &mut self,
287 _secondary: bool,
288 _window: &mut Window,
289 _: &mut Context<Picker<Self>>,
290 ) {
291 }
292 fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>);
293 fn should_dismiss(&self) -> bool {
294 true
295 }
296 fn confirm_completion(
297 &mut self,
298 _query: String,
299 _window: &mut Window,
300 _: &mut Context<Picker<Self>>,
301 ) -> Option<String> {
302 None
303 }
304 /// Called when `SelectChild` fires (e.g. shift-right-arrow). Return `Some(query)`
305 /// to step into the currently selected item (e.g. a directory); the picker
306 /// will set the query and refresh matches.
307 fn select_child(
308 &mut self,
309 _window: &mut Window,
310 _cx: &mut Context<Picker<Self>>,
311 ) -> Option<String> {
312 None
313 }
314
315 /// Called when `SelectParent` fires (e.g. shift-left-arrow). Return `Some(query)`
316 /// to step back to the parent; the picker will set the query and refresh
317 /// matches.
318 fn select_parent(
319 &mut self,
320 _window: &mut Window,
321 _cx: &mut Context<Picker<Self>>,
322 ) -> Option<String> {
323 None
324 }
325
326 fn editor_position(&self) -> PickerEditorPosition {
327 PickerEditorPosition::default()
328 }
329
330 /// Prevent closing the modal on clicking in a popover menu that portrudes out
331 /// This is already set by the Actions menu from the picker, this is here to
332 /// support extra menus added by the delegate.
333 fn has_another_open_menu(&self, _window: &Window, _cx: &App) -> bool {
334 false
335 }
336
337 /// An optional control rendered at the trailing edge of the search bar, e.g.
338 /// a filter toggle. Returning `Some` is the easy way to add such a control;
339 /// for full control over the search bar, override [`Self::render_editor`].
340 fn searchbar_trailer(
341 &self,
342 _window: &mut Window,
343 _cx: &mut Context<Picker<Self>>,
344 ) -> Option<AnyElement> {
345 None
346 }
347
348 /// Overrides the search bar entirely. Most delegates should return `None`
349 /// to get the picker-rendered default (which includes
350 /// [`Self::searchbar_trailer`] and the multi-select toggle); override for
351 /// full control over the search bar.
352 fn render_editor(
353 &self,
354 _editor: &Arc<dyn ErasedEditor>,
355 _window: &mut Window,
356 _cx: &mut Context<Picker<Self>>,
357 ) -> Option<Div> {
358 None
359 }
360
361 fn try_get_preview_data_for_match(&self, _cx: &App) -> Option<PreviewUpdate> {
362 None
363 }
364
365 /// Called on the delegate when opening a preview to the side. Delegates can
366 /// then change how much space they use for rendering the match
367 fn preview_layout_changed(&mut self, _layout_is_horizontal: bool) {}
368
369 fn render_match(
370 &self,
371 ix: usize,
372 selected: bool,
373 window: &mut Window,
374 cx: &mut Context<Picker<Self>>,
375 ) -> Option<Self::ListItem>;
376
377 fn render_match_with_checkbox(
378 &self,
379 _ix: usize,
380 _selected: bool,
381 _checkbox: AnyElement,
382 _window: &mut Window,
383 _cx: &mut Context<Picker<Self>>,
384 ) -> Option<Self::ListItem> {
385 None
386 }
387
388 fn render_header(
389 &self,
390 _window: &mut Window,
391 _: &mut Context<Picker<Self>>,
392 ) -> Option<AnyElement> {
393 None
394 }
395
396 /// Overrides the picker's footer.
397 ///
398 /// Note there normally isn't a footer unless this is set or the picker has
399 /// a preview. If the picker has a preview add actions to it using picker_actions.
400 fn render_footer(
401 &self,
402 _window: &mut Window,
403 _: &mut Context<Picker<Self>>,
404 ) -> Option<AnyElement> {
405 None
406 }
407
408 /// Make this non-empty to have an `Actions` menu show up in the footer
409 fn actions_menu(
410 &self,
411 _window: &mut Window,
412 _cx: &mut Context<Picker<Self>>,
413 ) -> Vec<footer::PickerAction> {
414 Vec::new()
415 }
416
417 fn documentation_aside(
418 &self,
419 _window: &mut Window,
420 _cx: &mut Context<Picker<Self>>,
421 ) -> Option<DocumentationAside> {
422 None
423 }
424
425 /// Returns the index of the item whose documentation aside should be shown.
426 /// This is used to position the aside relative to that item.
427 /// Typically this is the hovered item, not necessarily the selected item.
428 fn documentation_aside_index(&self) -> Option<usize> {
429 None
430 }
431}
432
433impl<D: PickerDelegate> Focusable for Picker<D> {
434 fn focus_handle(&self, cx: &App) -> FocusHandle {
435 match &self.head {
436 Head::Editor(editor) => editor.focus_handle(cx),
437 Head::Empty(head) => head.focus_handle(cx),
438 }
439 }
440}
441
442#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
443enum ContainerKind {
444 List,
445 UniformList,
446}
447
448impl<D: PickerDelegate> Picker<D> {
449 /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height.
450 /// The picker allows the user to perform search items by text.
451 /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`.
452 pub fn uniform_list(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
453 let head = Head::editor(
454 delegate.placeholder_text(window, cx),
455 Self::on_input_editor_event,
456 window,
457 cx,
458 );
459
460 Self::new(delegate, ContainerKind::UniformList, head, None, window, cx)
461 }
462
463 /// A picker similar to [`uniform_list()`](Self::uniform_list) however this picker has a
464 /// preview window where it shows extra information.
465 pub fn uniform_list_with_preview(
466 delegate: D,
467 preview: Arc<dyn PreviewBackend>,
468 window: &mut Window,
469 cx: &mut Context<Self>,
470 ) -> Self {
471 let head = Head::editor(
472 delegate.placeholder_text(window, cx),
473 Self::on_input_editor_event,
474 window,
475 cx,
476 );
477
478 let preview = Preview::new(preview);
479 Self::new(
480 delegate,
481 ContainerKind::UniformList,
482 head,
483 Some(preview),
484 window,
485 cx,
486 )
487 }
488
489 /// A picker similar to [`list()`](Self::list) (variable-height rows) but with
490 /// a preview window. Use this instead of [`uniform_list_with_preview()`](Self::uniform_list_with_preview)
491 /// when [`PickerDelegate::render_match`] can return rows of different heights
492 /// (e.g. section headers and separators interleaved with matches).
493 pub fn list_with_preview(
494 delegate: D,
495 preview: Arc<dyn PreviewBackend>,
496 window: &mut Window,
497 cx: &mut Context<Self>,
498 ) -> Self {
499 let head = Head::editor(
500 delegate.placeholder_text(window, cx),
501 Self::on_input_editor_event,
502 window,
503 cx,
504 );
505
506 let preview = Preview::new(preview);
507 Self::new(
508 delegate,
509 ContainerKind::List,
510 head,
511 Some(preview),
512 window,
513 cx,
514 )
515 }
516
517 /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height.
518 /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`.
519 pub fn nonsearchable_uniform_list(
520 delegate: D,
521 window: &mut Window,
522 cx: &mut Context<Self>,
523 ) -> Self {
524 let head = Head::empty(Self::on_empty_head_blur, window, cx);
525
526 Self::new(delegate, ContainerKind::UniformList, head, None, window, cx)
527 }
528
529 /// A picker, which displays its matches using `gpui::list`, matches can have different heights.
530 /// The picker allows the user to perform search items by text.
531 /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that.
532 pub fn nonsearchable_list(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
533 let head = Head::empty(Self::on_empty_head_blur, window, cx);
534
535 Self::new(delegate, ContainerKind::List, head, None, window, cx)
536 }
537
538 /// A picker, which displays its matches using `gpui::list`, matches can have different heights.
539 /// The picker allows the user to perform search items by text.
540 /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that.
541 pub fn list(delegate: D, window: &mut Window, cx: &mut Context<Self>) -> Self {
542 let head = Head::editor(
543 delegate.placeholder_text(window, cx),
544 Self::on_input_editor_event,
545 window,
546 cx,
547 );
548
549 Self::new(delegate, ContainerKind::List, head, None, window, cx)
550 }
551
552 fn new(
553 delegate: D,
554 container: ContainerKind,
555 head: Head,
556 mut preview: Option<Preview>,
557 window: &mut Window,
558 cx: &mut Context<Self>,
559 ) -> Self {
560 let element_container = Self::create_element_container(container);
561 if let Some(preview) = &mut preview {
562 preview.layout = persistence::load_last_preview_layout(D::name(), cx)
563 .log_err()
564 .flatten()
565 .unwrap_or_default();
566 };
567 let has_preview = preview.is_some();
568 let persisted_shape =
569 persistence::try_load_shape(D::name(), preview.as_ref().map(|p| p.layout), cx)
570 .log_err()
571 .flatten();
572 // Every picker opens at the standard "simple" size: a fixed width and a
573 // standard max height it shrinks below when there's little content.
574 // Showing a preview expands it to the larger "telescope" size (see
575 // `default_shape_for_layout`).
576 let default_shape = shape::Centered::simple();
577 let initial_layout = preview
578 .as_ref()
579 .map(|p| p.layout)
580 .unwrap_or(preview::Layout::Hidden);
581 let mut size_bounds = shape::SizeBounds::default();
582 // For a plain picker the whole-picker minimum is just its opening width,
583 // so it can't be resized/clamped narrower than it opens. Preview pickers
584 // keep the standard per-pane minimums.
585 if !has_preview && let Some(width) = default_shape.width.as_rems() {
586 size_bounds.min_results.width = width;
587 }
588 let mut this = Self {
589 delegate,
590 head,
591 element_container,
592 pending_update_matches: None,
593 confirm_on_update: None,
594 select_instead_of_open: false,
595 preview,
596 shape_loaded_from_persistence: persisted_shape.is_some(),
597 shape: persisted_shape.unwrap_or_else(|| {
598 shape::Shape::HorizontallyCentered(default_shape_for_layout(
599 default_shape,
600 initial_layout,
601 ))
602 }),
603 default_shape,
604 show_scrollbar: false,
605 presentation: Presentation::Modal {
606 resizable: has_preview,
607 },
608 picker_bounds: Rc::new(Cell::new(None)),
609 item_bounds: Rc::new(RefCell::new(HashMap::default())),
610 size_bounds,
611 actions_menu_handle: PopoverMenuHandle::default(),
612 reopenable: true,
613 };
614 // give delegate the initial preview layout
615 this.delegate
616 .preview_layout_changed(matches!(initial_layout, preview::Layout::Right));
617 if this.reopenable {
618 let focus_handle = this.focus_handle(cx);
619 workspace::register_reopenable_picker(&focus_handle, cx);
620 }
621 this.update_matches("".to_string(), window, cx);
622 // give the delegate 4ms to render the first set of suggestions.
623 this.delegate
624 .finalize_update_matches("".to_string(), Duration::from_millis(4), window, cx);
625 this
626 }
627
628 fn create_element_container(container: ContainerKind) -> ElementContainer {
629 match container {
630 ContainerKind::UniformList => {
631 ElementContainer::UniformList(UniformListScrollHandle::new())
632 }
633 ContainerKind::List => {
634 ElementContainer::List(ListState::new(0, gpui::ListAlignment::Top, px(1000.)))
635 }
636 }
637 }
638
639 /// Overrides the width the picker opens at (and resets to). Plain pickers
640 /// default to [`DEFAULT_MODAL_WIDTH`]; only call this for pickers that need a
641 /// different width (e.g. narrow popover selectors).
642 ///
643 /// For a plain (no-preview) picker the opening width is also its minimum, so
644 /// it can't be resized or clamped narrower than it opens.
645 pub fn initial_width(mut self, width: impl Into<RelativeWidth>) -> Self {
646 let width = width.into();
647 self.default_shape.width = width;
648 if self.live_shape_is_hidden_default() {
649 self.shape.set_initial_width(width);
650 }
651 // A plain picker's whole-picker minimum tracks its opening width. Preview
652 // pickers keep the standard per-pane minimums.
653 if self.preview.is_none()
654 && let Some(rems) = width.as_rems()
655 {
656 self.size_bounds.min_results.width = rems;
657 }
658 self
659 }
660
661 /// Overrides the picker's max height. Plain pickers default to
662 /// [`DEFAULT_MODAL_MAX_HEIGHT`] and shrink below it to fit their content;
663 /// only call this for pickers that want a different cap (e.g. the outline
664 /// view, which is taller).
665 pub fn max_height(mut self, height: impl Into<RelativeHeight>) -> Self {
666 let height = height.into();
667 self.default_shape.height = height;
668 if self.live_shape_is_hidden_default() {
669 self.shape.set_initial_height(height);
670 }
671 self
672 }
673
674 /// Whether the live shape still reflects the hidden-layout default, i.e. it
675 /// was not restored from a persisted size and currently opens with no
676 /// preview. Only then may `initial_width`/`max_height` push their
677 /// hidden/no-preview defaults onto it; a picker reopened in a preview layout
678 /// keeps the larger telescope default, which those (smaller) no-preview
679 /// values must not shrink.
680 fn live_shape_is_hidden_default(&self) -> bool {
681 !self.shape_loaded_from_persistence
682 && self.preview_layout().unwrap_or(preview::Layout::Hidden) == preview::Layout::Hidden
683 }
684
685 /// Whether the picker fills its full height (preview visible) or shrinks to
686 /// fit its content, treating the height as a maximum (no preview visible).
687 fn fill_height(&self) -> bool {
688 self.preview
689 .as_ref()
690 .is_some_and(|preview| preview.layout != preview::Layout::Hidden)
691 }
692
693 pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self {
694 self.show_scrollbar = show_scrollbar;
695 self
696 }
697
698 /// Controls whether a modal hosting this picker can be revealed again with
699 /// `workspace::ReopenLastPicker` after it's dismissed. Defaults to `true`;
700 /// pass `false` to exclude this picker. As a builder, this only takes effect
701 /// while constructing the picker, before it's opened.
702 pub fn reopenable(mut self, reopenable: bool, cx: &mut App) -> Self {
703 if reopenable == self.reopenable {
704 return self;
705 }
706 self.reopenable = reopenable;
707 let focus_handle = self.focus_handle(cx);
708 if reopenable {
709 workspace::register_reopenable_picker(&focus_handle, cx);
710 } else {
711 workspace::deregister_reopenable_picker(&focus_handle, cx);
712 }
713 self
714 }
715
716 /// Presents the picker as embedded inside a larger container that provides
717 /// its own chrome and dismissal, rather than the default self-contained
718 /// modal. Use [`popover`](Self::popover) instead for menu-attached pickers.
719 pub fn embedded(mut self) -> Self {
720 self.presentation = Presentation::Embedded;
721 self
722 }
723
724 /// Presents the picker as a popover surface: it draws its own elevated
725 /// background (like a modal) but is not resizable. Use this for pickers
726 /// shown inside a menu/popover.
727 pub fn popover(mut self) -> Self {
728 self.set_popover();
729 self
730 }
731
732 pub(crate) fn set_popover(&mut self) {
733 self.presentation = Presentation::Popover;
734 }
735
736 /// Controls whether the user can drag to resize the picker (and whether its
737 /// size is persisted). Only applies to modal pickers; no-op otherwise.
738 /// Defaults to `true` for pickers with a preview and `false` otherwise.
739 pub fn resizable(mut self, resizable: bool) -> Self {
740 if let Presentation::Modal { resizable: r } = &mut self.presentation {
741 *r = resizable;
742 }
743 self
744 }
745
746 /// Whether the picker draws its own elevated background and dismisses on
747 /// blur (modals and popovers, but not embedded pickers).
748 fn draws_own_container(&self) -> bool {
749 matches!(
750 self.presentation,
751 Presentation::Modal { .. } | Presentation::Popover
752 )
753 }
754
755 /// Whether the picker can be resized (only ever true for modals).
756 fn is_resizable(&self) -> bool {
757 matches!(self.presentation, Presentation::Modal { resizable: true })
758 }
759
760 pub fn list_measure_all(mut self) -> Self {
761 match self.element_container {
762 ElementContainer::List(state) => {
763 self.element_container = ElementContainer::List(state.measure_all());
764 }
765 _ => {}
766 }
767 self
768 }
769
770 pub fn focus(&self, window: &mut Window, cx: &mut App) {
771 self.focus_handle(cx).focus(window, cx);
772 }
773
774 /// Handles the selecting an index, and passing the change to the delegate.
775 /// If `fallback_direction` is set to `None`, the index will not be selected
776 /// if the element at that index cannot be selected.
777 /// If `fallback_direction` is set to
778 /// `Some(..)`, the next selectable element will be selected in the
779 /// specified direction (Down or Up), cycling through all elements until
780 /// finding one that can be selected or returning if there are no selectable elements.
781 /// If `scroll_to_index` is true, the new selected index will be scrolled into
782 /// view.
783 ///
784 /// If some effect is bound to `selected_index_changed`, it will be executed.
785 pub fn set_selected_index(
786 &mut self,
787 mut ix: usize,
788 fallback_direction: Option<Direction>,
789 scroll_to_index: bool,
790 window: &mut Window,
791 cx: &mut Context<Self>,
792 ) {
793 let match_count = self.delegate.match_count();
794 if match_count == 0 {
795 return;
796 }
797
798 if let Some(bias) = fallback_direction {
799 let mut curr_ix = ix;
800 while !self.delegate.can_select(curr_ix, window, cx) {
801 curr_ix = match bias {
802 Direction::Down => {
803 if curr_ix == match_count - 1 {
804 0
805 } else {
806 curr_ix + 1
807 }
808 }
809 Direction::Up => {
810 if curr_ix == 0 {
811 match_count - 1
812 } else {
813 curr_ix - 1
814 }
815 }
816 };
817 // There is no item that can be selected
818 if ix == curr_ix {
819 return;
820 }
821 }
822 ix = curr_ix;
823 } else if !self.delegate.can_select(ix, window, cx) {
824 return;
825 }
826
827 let previous_index = self.delegate.selected_index();
828 self.delegate.set_selected_index(ix, window, cx);
829 let current_index = self.delegate.selected_index();
830
831 if previous_index != current_index {
832 if let Some(action) = self.delegate.selected_index_changed(ix, window, cx) {
833 action(window, cx);
834 }
835 if let Some(preview) = &mut self.preview
836 && let Some(update) = self.delegate.try_get_preview_data_for_match(cx)
837 {
838 preview.update(update, window, cx);
839 }
840 if scroll_to_index {
841 self.scroll_to_item_index(ix);
842 }
843 }
844 }
845
846 pub fn select_next(
847 &mut self,
848 _: &menu::SelectNext,
849 window: &mut Window,
850 cx: &mut Context<Self>,
851 ) {
852 let query = self.query(cx);
853 if let Some(query) = self
854 .delegate
855 .select_history(Direction::Down, &query, window, cx)
856 {
857 self.set_query(&query, window, cx);
858 return;
859 }
860 let count = self.delegate.match_count();
861 if count > 0 {
862 let index = self.delegate.selected_index();
863 let ix = if index == count - 1 { 0 } else { index + 1 };
864 self.set_selected_index(ix, Some(Direction::Down), true, window, cx);
865 cx.notify();
866 }
867 }
868
869 pub fn editor_move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
870 self.select_previous(&Default::default(), window, cx);
871 }
872
873 fn select_previous(
874 &mut self,
875 _: &menu::SelectPrevious,
876 window: &mut Window,
877 cx: &mut Context<Self>,
878 ) {
879 let query = self.query(cx);
880 if let Some(query) = self
881 .delegate
882 .select_history(Direction::Up, &query, window, cx)
883 {
884 self.set_query(&query, window, cx);
885 return;
886 }
887 let count = self.delegate.match_count();
888 if count > 0 {
889 let index = self.delegate.selected_index();
890 let ix = if index == 0 { count - 1 } else { index - 1 };
891 self.set_selected_index(ix, Some(Direction::Up), true, window, cx);
892 cx.notify();
893 }
894 }
895
896 pub fn editor_move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
897 self.select_next(&Default::default(), window, cx);
898 }
899
900 pub fn select_first(
901 &mut self,
902 _: &menu::SelectFirst,
903 window: &mut Window,
904 cx: &mut Context<Self>,
905 ) {
906 let count = self.delegate.match_count();
907 if count > 0 {
908 self.set_selected_index(0, Some(Direction::Down), true, window, cx);
909 cx.notify();
910 }
911 }
912
913 fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
914 let count = self.delegate.match_count();
915 if count > 0 {
916 self.set_selected_index(count - 1, Some(Direction::Up), true, window, cx);
917 cx.notify();
918 }
919 }
920
921 pub fn cycle_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
922 let count = self.delegate.match_count();
923 let index = self.delegate.selected_index();
924 let new_index = if index + 1 == count { 0 } else { index + 1 };
925 self.set_selected_index(new_index, Some(Direction::Down), true, window, cx);
926 cx.notify();
927 }
928
929 pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
930 if self.delegate.should_dismiss() {
931 self.select_instead_of_open = false;
932 self.delegate.clear_selection(cx);
933 self.delegate.dismissed(window, cx);
934 cx.emit(DismissEvent);
935 }
936 }
937
938 fn toggle_multi_select(
939 &mut self,
940 _: &ToggleMultiSelect,
941 _window: &mut Window,
942 cx: &mut Context<Self>,
943 ) {
944 if !self.delegate.supports_multi_select() {
945 cx.propagate();
946 return;
947 }
948 self.select_instead_of_open = !self.select_instead_of_open;
949 if !self.select_instead_of_open {
950 self.delegate.clear_selection(cx);
951 }
952 cx.notify();
953 }
954
955 fn multi_select_next(
956 &mut self,
957 _: &MultiSelectNext,
958 window: &mut Window,
959 cx: &mut Context<Self>,
960 ) {
961 // Propagate so `tab` retains its other meanings (e.g.
962 // `ConfirmCompletion`) in pickers without multi-select.
963 if !self.delegate.supports_multi_select() {
964 cx.propagate();
965 return;
966 }
967 self.select_instead_of_open = true;
968 let ix = self.delegate.selected_index();
969 self.delegate.toggle_item_selected(ix, window, cx);
970 self.select_next(&menu::SelectNext, window, cx);
971 cx.notify();
972 }
973
974 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
975 if self.pending_update_matches.is_some()
976 && !self.delegate.finalize_update_matches(
977 self.query(cx),
978 Duration::from_millis(16),
979 window,
980 cx,
981 )
982 {
983 self.confirm_on_update = Some(false)
984 } else {
985 self.pending_update_matches.take();
986 self.do_confirm(false, window, cx);
987 }
988 }
989
990 fn secondary_confirm(
991 &mut self,
992 _: &menu::SecondaryConfirm,
993 window: &mut Window,
994 cx: &mut Context<Self>,
995 ) {
996 if self.pending_update_matches.is_some()
997 && !self.delegate.finalize_update_matches(
998 self.query(cx),
999 Duration::from_millis(16),
1000 window,
1001 cx,
1002 )
1003 {
1004 self.confirm_on_update = Some(true)
1005 } else {
1006 self.do_confirm(true, window, cx);
1007 }
1008 }
1009
1010 fn confirm_input(&mut self, input: &ConfirmInput, window: &mut Window, cx: &mut Context<Self>) {
1011 self.delegate.confirm_input(input.secondary, window, cx);
1012 }
1013
1014 fn confirm_completion(
1015 &mut self,
1016 _: &ConfirmCompletion,
1017 window: &mut Window,
1018 cx: &mut Context<Self>,
1019 ) {
1020 if let Some(new_query) = self.delegate.confirm_completion(self.query(cx), window, cx) {
1021 self.set_query(&new_query, window, cx);
1022 } else {
1023 cx.propagate()
1024 }
1025 }
1026 fn select_child(&mut self, _: &menu::SelectChild, window: &mut Window, cx: &mut Context<Self>) {
1027 if let Some(new_query) = self.delegate.select_child(window, cx) {
1028 self.set_query(&new_query, window, cx);
1029 } else {
1030 cx.propagate()
1031 }
1032 }
1033
1034 fn select_parent(
1035 &mut self,
1036 _: &menu::SelectParent,
1037 window: &mut Window,
1038 cx: &mut Context<Self>,
1039 ) {
1040 if let Some(new_query) = self.delegate.select_parent(window, cx) {
1041 self.set_query(&new_query, window, cx);
1042 } else {
1043 cx.propagate()
1044 }
1045 }
1046
1047 fn set_preview_right(
1048 &mut self,
1049 _: &SetPreviewRight,
1050 window: &mut Window,
1051 cx: &mut Context<Self>,
1052 ) {
1053 self.set_preview_layout(preview::Layout::Right, window, cx);
1054 }
1055
1056 fn set_preview_below(
1057 &mut self,
1058 _: &SetPreviewBelow,
1059 window: &mut Window,
1060 cx: &mut Context<Self>,
1061 ) {
1062 self.set_preview_layout(preview::Layout::Below, window, cx);
1063 }
1064
1065 fn set_preview_hidden(
1066 &mut self,
1067 _: &SetPreviewHidden,
1068 window: &mut Window,
1069 cx: &mut Context<Self>,
1070 ) {
1071 self.set_preview_layout(preview::Layout::Hidden, window, cx);
1072 }
1073
1074 fn toggle_actions_menu(
1075 &mut self,
1076 _: &ToggleActionsMenu,
1077 window: &mut Window,
1078 cx: &mut Context<Self>,
1079 ) {
1080 self.actions_menu_handle.toggle(window, cx);
1081 }
1082
1083 fn handle_click(
1084 &mut self,
1085 ix: usize,
1086 secondary: bool,
1087 window: &mut Window,
1088 cx: &mut Context<Self>,
1089 ) {
1090 cx.stop_propagation();
1091 window.prevent_default();
1092 if !self.delegate.can_select(ix, window, cx) {
1093 return;
1094 }
1095 self.set_selected_index(ix, None, false, window, cx);
1096 if self.delegate.supports_multi_select() && (secondary || self.select_instead_of_open) {
1097 self.select_instead_of_open = true;
1098 self.delegate.toggle_item_selected(ix, window, cx);
1099 cx.notify();
1100 } else {
1101 self.do_confirm(secondary, window, cx);
1102 }
1103 }
1104
1105 fn do_confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Self>) {
1106 if self.delegate.supports_multi_select() && self.delegate.selected_item_count() > 0 {
1107 self.select_instead_of_open = false;
1108 self.delegate.confirm_multi(secondary, window, cx);
1109 } else if let Some(update_query) = self.delegate.confirm_update_query(window, cx) {
1110 self.set_query(&update_query, window, cx);
1111 self.set_selected_index(0, Some(Direction::Down), false, window, cx);
1112 } else {
1113 self.delegate.confirm(secondary, window, cx)
1114 }
1115 }
1116
1117 fn on_input_editor_event(
1118 &mut self,
1119 event: &ErasedEditorEvent,
1120 window: &mut Window,
1121 cx: &mut Context<Self>,
1122 ) {
1123 let Head::Editor(editor) = &self.head else {
1124 panic!("unexpected call");
1125 };
1126 match event {
1127 ErasedEditorEvent::BufferEdited => {
1128 let query = editor.text(cx);
1129 self.update_matches(query, window, cx);
1130 }
1131 ErasedEditorEvent::Blurred => {
1132 // Opening a footer/search-bar menu blurs the editor; don't
1133 // dismiss the picker while such a menu is open/focused.
1134 let menu_focused = self.actions_menu_handle.is_focused(window, cx)
1135 || self.actions_menu_handle.is_deployed()
1136 || self.delegate.has_another_open_menu(window, cx);
1137 if self.draws_own_container() && window.is_window_active() && !menu_focused {
1138 self.cancel(&menu::Cancel, window, cx);
1139 }
1140 }
1141 }
1142 }
1143
1144 fn on_empty_head_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1145 let Head::Empty(_) = &self.head else {
1146 panic!("unexpected call");
1147 };
1148 if window.is_window_active() {
1149 self.cancel(&menu::Cancel, window, cx);
1150 }
1151 }
1152
1153 pub fn refresh_placeholder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1154 match &self.head {
1155 Head::Editor(editor) => {
1156 let placeholder = self.delegate.placeholder_text(window, cx);
1157
1158 editor.set_placeholder_text(placeholder.as_ref(), window, cx);
1159 cx.notify();
1160 }
1161 Head::Empty(_) => {}
1162 }
1163 }
1164
1165 pub fn refresh(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1166 let query = self.query(cx);
1167 self.update_matches(query, window, cx);
1168 }
1169
1170 pub fn update_matches(&mut self, query: String, window: &mut Window, cx: &mut Context<Self>) {
1171 self.update_matches_with_options(query, ScrollBehavior::RevealSelected, window, cx);
1172 }
1173
1174 pub fn update_matches_with_options(
1175 &mut self,
1176 query: String,
1177 scroll_behavior: ScrollBehavior,
1178 window: &mut Window,
1179 cx: &mut Context<Self>,
1180 ) {
1181 let delegate_pending_update_matches = self.delegate.update_matches(query, window, cx);
1182
1183 self.matches_updated(scroll_behavior, window, cx);
1184 // This struct ensures that we can synchronously drop the task returned by the
1185 // delegate's `update_matches` method and the task that the picker is spawning.
1186 // If we simply capture the delegate's task into the picker's task, when the picker's
1187 // task gets synchronously dropped, the delegate's task would keep running until
1188 // the picker's task has a chance of being scheduled, because dropping a task happens
1189 // asynchronously.
1190 self.pending_update_matches = Some(PendingUpdateMatches {
1191 delegate_update_matches: Some(delegate_pending_update_matches),
1192 _task: cx.spawn_in(window, async move |this, cx| {
1193 let delegate_pending_update_matches = this.update(cx, |this, _| {
1194 this.pending_update_matches
1195 .as_mut()
1196 .unwrap()
1197 .delegate_update_matches
1198 .take()
1199 .unwrap()
1200 })?;
1201 delegate_pending_update_matches.await;
1202 this.update_in(cx, |this, window, cx| {
1203 this.matches_updated(scroll_behavior, window, cx);
1204 })
1205 }),
1206 });
1207 }
1208
1209 fn matches_updated(
1210 &mut self,
1211 scroll_behavior: ScrollBehavior,
1212 window: &mut Window,
1213 cx: &mut Context<Self>,
1214 ) {
1215 let match_count = self.delegate.match_count();
1216 if match_count == 0
1217 && let Some(preview) = &mut self.preview
1218 {
1219 preview.clear(cx)
1220 }
1221
1222 match &mut self.element_container {
1223 ElementContainer::List(state) => match scroll_behavior {
1224 ScrollBehavior::RevealSelected => {
1225 state.reset(match_count);
1226 let index = self.delegate.selected_index();
1227 self.scroll_to_item_index(index);
1228 }
1229 ScrollBehavior::PreserveOffset => {
1230 let offset = state.logical_scroll_top();
1231 state.reset(match_count);
1232 state.scroll_to(offset);
1233 }
1234 },
1235 ElementContainer::UniformList(_) => match scroll_behavior {
1236 ScrollBehavior::RevealSelected => {
1237 let index = self.delegate.selected_index();
1238 self.scroll_to_item_index(index);
1239 }
1240 ScrollBehavior::PreserveOffset => {}
1241 },
1242 }
1243 self.pending_update_matches = None;
1244 if let Some(update) = self.delegate.try_get_preview_data_for_match(cx)
1245 && let Some(preview) = &mut self.preview
1246 {
1247 preview.update(update, window, cx);
1248 }
1249 if let Some(secondary) = self.confirm_on_update.take() {
1250 self.do_confirm(secondary, window, cx);
1251 }
1252 cx.notify();
1253 }
1254
1255 pub fn query(&self, cx: &App) -> String {
1256 match &self.head {
1257 Head::Editor(editor) => editor.text(cx),
1258 Head::Empty(_) => "".to_string(),
1259 }
1260 }
1261
1262 pub fn set_query(&self, query: &str, window: &mut Window, cx: &mut App) {
1263 if let Head::Editor(editor) = &self.head {
1264 editor.set_text(query, window, cx);
1265 editor.move_selection_to_end(window, cx);
1266 }
1267 }
1268
1269 /// Selects the entire query, so the next keystroke replaces it (and a single
1270 /// backspace clears it). Matches the buffer search bar's seeded-query behavior.
1271 pub fn select_query(&self, window: &mut Window, cx: &mut App) {
1272 if let Head::Editor(editor) = &self.head {
1273 editor.select_all(window, cx);
1274 }
1275 }
1276
1277 fn scroll_to_item_index(&mut self, ix: usize) {
1278 match &mut self.element_container {
1279 ElementContainer::List(state) => state.scroll_to_reveal_item(ix),
1280 ElementContainer::UniformList(scroll_handle) => {
1281 scroll_handle.scroll_to_item(ix, ScrollStrategy::Nearest)
1282 }
1283 }
1284 }
1285
1286 pub fn is_scrolled_to_end(&self) -> Option<bool> {
1287 match &self.element_container {
1288 ElementContainer::List(state) => state.is_scrolled_to_end(),
1289 ElementContainer::UniformList(scroll_handle) => scroll_handle.is_scrolled_to_end(),
1290 }
1291 }
1292
1293 fn render_element(
1294 &self,
1295 window: &mut Window,
1296 cx: &mut Context<Self>,
1297 ix: usize,
1298 ) -> impl IntoElement + use<D> {
1299 let item_bounds = self.item_bounds.clone();
1300 let selectable =
1301 ix < self.delegate.match_count() && self.delegate.can_select(ix, window, cx);
1302
1303 let supports_multi_select = self.delegate.supports_multi_select();
1304 let is_multi_selected = supports_multi_select && self.delegate.is_item_selected(ix);
1305 let multi_select_active = supports_multi_select && self.select_instead_of_open;
1306
1307 let item_with_checkbox = if multi_select_active && selectable {
1308 let checkbox = self
1309 .render_multi_select_indicator(ix, is_multi_selected, cx)
1310 .into_any_element();
1311 self.delegate.render_match_with_checkbox(
1312 ix,
1313 ix == self.delegate.selected_index(),
1314 checkbox,
1315 window,
1316 cx,
1317 )
1318 } else {
1319 None
1320 };
1321
1322 let use_fallback_indicator =
1323 multi_select_active && selectable && item_with_checkbox.is_none();
1324 let focus_handle = self.focus_handle(cx);
1325
1326 div()
1327 .id(("item", ix))
1328 .when(selectable, |this| this.cursor_pointer())
1329 .when(use_fallback_indicator, |this| {
1330 this.hover(|s| s.bg(cx.theme().colors().ghost_element_hover))
1331 })
1332 .when(multi_select_active && selectable, |this| {
1333 this.tooltip(Tooltip::element(move |_window, cx| {
1334 h_flex()
1335 .gap_2()
1336 .child(
1337 h_flex()
1338 .gap_1()
1339 .child(KeyBinding::for_action_in(
1340 &MultiSelectNext,
1341 &focus_handle,
1342 cx,
1343 ))
1344 .child(Label::new("Select")),
1345 )
1346 .child(Divider::vertical())
1347 .child(
1348 h_flex()
1349 .gap_1()
1350 .child(KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx))
1351 .child(Label::new("Open")),
1352 )
1353 .into_any_element()
1354 }))
1355 })
1356 .child(
1357 canvas(
1358 move |bounds, _window, _cx| {
1359 item_bounds.borrow_mut().insert(ix, bounds);
1360 },
1361 |_bounds, _state, _window, _cx| {},
1362 )
1363 .size_full()
1364 .absolute()
1365 .top_0()
1366 .left_0(),
1367 )
1368 .when(!self.delegate.select_on_hover(), |this| {
1369 this.on_mouse_down(MouseButton::Left, |_, window, _cx| {
1370 window.prevent_default();
1371 })
1372 })
1373 .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| {
1374 this.handle_click(ix, event.modifiers().secondary(), window, cx)
1375 }))
1376 // As of this writing, GPUI intercepts `ctrl-[mouse-event]`s on macOS
1377 // and produces right mouse button events. This matches platforms norms
1378 // but means that UIs which depend on holding ctrl down (such as the tab
1379 // switcher) can't be clicked on. Hence, this handler.
1380 .on_mouse_up(
1381 MouseButton::Right,
1382 cx.listener(move |this, event: &MouseUpEvent, window, cx| {
1383 // We specifically want to use the platform key here, as
1384 // ctrl will already be held down for the tab switcher.
1385 this.handle_click(ix, event.modifiers.platform, window, cx)
1386 }),
1387 )
1388 .when(self.delegate.select_on_hover(), |this| {
1389 this.on_hover(cx.listener(move |this, hovered: &bool, window, cx| {
1390 if *hovered {
1391 this.set_selected_index(ix, None, false, window, cx);
1392 cx.notify();
1393 }
1394 }))
1395 })
1396 .map(|row| {
1397 if let Some(item) = item_with_checkbox {
1398 row.child(item)
1399 } else if supports_multi_select {
1400 row.child(
1401 h_flex()
1402 // Headers and separators cannot be part of the
1403 // selection, so they get no indicator.
1404 .when(use_fallback_indicator, |this| {
1405 this.child(self.render_multi_select_indicator(
1406 ix,
1407 is_multi_selected,
1408 cx,
1409 ))
1410 })
1411 .children(self.delegate.render_match(
1412 ix,
1413 ix == self.delegate.selected_index(),
1414 window,
1415 cx,
1416 )),
1417 )
1418 } else {
1419 row.children(self.delegate.render_match(
1420 ix,
1421 ix == self.delegate.selected_index(),
1422 window,
1423 cx,
1424 ))
1425 }
1426 })
1427 .when(
1428 self.delegate.separators_after_indices().contains(&ix),
1429 |picker| {
1430 picker
1431 .border_color(cx.theme().colors().border_variant)
1432 .border_b_1()
1433 .py(px(-1.0))
1434 },
1435 )
1436 }
1437
1438 fn render_multi_select_indicator(
1439 &self,
1440 ix: usize,
1441 is_selected: bool,
1442 cx: &mut Context<Self>,
1443 ) -> impl IntoElement {
1444 Checkbox::new(("picker-multi-select-checkbox", ix), is_selected.into())
1445 .fill()
1446 .elevation(ui::ElevationIndex::ModalSurface)
1447 .on_click(cx.listener(move |this, _: &ui::ToggleState, window, cx| {
1448 // Don't let the row's click handler also toggle the item.
1449 cx.stop_propagation();
1450 this.delegate.toggle_item_selected(ix, window, cx);
1451 cx.notify();
1452 }))
1453 }
1454
1455 fn render_element_container(&self, cx: &mut Context<Self>) -> impl IntoElement {
1456 // When the picker shrinks to fit its content, the list infers its size
1457 // from its items. When it fills its full height (preview visible), the
1458 // list fills the available space.
1459 let sizing_behavior = if self.fill_height() {
1460 ListSizingBehavior::Auto
1461 } else {
1462 ListSizingBehavior::Infer
1463 };
1464
1465 match &self.element_container {
1466 ElementContainer::UniformList(scroll_handle) => uniform_list(
1467 "candidates",
1468 self.delegate.match_count(),
1469 cx.processor(move |picker, visible_range: Range<usize>, window, cx| {
1470 visible_range
1471 .map(|ix| picker.render_element(window, cx, ix))
1472 .collect()
1473 }),
1474 )
1475 .with_sizing_behavior(sizing_behavior)
1476 .flex_grow_1()
1477 .py_1()
1478 .track_scroll(&scroll_handle)
1479 .into_any_element(),
1480 ElementContainer::List(state) => list(
1481 state.clone(),
1482 cx.processor(|this, ix, window, cx| {
1483 this.render_element(window, cx, ix).into_any_element()
1484 }),
1485 )
1486 .with_sizing_behavior(sizing_behavior)
1487 .flex_grow_1()
1488 .py(DynamicSpacing::Base04.rems(cx))
1489 .into_any_element(),
1490 }
1491 }
1492
1493 #[cfg(any(test, feature = "test-support"))]
1494 pub fn logical_scroll_top_index(&self) -> usize {
1495 match &self.element_container {
1496 ElementContainer::List(state) => state.logical_scroll_top().item_ix,
1497 ElementContainer::UniformList(scroll_handle) => {
1498 scroll_handle.logical_scroll_top_index()
1499 }
1500 }
1501 }
1502
1503 fn preview_layout(&self) -> Option<preview::Layout> {
1504 self.preview.as_ref().map(|p| p.layout)
1505 }
1506 fn is_auto_vertical(&self, window: &Window) -> bool {
1507 self.preview_layout() == Some(preview::Layout::Right)
1508 && self
1509 .size_bounds
1510 .would_clamp_width_if_horizontal(&self.shape, window)
1511 }
1512 /// To check whether we're rendering vertically instead of
1513 /// horizontally due to the auto override
1514 fn preview_layout_rendered(&self, window: &Window) -> Option<preview::Layout> {
1515 let would_clamp = matches!(
1516 self.preview,
1517 Some(Preview {
1518 layout: preview::Layout::Right,
1519 ..
1520 })
1521 ) && self.is_auto_vertical(window);
1522 if would_clamp {
1523 Some(preview::Layout::Below)
1524 } else {
1525 self.preview_layout()
1526 }
1527 }
1528
1529 #[cfg(any(test, feature = "test-support"))]
1530 pub fn results_width(&self, window: &Window) -> gpui::Pixels {
1531 let layout = self
1532 .preview_layout_rendered(window)
1533 .unwrap_or(preview::Layout::Hidden);
1534 let pos = self
1535 .shape
1536 .results_position_and_size(layout, &self.size_bounds, window);
1537 pos.right - pos.left
1538 }
1539
1540 fn toggle_preview(&mut self, _: &TogglePreview, window: &mut Window, cx: &mut Context<Self>) {
1541 self.toggle_preview_visible(window, cx);
1542 }
1543
1544 fn toggle_preview_visible(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1545 let next = match self.preview_layout() {
1546 Some(preview::Layout::Hidden) | None => preview::Layout::Right,
1547 Some(_) => preview::Layout::Hidden,
1548 };
1549 self.set_preview_layout(next, window, cx);
1550 }
1551
1552 fn set_preview_layout(
1553 &mut self,
1554 layout: preview::Layout,
1555 _window: &mut Window,
1556 cx: &mut Context<Self>,
1557 ) {
1558 persistence::store_last_layout(D::name(), self.preview.as_ref().map(|_| layout), cx);
1559
1560 let Some(preview) = &mut self.preview else {
1561 return;
1562 };
1563 preview.layout = layout;
1564 // Restore the size the user last left this layout at, or fall back to the
1565 // layout's default (simple when hidden, larger when a preview is shown).
1566 self.shape = persistence::try_load_shape(D::name(), layout, cx)
1567 .log_err()
1568 .flatten()
1569 .unwrap_or_else(|| {
1570 shape::Shape::HorizontallyCentered(default_shape_for_layout(
1571 self.default_shape,
1572 layout,
1573 ))
1574 });
1575 self.delegate
1576 .preview_layout_changed(matches!(layout, preview::Layout::Right));
1577 cx.notify();
1578 }
1579}
1580
1581#[cfg(test)]
1582mod tests {
1583 use super::*;
1584 use gpui::TestAppContext;
1585 use std::cell::Cell;
1586
1587 struct TestDelegate {
1588 items: Vec<bool>,
1589 selected_index: usize,
1590 confirmed_index: Rc<Cell<Option<usize>>>,
1591 supports_multi_select: bool,
1592 selected_items: Vec<usize>,
1593 multi_confirmed: Rc<Cell<Option<Vec<usize>>>>,
1594 }
1595
1596 impl TestDelegate {
1597 fn new(items: Vec<bool>) -> Self {
1598 Self {
1599 items,
1600 selected_index: 0,
1601 confirmed_index: Rc::new(Cell::new(None)),
1602 supports_multi_select: false,
1603 selected_items: Vec::new(),
1604 multi_confirmed: Rc::new(Cell::new(None)),
1605 }
1606 }
1607
1608 fn with_multi_select(mut self) -> Self {
1609 self.supports_multi_select = true;
1610 self
1611 }
1612 }
1613
1614 impl PickerDelegate for TestDelegate {
1615 type ListItem = ui::ListItem;
1616
1617 fn name() -> &'static str {
1618 "test"
1619 }
1620
1621 fn match_count(&self) -> usize {
1622 self.items.len()
1623 }
1624
1625 fn selected_index(&self) -> usize {
1626 self.selected_index
1627 }
1628
1629 fn set_selected_index(
1630 &mut self,
1631 ix: usize,
1632 _window: &mut Window,
1633 _cx: &mut Context<Picker<Self>>,
1634 ) {
1635 self.selected_index = ix;
1636 }
1637
1638 fn can_select(
1639 &self,
1640 ix: usize,
1641 _window: &mut Window,
1642 _cx: &mut Context<Picker<Self>>,
1643 ) -> bool {
1644 self.items.get(ix).copied().unwrap_or(false)
1645 }
1646
1647 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
1648 "Test".into()
1649 }
1650
1651 fn update_matches(
1652 &mut self,
1653 _query: String,
1654 _window: &mut Window,
1655 _cx: &mut Context<Picker<Self>>,
1656 ) -> Task<()> {
1657 Task::ready(())
1658 }
1659
1660 fn confirm(
1661 &mut self,
1662 _secondary: bool,
1663 _window: &mut Window,
1664 _cx: &mut Context<Picker<Self>>,
1665 ) {
1666 self.confirmed_index.set(Some(self.selected_index));
1667 }
1668
1669 fn supports_multi_select(&self) -> bool {
1670 self.supports_multi_select
1671 }
1672
1673 fn is_item_selected(&self, ix: usize) -> bool {
1674 self.selected_items.contains(&ix)
1675 }
1676
1677 fn toggle_item_selected(
1678 &mut self,
1679 ix: usize,
1680 _window: &mut Window,
1681 _cx: &mut Context<Picker<Self>>,
1682 ) {
1683 if let Some(position) = self.selected_items.iter().position(|&item| item == ix) {
1684 self.selected_items.remove(position);
1685 } else {
1686 self.selected_items.push(ix);
1687 }
1688 }
1689
1690 fn selected_item_count(&self) -> usize {
1691 self.selected_items.len()
1692 }
1693
1694 fn clear_selection(&mut self, _cx: &mut Context<Picker<Self>>) {
1695 self.selected_items.clear();
1696 }
1697
1698 fn confirm_multi(
1699 &mut self,
1700 _secondary: bool,
1701 _window: &mut Window,
1702 _cx: &mut Context<Picker<Self>>,
1703 ) {
1704 self.multi_confirmed
1705 .set(Some(std::mem::take(&mut self.selected_items)));
1706 }
1707
1708 fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
1709
1710 fn render_match(
1711 &self,
1712 ix: usize,
1713 selected: bool,
1714 _window: &mut Window,
1715 _cx: &mut Context<Picker<Self>>,
1716 ) -> Option<Self::ListItem> {
1717 Some(
1718 ui::ListItem::new(ix)
1719 .inset(true)
1720 .toggle_state(selected)
1721 .child(ui::Label::new(format!("Item {ix}"))),
1722 )
1723 }
1724 }
1725
1726 fn init_test(cx: &mut TestAppContext) {
1727 cx.update(|cx| {
1728 let store = settings::SettingsStore::test(cx);
1729 cx.set_global(store);
1730 theme_settings::init(theme::LoadThemes::JustBase, cx);
1731 editor::init(cx);
1732 });
1733 }
1734
1735 #[gpui::test]
1736 async fn test_clicking_non_selectable_item_does_not_confirm(cx: &mut TestAppContext) {
1737 init_test(cx);
1738
1739 let confirmed_index = Rc::new(Cell::new(None));
1740 let (picker, cx) = cx.add_window_view(|window, cx| {
1741 let mut delegate = TestDelegate::new(vec![true, false, true]);
1742 delegate.confirmed_index = confirmed_index.clone();
1743 Picker::uniform_list(delegate, window, cx)
1744 });
1745
1746 picker.update(cx, |picker, _cx| {
1747 assert_eq!(picker.delegate.selected_index(), 0);
1748 });
1749
1750 picker.update_in(cx, |picker, window, cx| {
1751 picker.handle_click(1, false, window, cx);
1752 });
1753 assert!(
1754 confirmed_index.get().is_none(),
1755 "clicking a non-selectable item should not confirm"
1756 );
1757
1758 picker.update_in(cx, |picker, window, cx| {
1759 picker.handle_click(0, false, window, cx);
1760 });
1761 assert_eq!(
1762 confirmed_index.get(),
1763 Some(0),
1764 "clicking a selectable item should confirm"
1765 );
1766 }
1767
1768 #[gpui::test]
1769 async fn test_keyboard_navigation_skips_non_selectable_items(cx: &mut TestAppContext) {
1770 init_test(cx);
1771
1772 let (picker, cx) = cx.add_window_view(|window, cx| {
1773 Picker::uniform_list(TestDelegate::new(vec![true, false, true]), window, cx)
1774 });
1775
1776 picker.update(cx, |picker, _cx| {
1777 assert_eq!(picker.delegate.selected_index(), 0);
1778 });
1779
1780 picker.update_in(cx, |picker, window, cx| {
1781 picker.select_next(&menu::SelectNext, window, cx);
1782 });
1783 picker.update(cx, |picker, _cx| {
1784 assert_eq!(
1785 picker.delegate.selected_index(),
1786 2,
1787 "select_next should skip non-selectable item at index 1"
1788 );
1789 });
1790
1791 picker.update_in(cx, |picker, window, cx| {
1792 picker.select_previous(&menu::SelectPrevious, window, cx);
1793 });
1794 picker.update(cx, |picker, _cx| {
1795 assert_eq!(
1796 picker.delegate.selected_index(),
1797 0,
1798 "select_previous should skip non-selectable item at index 1"
1799 );
1800 });
1801 }
1802
1803 #[gpui::test]
1804 async fn test_multi_select_mode_routes_clicks(cx: &mut TestAppContext) {
1805 init_test(cx);
1806
1807 let confirmed_index = Rc::new(Cell::new(None));
1808 let multi_confirmed = Rc::new(Cell::new(None));
1809 let (picker, cx) = cx.add_window_view(|window, cx| {
1810 let mut delegate = TestDelegate::new(vec![true, true, true]).with_multi_select();
1811 delegate.confirmed_index = confirmed_index.clone();
1812 delegate.multi_confirmed = multi_confirmed.clone();
1813 Picker::uniform_list(delegate, window, cx)
1814 });
1815
1816 // A plain click confirms just like in any picker.
1817 picker.update_in(cx, |picker, window, cx| {
1818 picker.handle_click(1, false, window, cx);
1819 });
1820 assert_eq!(confirmed_index.take(), Some(1));
1821 picker.update(cx, |picker, _cx| {
1822 assert_eq!(picker.delegate.selected_item_count(), 0);
1823 });
1824
1825 // A secondary (cmd) click starts multi-select mode and toggles the
1826 // clicked item into the selection instead of confirming.
1827 picker.update_in(cx, |picker, window, cx| {
1828 picker.handle_click(2, true, window, cx);
1829 });
1830 assert_eq!(confirmed_index.take(), None, "cmd+click must not confirm");
1831 picker.update(cx, |picker, _cx| {
1832 assert!(
1833 picker.select_instead_of_open,
1834 "cmd+click should start multi-select mode"
1835 );
1836 assert!(picker.delegate.is_item_selected(2));
1837 });
1838
1839 // While the mode is on, plain clicks toggle items too.
1840 picker.update_in(cx, |picker, window, cx| {
1841 picker.handle_click(0, false, window, cx);
1842 });
1843 assert_eq!(
1844 confirmed_index.take(),
1845 None,
1846 "in-mode clicks must not confirm"
1847 );
1848 picker.update(cx, |picker, _cx| {
1849 assert!(picker.delegate.is_item_selected(0));
1850 assert!(picker.delegate.is_item_selected(2));
1851 assert!(!picker.delegate.is_item_selected(1));
1852 });
1853
1854 // Confirming opens the whole selection and exits the mode.
1855 picker.update_in(cx, |picker, window, cx| {
1856 picker.do_confirm(false, window, cx);
1857 });
1858 assert_eq!(multi_confirmed.take(), Some(vec![2, 0]));
1859 picker.update_in(cx, |picker, window, cx| {
1860 picker.handle_click(1, false, window, cx);
1861 });
1862 assert_eq!(
1863 confirmed_index.take(),
1864 Some(1),
1865 "the mode should be off again after confirming"
1866 );
1867 }
1868
1869 #[gpui::test]
1870 async fn test_multi_select_next_starts_multi_select_mode(cx: &mut TestAppContext) {
1871 init_test(cx);
1872
1873 let (picker, cx) = cx.add_window_view(|window, cx| {
1874 Picker::uniform_list(
1875 TestDelegate::new(vec![true, true, true]).with_multi_select(),
1876 window,
1877 cx,
1878 )
1879 });
1880
1881 picker.update_in(cx, |picker, window, cx| {
1882 picker.multi_select_next(&MultiSelectNext, window, cx);
1883 });
1884 picker.update(cx, |picker, _cx| {
1885 assert!(
1886 picker.select_instead_of_open,
1887 "selecting an item should start multi-select mode"
1888 );
1889 assert!(picker.delegate.is_item_selected(0));
1890 assert_eq!(
1891 picker.delegate.selected_index(),
1892 1,
1893 "selecting should advance the cursor to the next item"
1894 );
1895 });
1896
1897 // In pickers without multi-select the action does nothing.
1898 let (plain_picker, cx) = cx.add_window_view(|window, cx| {
1899 Picker::uniform_list(TestDelegate::new(vec![true, true]), window, cx)
1900 });
1901 plain_picker.update_in(cx, |picker, window, cx| {
1902 picker.multi_select_next(&MultiSelectNext, window, cx);
1903 });
1904 plain_picker.update(cx, |picker, _cx| {
1905 assert!(!picker.select_instead_of_open);
1906 assert_eq!(picker.delegate.selected_item_count(), 0);
1907 });
1908 }
1909
1910 #[gpui::test]
1911 async fn test_exiting_multi_select_mode_clears_selection(cx: &mut TestAppContext) {
1912 init_test(cx);
1913
1914 let (picker, cx) = cx.add_window_view(|window, cx| {
1915 Picker::uniform_list(
1916 TestDelegate::new(vec![true, true]).with_multi_select(),
1917 window,
1918 cx,
1919 )
1920 });
1921
1922 picker.update_in(cx, |picker, window, cx| {
1923 picker.toggle_multi_select(&ToggleMultiSelect, window, cx);
1924 picker.handle_click(0, false, window, cx);
1925 picker.toggle_multi_select(&ToggleMultiSelect, window, cx);
1926 });
1927 picker.update(cx, |picker, _cx| {
1928 assert_eq!(
1929 picker.delegate.selected_item_count(),
1930 0,
1931 "leaving the mode should clear the selection"
1932 );
1933 });
1934 }
1935}
1936
1937impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
1938impl<D: PickerDelegate> ModalView for Picker<D> {}
1939impl<D: PickerDelegate> ui::FluentBuilder for Picker<D> {}
1940