Action、快捷键、菜单与输入
GPUI 的交互系统以 Action 为中心。鼠标点击可以直接绑定闭包,键盘交互则推荐绑定到 Action,再由元素树或应用上下文处理。这样能把“用户按了什么键”和“应用要执行什么命令”解耦。
定义 Action
简单 Action 可以用 actions! 宏:
use gpui::actions;
actions!(hiposter, [OpenAbout, Quit, SendRequest]);
当前 Zed 主仓的 key dispatch 文档也支持 #[gpui::action] 定义带字段的 Action:
#[gpui::action]
struct MoveRequestTab {
direction: Direction,
}
大部分桌面命令用 unit struct 就够了,例如退出、打开设置、发送请求、格式化 JSON。
应用级 Action
应用级命令在 App 上注册:
fn quit(_: &Quit, cx: &mut App) {
cx.quit();
}
fn open_about(_: &OpenAbout, cx: &mut App) {
open_about_window(cx);
}
fn init_actions(cx: &mut App) {
cx.on_action(quit);
cx.on_action(open_about);
}
HiPoster 里也可以看到闭包式写法:
cx.on_action(|_: &Quit, cx: &mut App| {
cx.quit();
});
两种写法都能表达同一件事。函数式写法更适合复用和测试,闭包式写法更适合非常短的初始化代码。
元素级 Action
键盘优先的交互通常绑定在元素树中。当前 GPUI 文档推荐为局部区域声明 key context:
impl Render for RequestEditor {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.key_context("RequestEditor")
.on_action(cx.listener(Self::send_request))
.on_action(cx.listener(Self::format_json))
.child(self.render_form(cx))
}
}
处理函数可以是普通方法:
impl RequestEditor {
fn send_request(
&mut self,
_: &SendRequest,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.loading = true;
cx.emit(self.request.clone());
cx.notify();
}
}
元素级 Action 的好处是作用域清楚:只有焦点或 key context 落在请求编辑器中时,cmd-enter 才会发送请求。
绑定快捷键
应用启动时绑定快捷键:
cx.bind_keys([
KeyBinding::new("cmd-q", Quit, None),
KeyBinding::new("ctrl-q", Quit, None),
KeyBinding::new("alt-f4", Quit, None),
KeyBinding::new("cmd-enter", SendRequest, Some("RequestEditor")),
KeyBinding::new("ctrl-enter", SendRequest, Some("RequestEditor")),
]);
第三个参数是可选 key context。传 None 表示全局可用;传 Some("RequestEditor") 表示只在匹配上下文中生效。
快捷键设计建议:
- 退出、打开命令面板、打开设置这类命令可以全局绑定。
- 发送请求、格式化 Body、切换响应视图这类命令应绑定到具体面板。
- 文本输入框里的复制、粘贴、撤销、全选尽量复用组件库内置 Action。
菜单
系统菜单同样绑定 Action。当前 Zed 示例使用 Menu::new(...).items([...]):
fn set_app_menus(cx: &mut App) {
cx.set_menus([
Menu::new("HiPoster").items([
MenuItem::action("About HiPoster", OpenAbout),
MenuItem::separator(),
MenuItem::action("Quit HiPoster", Quit),
]),
Menu::new("Edit").items([
MenuItem::action("Undo", gpui_component::input::Undo),
MenuItem::action("Redo", gpui_component::input::Redo),
MenuItem::separator(),
MenuItem::action("Cut", gpui_component::input::Cut),
MenuItem::action("Copy", gpui_component::input::Copy),
MenuItem::action("Paste", gpui_component::input::Paste),
MenuItem::separator(),
MenuItem::action("Select All", gpui_component::input::SelectAll),
]),
]);
}
HiPoster 本地版本使用结构体字面量构造 Menu。升级到最新 Zed main 时,优先检查 crates/gpui/examples/set_menus.rs,按最新示例调整。
输入焦点
复杂键盘交互需要显式处理焦点。GPUI 测试示例展示了 Focusable 和 FocusHandle:
pub struct Counter {
focus_handle: FocusHandle,
}
impl Counter {
fn new(cx: &mut Context<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
}
}
}
impl Focusable for Counter {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
渲染时跟踪焦点:
div()
.key_context("Counter")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::increment))
打开窗口后可以主动聚焦:
let counter = cx.new(|cx| Counter::new(cx));
counter.focus_handle(cx).focus(window, cx);
counter
HTTP 工具里最实用的焦点策略是:新建 Tab 后自动聚焦 URL 输入框;发送请求时不要抢走输入焦点;响应区获得焦点后启用搜索、复制和滚动快捷键。
鼠标点击
鼠标交互可以直接挂在元素上:
Button::new("send")
.label("Send")
.primary()
.on_click(cx.listener(|this, _, _window, cx| {
this.prepare_and_emit_request(cx);
}))
如果点击在子元素中,不希望事件继续冒泡,可以:
.on_click(cx.listener(|this, _, _, cx| {
cx.stop_propagation();
this.remove_current_item(cx);
}))
典型场景是 Tab 关闭按钮:点击关闭按钮不应该同时触发父级 Tab 的选中事件。
交互设计清单
- 所有可从键盘触发的业务命令都定义为 Action。
- 全局快捷键只处理应用级命令,面板命令绑定 key context。
- 菜单项和快捷键复用同一个 Action。
- 文本输入操作优先复用
gpui_component::input内置命令。 - 新窗口、新面板和列表项需要明确焦点策略。
- 事件冒泡只在有意时发生,删除、关闭、菜单按钮通常要阻止冒泡。
有了 Action 和输入体系,应用就不只是“能点”,而是可以用键盘高效工作。