1use gpui::{
22 App, Context, Entity, EventEmitter, FocusHandle, Focusable, KeyBinding, SharedString, Window,
23 actions, div, prelude::*, px,
24};
25
26use motion::{Fade, Painter};
27use theme::Theme;
28
29use crate::{
30 input::{self, TextField},
31 material::Glass as _,
32 popover,
33};
34
35actions!(
36 bezel_command_palette,
37 [SelectNext, SelectPrevious, Confirm, Dismiss]
38);
39
40pub const KEY_CONTEXT: &str = "CommandPalette";
43
44pub fn init(cx: &mut App) {
47 let ctx = Some(KEY_CONTEXT);
48 cx.bind_keys([
49 KeyBinding::new("down", SelectNext, ctx),
50 KeyBinding::new("up", SelectPrevious, ctx),
51 KeyBinding::new("enter", Confirm, ctx),
52 KeyBinding::new("escape", Dismiss, ctx),
53 KeyBinding::new("ctrl-n", SelectNext, ctx),
55 KeyBinding::new("ctrl-p", SelectPrevious, ctx),
56 ]);
57}
58
59#[derive(Clone, Debug, PartialEq, Eq)]
63pub enum PaletteEvent {
64 Selected(usize),
65 Dismissed,
66}
67
68pub struct CommandPalette {
69 query: Entity<TextField>,
70 filter: popover::Filter,
71 focus_handle: FocusHandle,
72}
73
74impl EventEmitter<PaletteEvent> for CommandPalette {}
75
76impl CommandPalette {
77 pub fn new(items: Vec<SharedString>, cx: &mut Context<Self>) -> Self {
78 let query = cx.new(|cx| TextField::new(cx).with_placeholder("Type a command…"));
79 cx.observe(&query, |palette, _, cx| {
81 palette.refilter(cx);
82 })
83 .detach();
84 Self {
85 query,
86 filter: popover::Filter::new(items),
87 focus_handle: cx.focus_handle(),
88 }
89 }
90
91 pub fn focus(&self, window: &mut Window, cx: &mut App) {
94 window.focus(&self.query.focus_handle(cx), cx);
95 }
96
97 pub fn query_text(&self, cx: &App) -> SharedString {
98 self.query.read(cx).content().clone()
99 }
100
101 pub fn active_item(&self) -> Option<usize> {
103 self.filter.active_item()
104 }
105
106 fn refilter(&mut self, cx: &mut Context<Self>) {
107 let query = self.query.read(cx).content().clone();
108 self.filter.refilter(&query);
109 cx.notify();
110 }
111
112 fn select_next(&mut self, _: &SelectNext, _: &mut Window, cx: &mut Context<Self>) {
113 self.filter.step(1);
114 cx.notify();
115 }
116
117 fn select_previous(&mut self, _: &SelectPrevious, _: &mut Window, cx: &mut Context<Self>) {
118 self.filter.step(-1);
119 cx.notify();
120 }
121
122 fn confirm(&mut self, _: &Confirm, _: &mut Window, cx: &mut Context<Self>) {
123 if let Some(item) = self.active_item() {
124 cx.emit(PaletteEvent::Selected(item));
125 }
126 }
127
128 fn dismiss(&mut self, _: &Dismiss, _: &mut Window, cx: &mut Context<Self>) {
129 cx.emit(PaletteEvent::Dismissed);
130 }
131}
132
133impl Focusable for CommandPalette {
134 fn focus_handle(&self, _: &App) -> FocusHandle {
136 self.focus_handle.clone()
137 }
138}
139
140impl Render for CommandPalette {
141 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
142 let theme = Theme::of(cx).clone();
143 let view = Painter::of(cx);
144 let rows: Vec<gpui::AnyElement> = self
145 .filter
146 .filtered()
147 .iter()
148 .enumerate()
149 .map(|(position, &item)| {
150 popover::menu_row(
151 &theme,
152 Some(position) == self.filter.active(),
153 Fade::new(view, format!("palette-row-{item}")),
154 )
155 .id(SharedString::from(format!("palette-{item}")))
156 .on_click(cx.listener(move |_, _, _, cx| {
157 cx.emit(PaletteEvent::Selected(item));
158 }))
159 .child(self.filter.items()[item].clone())
160 .into_any_element()
161 })
162 .collect();
163
164 let card = popover::popover_card(&theme)
165 .w(px(420.0))
166 .child(popover::search_input_frame(
167 &theme,
168 self.query.clone().into_any_element(),
169 ))
170 .child(if rows.is_empty() {
171 div()
172 .px(px(10.0))
173 .py(px(8.0))
174 .text_size(px(13.0))
175 .text_color(theme.text_muted)
176 .child("No matches")
177 .into_any_element()
178 } else {
179 div().flex().flex_col().children(rows).into_any_element()
180 });
181
182 div()
186 .key_context(KEY_CONTEXT)
187 .track_focus(&self.focus_handle)
188 .on_action(cx.listener(Self::select_next))
189 .on_action(cx.listener(Self::select_previous))
190 .on_action(cx.listener(Self::confirm))
191 .on_action(cx.listener(Self::dismiss))
192 .child(card.material(crate::material::MENU_BLUR))
193 }
194}
195
196pub use input::KEY_CONTEXT as FIELD_KEY_CONTEXT;