应用入口、窗口与生命周期
GPUI 应用的生命周期由平台事件循环驱动。你不会在 main 里写一个手动循环,而是把初始化逻辑交给 Application::run 或 gpui_platform::application().run。回调中的 App 上下文就是整个应用和窗口系统的入口。
main.rs 只做一件事
HiPoster 的 main.rs 很干净:
#![cfg_attr(target_os = "windows", windows_subsystem = "windows")]
fn main() {
hiposter_gpui::run();
}
这样做有两个好处:
- GUI 入口、库代码和测试代码分开。
- Windows 发布版不会弹出额外控制台窗口。
初始化异步运行时
HiPoster 的网络层使用 reqwest 和 tokio,所以在 GPUI 启动前先创建 Tokio runtime:
pub fn run() {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("build tokio runtime");
let _guard = runtime.enter();
let app = gpui_platform::application().with_assets(AppAssets);
app.run(move |cx| {
gpui_component::init(cx);
open_main_window(cx);
});
}
这里的重点是 runtime 必须活得足够久。不要在一个临时函数里创建 runtime 后立刻 drop,否则后续 cx.spawn 或 tokio::fs 可能没有可用运行时。
创建窗口
GPUI 的窗口创建通常发生在 App 上下文里。当前 Zed 主仓示例使用 Bounds::centered 加 WindowBounds::Windowed:
fn open_main_window(cx: &mut App) {
let bounds = Bounds::centered(None, size(px(1400.), px(900.)), cx);
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
};
cx.open_window(options, |_, cx| {
cx.new(|_| HiposterShell)
})
.expect("open main window");
}
HiPoster 当前依赖版本和 GPUI Component 组合下的写法是:
fn open_main_window(cx: &mut App) {
let options = WindowOptions {
window_bounds: Some(WindowBounds::centered(size(px(1400.), px(900.)), cx)),
titlebar: Some(gpui_component::TitleBar::title_bar_options()),
..Default::default()
};
cx.open_window(options, |window, cx| {
let view = cx.new(|cx| Hiposter::new(window, cx));
cx.new(|cx| gpui_component::Root::new(view, window, cx))
})
.expect("open main window");
}
窗口创建闭包里可以拿到两个上下文:
window: &mut Window:窗口级操作,例如焦点、布局、输入状态初始化。cx: &mut Context<T>或窗口创建上下文:创建根视图、读写实体、发通知。
如果使用 GPUI Component,根视图通常要被 Root 包裹;它负责组件库需要的基础设施,比如主题和输入处理。
创建子窗口
关于窗口、设置窗口、浮层工具等可以通过同样方式打开:
pub fn open_about_window(cx: &mut App) {
let options = WindowOptions {
window_bounds: Some(WindowBounds::centered(size(px(400.), px(300.)), cx)),
titlebar: Some(gpui_component::TitleBar::title_bar_options()),
kind: WindowKind::Normal,
is_movable: true,
..Default::default()
};
cx.open_window(options, |window, cx| {
let view = cx.new(|cx| AboutWindow::new(cx));
cx.new(|cx| gpui_component::Root::new(view, window, cx))
})
.ok();
}
子窗口不要直接共享大量可变状态。推荐传入只读配置、Entity<T> 句柄或通过事件回传结果。
生命周期中的常见入口
app.run 回调里通常完成这些工作:
- 初始化组件库:
gpui_component::init(cx)。 - 注册菜单:
cx.set_menus(...)。 - 注册全局 Action:
cx.on_action(...)。 - 绑定快捷键:
cx.bind_keys(...)。 - 打开主窗口:
cx.open_window(...)。 - 初始化全局资源:主题、字体、资源、日志、配置路径。
在窗口视图的构造函数中,通常完成这些工作:
- 创建输入框、列表项和子视图。
- 读取本地配置和历史记录。
- 订阅子视图事件。
- 应用当前主题。
Context 的边界
GPUI 的上下文类型很多,初学时容易混。先记住这个实践规则:
App管全局应用和窗口,例如退出、菜单、打开窗口。Window管单个窗口,例如输入初始化、焦点和尺寸。Context<Self>管当前实体,例如读写状态、订阅、通知重绘。AsyncApp用在异步任务里,把结果安全送回 UI 线程。
不要把 Window 或 Context<Self> 存进结构体长期持有。需要时由 GPUI 在 render、事件回调或更新闭包中传给你。
关闭与退出
退出应用可以注册 Action:
actions!(hiposter, [Quit]);
cx.on_action(|_: &Quit, cx: &mut App| {
cx.quit();
});
cx.bind_keys([
KeyBinding::new("cmd-q", Quit, None),
KeyBinding::new("ctrl-q", Quit, None),
KeyBinding::new("alt-f4", Quit, None),
]);
窗口内关闭一个业务 Tab 则不应该退出应用,只需要更新根视图状态:
pub fn close_tab(&mut self, index: usize, cx: &mut Context<Self>) {
self.tabs.remove(index);
if self.active_tab_index >= self.tabs.len() && !self.tabs.is_empty() {
self.active_tab_index = self.tabs.len() - 1;
}
cx.notify();
}
应用生命周期的关键是把“应用级事件”和“视图级状态变化”分开。前者走 App,后者走 Entity 和 Context<Self>。