1use gpui::{
22 App, Context, EventEmitter, FocusHandle, Focusable, KeyBinding, SharedString, Window, actions,
23 div, prelude::*, px,
24};
25
26use theme::Theme;
27
28use crate::{input, popover, search::SearchList, surface::Surfaced as _};
29
30actions!(
31 bezel_command_palette,
32 [SelectNext, SelectPrevious, Confirm, Dismiss]
33);
34
35pub const KEY_CONTEXT: &str = "CommandPalette";
38
39pub fn init(cx: &mut App) {
42 cx.bind_keys(bindings());
43}
44
45pub fn bindings() -> Vec<KeyBinding> {
49 let mut bindings = Vec::new();
50 let ctx = Some(KEY_CONTEXT);
51 bindings.extend([
52 KeyBinding::new("down", SelectNext, ctx),
53 KeyBinding::new("up", SelectPrevious, ctx),
54 KeyBinding::new("enter", Confirm, ctx),
55 KeyBinding::new("escape", Dismiss, ctx),
56 KeyBinding::new("ctrl-n", SelectNext, ctx),
58 KeyBinding::new("ctrl-p", SelectPrevious, ctx),
59 ]);
60
61 bindings
62}
63
64#[derive(Clone, Debug, PartialEq, Eq)]
68pub enum PaletteEvent {
69 Selected(usize),
70 Dismissed,
71}
72
73pub struct CommandPalette {
77 search: SearchList,
78 focus_handle: FocusHandle,
79}
80
81impl EventEmitter<PaletteEvent> for CommandPalette {}
82
83impl CommandPalette {
84 pub fn new(items: Vec<SharedString>, cx: &mut Context<Self>) -> Self {
85 Self {
86 search: SearchList::new(
87 items,
88 "Type a command…",
89 |view: &mut Self| &mut view.search,
90 cx,
91 ),
92 focus_handle: cx.focus_handle(),
93 }
94 }
95
96 pub fn focus(&self, window: &mut Window, cx: &mut App) {
99 window.focus(&self.search.query.focus_handle(cx), cx);
100 }
101
102 pub fn query_text(&self, cx: &App) -> SharedString {
103 self.search.query.read(cx).content().clone()
104 }
105
106 pub fn active_item(&self) -> Option<usize> {
108 self.search.filter.active_item()
109 }
110
111 fn choose(&mut self, item: usize, _: &mut Window, cx: &mut Context<Self>) {
112 cx.emit(PaletteEvent::Selected(item));
113 }
114
115 fn select_next(&mut self, _: &SelectNext, _: &mut Window, cx: &mut Context<Self>) {
116 self.search.filter.step(1);
117 cx.notify();
118 }
119
120 fn select_previous(&mut self, _: &SelectPrevious, _: &mut Window, cx: &mut Context<Self>) {
121 self.search.filter.step(-1);
122 cx.notify();
123 }
124
125 fn confirm(&mut self, _: &Confirm, _: &mut Window, cx: &mut Context<Self>) {
126 if let Some(item) = self.active_item() {
127 cx.emit(PaletteEvent::Selected(item));
128 }
129 }
130
131 fn dismiss(&mut self, _: &Dismiss, _: &mut Window, cx: &mut Context<Self>) {
132 cx.emit(PaletteEvent::Dismissed);
133 }
134}
135
136impl Focusable for CommandPalette {
137 fn focus_handle(&self, _: &App) -> FocusHandle {
139 self.focus_handle.clone()
140 }
141}
142
143impl Render for CommandPalette {
144 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
145 let theme = Theme::of(cx).clone();
146 let card = popover::popover_card(&theme)
147 .w(px(420.0))
148 .child(self.search.body(
149 &theme,
150 None,
151 None,
152 |view| &mut view.search,
153 Self::choose,
154 cx,
155 ));
156
157 div()
161 .key_context(KEY_CONTEXT)
162 .track_focus(&self.focus_handle)
163 .on_action(cx.listener(Self::select_next))
164 .on_action(cx.listener(Self::select_previous))
165 .on_action(cx.listener(Self::confirm))
166 .on_action(cx.listener(Self::dismiss))
167 .child(card.surface(&theme, theme.popover_surface))
168 }
169}
170
171pub use input::KEY_CONTEXT as FIELD_KEY_CONTEXT;