Skip to main content

应用入口、窗口与生命周期

GPUI 应用的生命周期由平台事件循环驱动。你不会在 main 里写一个手动循环,而是把初始化逻辑交给 Application::rungpui_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 的网络层使用 reqwesttokio,所以在 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.spawntokio::fs 可能没有可用运行时。

创建窗口

GPUI 的窗口创建通常发生在 App 上下文里。当前 Zed 主仓示例使用 Bounds::centeredWindowBounds::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 线程。

不要把 WindowContext<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,后者走 EntityContext<Self>