use gpui::{
App, Context, Entity, EventEmitter, FocusHandle, Focusable, KeyBinding, SharedString, Window,
actions, div, prelude::*, px,
};
use theme::Theme;
use crate::{
input::{self, TextField},
popover,
};
actions!(
bezel_command_palette,
[SelectNext, SelectPrevious, Confirm, Dismiss]
);
pub const KEY_CONTEXT: &str = "CommandPalette";
pub fn init(cx: &mut App) {
let ctx = Some(KEY_CONTEXT);
cx.bind_keys([
KeyBinding::new("down", SelectNext, ctx),
KeyBinding::new("up", SelectPrevious, ctx),
KeyBinding::new("enter", Confirm, ctx),
KeyBinding::new("escape", Dismiss, ctx),
KeyBinding::new("ctrl-n", SelectNext, ctx),
KeyBinding::new("ctrl-p", SelectPrevious, ctx),
]);
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PaletteEvent {
Selected(usize),
Dismissed,
}
pub struct CommandPalette {
query: Entity<TextField>,
filter: popover::Filter,
focus_handle: FocusHandle,
}
impl EventEmitter<PaletteEvent> for CommandPalette {}
impl CommandPalette {
pub fn new(items: Vec<SharedString>, cx: &mut Context<Self>) -> Self {
let query = cx.new(|cx| TextField::new(cx).with_placeholder("Type a command…"));
cx.observe(&query, |palette, _, cx| {
palette.refilter(cx);
})
.detach();
Self {
query,
filter: popover::Filter::new(items),
focus_handle: cx.focus_handle(),
}
}
pub fn focus(&self, window: &mut Window, cx: &mut App) {
window.focus(&self.query.focus_handle(cx), cx);
}
pub fn query_text(&self, cx: &App) -> SharedString {
self.query.read(cx).content().clone()
}
pub fn active_item(&self) -> Option<usize> {
self.filter.active_item()
}
fn refilter(&mut self, cx: &mut Context<Self>) {
let query = self.query.read(cx).content().clone();
self.filter.refilter(&query);
cx.notify();
}
fn select_next(&mut self, _: &SelectNext, _: &mut Window, cx: &mut Context<Self>) {
self.filter.step(1);
cx.notify();
}
fn select_previous(&mut self, _: &SelectPrevious, _: &mut Window, cx: &mut Context<Self>) {
self.filter.step(-1);
cx.notify();
}
fn confirm(&mut self, _: &Confirm, _: &mut Window, cx: &mut Context<Self>) {
if let Some(item) = self.active_item() {
cx.emit(PaletteEvent::Selected(item));
}
}
fn dismiss(&mut self, _: &Dismiss, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(PaletteEvent::Dismissed);
}
}
impl Focusable for CommandPalette {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for CommandPalette {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = Theme::of(cx).clone();
let rows: Vec<gpui::AnyElement> = self
.filter
.filtered()
.iter()
.enumerate()
.map(|(position, &item)| {
popover::menu_row(
&theme,
Some(position) == self.filter.active(),
SharedString::from(format!("palette-row-{item}")),
)
.id(SharedString::from(format!("palette-{item}")))
.on_click(cx.listener(move |_, _, _, cx| {
cx.emit(PaletteEvent::Selected(item));
}))
.child(self.filter.items()[item].clone())
.into_any_element()
})
.collect();
let card = popover::popover_card(&theme)
.w(px(420.0))
.child(popover::search_input_frame(
&theme,
self.query.clone().into_any_element(),
))
.child(if rows.is_empty() {
div()
.px(px(10.0))
.py(px(8.0))
.text_size(px(13.0))
.text_color(theme.text_muted)
.child("No matches")
.into_any_element()
} else {
div().flex().flex_col().children(rows).into_any_element()
});
div()
.key_context(KEY_CONTEXT)
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::dismiss))
.child(crate::material::material(
Theme::SURFACE_RADIUS,
crate::material::MENU_BLUR,
card,
))
}
}
pub use input::KEY_CONTEXT as FIELD_KEY_CONTEXT;