feat add command palette component

This commit is contained in:
2026-11-04 09:37:47 +07:00
parent fa73124ee4
commit 4bfc816cbe
7 changed files with 1040 additions and 11 deletions
+160 -3
View File
@@ -2,9 +2,9 @@ use std::time::Duration;
use gpui::prelude::FluentBuilder as _;
use gpui::{
App, AppContext as _, Bounds, Context, Entity, FontWeight, IntoElement, ParentElement as _,
Pixels, Render, SharedString, Styled as _, WeakEntity, Window, WindowBounds, WindowOptions,
div, px, size,
App, AppContext as _, Bounds, Context, Entity, FontWeight, InteractiveElement as _,
IntoElement, KeyBinding, ParentElement as _, Pixels, Render, SharedString, Styled as _,
WeakEntity, Window, WindowBounds, WindowOptions, actions, div, px, size,
};
use gpui_native_core::alert::{Alert, AlertSeverity, Alerts, AlertsView};
use gpui_native_core::avatar::Avatar;
@@ -12,6 +12,7 @@ use gpui_native_core::badge::{Badge, BadgeVariant};
use gpui_native_core::button::{Button, ButtonVariant};
use gpui_native_core::checkbox::Checkbox;
use gpui_native_core::combobox::{Combobox, ComboboxOption, ComboboxState};
use gpui_native_core::command_palette::{Command, CommandPalette, CommandPaletteState};
use gpui_native_core::common::{Sizable as _, Size, h_flex, v_flex};
use gpui_native_core::context_menu::ContextMenu;
use gpui_native_core::dialog::Dialog;
@@ -85,6 +86,7 @@ const CATALOG: &[(&str, &[(&str, &str)])] = &[
("alert", "Alert"),
("toast", "Toast"),
("dialog", "Dialog"),
("command-palette", "Command palette"),
("menu", "Menu"),
("context-menu", "Context menu"),
("popover", "Popover"),
@@ -174,11 +176,26 @@ impl PresetChoice {
/// The context menu demo's actions.
const CONTEXT_ACTIONS: [&str; 3] = ["Rename", "Duplicate", "Archive"];
actions!(gallery, [OpenCommandPalette]);
/// The command palette demo's actions.
fn gallery_commands() -> Vec<Command> {
vec![
Command::new("open-settings", "Open Settings").hint("Cmd+,"),
Command::new("toggle-theme", "Toggle Theme").hint("Shift+Cmd+T"),
Command::new("save-all", "Save All").hint("Cmd+Alt+S"),
Command::new("close-tab", "Close Tab").hint("Cmd+W"),
Command::new("reload-window", "Reload Window"),
]
}
struct Gallery {
layout_state: Entity<LayoutState>,
dock_state: Entity<DockState>,
alerts: Entity<Alerts>,
toasts: Entity<Toasts>,
palette: Option<Entity<CommandPaletteState>>,
palette_runs: Vec<SharedString>,
dialog_open: bool,
alert_open: bool,
}
@@ -352,6 +369,7 @@ impl Gallery {
layout_state: Entity<LayoutState>,
alerts: Entity<Alerts>,
toasts: Entity<Toasts>,
palette: Entity<CommandPaletteState>,
gallery: WeakEntity<Self>,
cx: &mut Context<DockState>,
) {
@@ -539,6 +557,7 @@ impl Gallery {
);
dock.register_panel(
PanelDescriptor::new("dialog", "Dialog", {
let gallery = gallery.clone();
move |_, cx| {
cx.new(|_| DialogDemo {
gallery: gallery.clone(),
@@ -548,6 +567,16 @@ impl Gallery {
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("command-palette", "Command palette", {
let palette = palette.clone();
move |_, cx| {
cx.new(|cx| CommandPaletteDemo::new(gallery.clone(), palette.clone(), cx))
.into()
}
}),
cx,
);
dock.register_panel(
PanelDescriptor::new("menu", "Menu", |_, cx| {
cx.new(|_| MenuDemo { open: false }).into()
@@ -608,6 +637,7 @@ impl Render for Gallery {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme();
let gallery = cx.entity().downgrade();
let palette_for_action = self.palette.clone();
// Dialogs are window-modal, so they mount at the window root; the
// Dialog demo panel only flips these flags.
@@ -617,11 +647,17 @@ impl Render for Gallery {
div()
.size_full()
.font_family(".SystemUIFont")
.on_action(move |_: &OpenCommandPalette, window, cx| {
if let Some(palette) = &palette_for_action {
palette.update(cx, |palette, cx| palette.toggle(window, cx));
}
})
.bg(theme.colors.background)
.text_color(theme.colors.foreground)
.child(self.render_shell(cx))
.child(dialog)
.child(alert_dialog)
.when_some(self.palette.clone(), |root, palette| root.child(palette))
.child(AlertsView::new("gallery-alerts", self.alerts.clone()))
.child(ToastsView::new("gallery-toasts", self.toasts.clone()))
}
@@ -2194,6 +2230,92 @@ impl Render for DialogDemo {
}
}
struct CommandPaletteDemo {
gallery: WeakEntity<Gallery>,
palette: Entity<CommandPaletteState>,
_subscription: gpui::Subscription,
}
impl CommandPaletteDemo {
fn new(
gallery: WeakEntity<Gallery>,
palette: Entity<CommandPaletteState>,
cx: &mut Context<Self>,
) -> Self {
// The panel is a cached view in the dock: the readouts re-render only
// when the palette state they read is notified.
let subscription = cx.observe(&palette, |_, _, cx| cx.notify());
Self {
gallery,
palette,
_subscription: subscription,
}
}
}
impl Render for CommandPaletteDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
let open = self.palette.read(cx).is_open();
let runs = self
.gallery
.upgrade()
.map(|gallery| gallery.read(cx).palette_runs.clone())
.unwrap_or_default();
let runs = if runs.is_empty() {
"none yet".to_string()
} else {
runs.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(", ")
};
let palette_toggle = self.palette.clone();
ScrollArea::new("command-palette-demo")
.size_full()
.p_4()
.child(demo_section(
"window-modal, mounted at the window root",
colors,
v_flex()
.gap_2()
.child(
Button::new("command-palette-demo-open")
.label("Open command palette")
.variant(ButtonVariant::Secondary)
.on_click(move |_, window, cx| {
palette_toggle.update(cx, |palette, cx| palette.toggle(window, cx));
}),
)
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child("Cmd+K (Ctrl+K elsewhere) opens it anywhere in the window."),
),
))
.child(demo_section(
"runs",
colors,
v_flex()
.gap_1()
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(if open { "open" } else { "closed" }),
)
.child(
div()
.text_xs()
.text_color(colors.muted_foreground)
.child(format!("executed: {runs}")),
),
))
}
}
struct MenuDemo {
open: bool,
}
@@ -2791,6 +2913,10 @@ fn main() {
.with_http_client(std::sync::Arc::new(reqwest_client::ReqwestClient::new()))
.run(|cx: &mut App| {
gpui_native_core::init(cx);
cx.bind_keys([
KeyBinding::new("cmd-k", OpenCommandPalette, None),
KeyBinding::new("ctrl-k", OpenCommandPalette, None),
]);
let layout_state = LayoutState::new(cx);
let dock_state = DockState::new(cx);
// A panel that declares a right sidebar view asks for it to open on
@@ -2842,16 +2968,47 @@ fn main() {
dock_state: dock_state.clone(),
alerts: alerts.clone(),
toasts: toasts.clone(),
palette: None,
palette_runs: Vec::new(),
dialog_open: false,
alert_open: false,
}
});
let palette = cx.new(|cx| {
let matcher = |query: &str, commands: &[Command]| {
let needle = query.to_lowercase();
commands
.iter()
.filter(|command| command.label.to_lowercase().contains(&needle))
.cloned()
.collect()
};
let gallery_for_execute = gallery.downgrade();
CommandPaletteState::new(
CommandPalette::new("gallery-palette", gallery_commands(), matcher)
.on_execute(move |command: &Command, _, cx| {
gallery_for_execute
.update(cx, |gallery, cx| {
gallery.palette_runs.push(command.id.clone());
cx.notify();
})
.ok();
}),
window,
cx,
)
});
gallery.update(cx, |gallery, cx| {
gallery.palette = Some(palette.clone());
cx.notify();
});
dock_state.update(cx, |dock, cx| {
Gallery::register_panels(
dock,
layout_state.clone(),
alerts.clone(),
toasts.clone(),
palette.clone(),
gallery.downgrade(),
cx,
);