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