状态管理、Entity 与通信
GPUI 的状态管理围绕 Entity<T> 和 Context<T> 展开。普通 Rust 结构体表达数据,Entity<T> 让数据进入 GPUI 的运行时,Context<T> 则负责创建、读取、更新、订阅和通知。
Context 是唯一入口
创建实体:
let tab = cx.new(|cx| ApiTab::with_request(request, theme, window, cx));
读取实体:
let title = tab.read(cx).request.url.clone();
更新实体:
tab.update(cx, |tab, cx| {
tab.loading = true;
cx.notify();
});
通知重绘:
cx.notify();
GPUI 的核心规则是:不要绕开上下文去修改实体。通过 read 和 update,框架才能知道状态变化发生在哪里。
Entity 有身份
Entity<T> 是一个稳定句柄。即使 T 内部字段变化,父视图仍然可以持有同一个句柄:
pub struct Hiposter {
pub tabs: Vec<Entity<ApiTab>>,
pub active_tab_index: usize,
}
这让多 Tab 应用非常自然:
pub fn add_tab(&mut self, request: HttpRequest, window: &mut Window, cx: &mut Context<Self>) {
let tab = cx.new(|cx| ApiTab::with_request(request, self.theme, window, cx));
self.tabs.push(tab);
self.active_tab_index = self.tabs.len() - 1;
cx.notify();
}
渲染当前 Tab 时直接把子实体作为 child:
if let Some(tab) = self.tabs.get(self.active_tab_index) {
div().flex_1().child(tab.clone()).into_any_element()
} else {
div().flex_1().into_any_element()
}
Observe:子状态变化后让父视图重绘
当子视图内部调用 cx.notify() 时,父视图不一定自动重绘。父视图可以观察子实体:
let observe_tab = cx.observe(&tab, |_, _, cx| {
cx.notify();
});
self.subscriptions.push(observe_tab);
典型场景:
- Tab 标题来自子视图 URL 输入框。
- 父级列表需要反映子级状态变化。
- 子组件尺寸、loading 状态或标题变化影响父布局。
保留 Subscription 很重要。若不保存,订阅可能提前 drop。
Subscribe:子组件向父组件发业务事件
observe 只表示“它变了”,不知道具体发生了什么。业务事件应该用 EventEmitter:
impl EventEmitter<HttpRequest> for ApiTab {}
子视图发送请求事件:
self.update_request_state(cx);
self.loading = true;
cx.emit(self.request.clone());
cx.notify();
父视图订阅:
let subscription = cx.subscribe(&tab, |this, tab, request: &HttpRequest, cx| {
this.execute_request(tab, request.clone(), cx);
});
self.subscriptions.push(subscription);
这种写法把请求按钮和网络执行解耦:ApiTab 只知道用户要发送当前请求,Hiposter 决定如何记录历史、启动异步任务和把响应写回 Tab。
WeakEntity:异步边界的安全句柄
异步任务回来时,原来的视图可能已经被关闭。不要假设实体还活着。GPUI 会在 cx.spawn 中提供弱引用:
cx.spawn(move |this: WeakEntity<Self>, mut cx| async move {
let result = load_data().await;
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| {
this.apply_result(result);
cx.notify();
})
.ok();
}
})
.detach();
如果你已经捕获了某个子实体,也应当接受更新可能失败:
let _ = tab.update(&mut cx, |tab, cx| {
tab.loading = false;
tab.set_response(response, cx);
});
这不是“吞错误”,而是承认 UI 资源可能被用户关闭。
状态同步:URL 和 Params
HiPoster 有一个很典型的双向同步问题:
- 用户直接编辑 URL,要解析查询参数并同步到 Params 表格。
- 用户编辑 Params 表格,要重新拼 URL。
它用两个脏标记避免死循环:
pub struct ApiTab {
pub url_dirty: bool,
pub params_dirty: bool,
}
URL 输入变化时:
cx.observe(&url_input, |this, _, cx| {
this.params_dirty = true;
cx.notify();
})
.detach();
Params 输入变化时:
cx.observe(&key, |this, _, cx| {
this.url_dirty = true;
cx.notify();
})
.detach();
渲染前统一同步:
if self.params_dirty {
self.sync_params_from_url(window, cx);
}
if self.url_dirty {
self.sync_url_from_params(window, cx);
}
这个模式很适合任何“同一数据有两种编辑形态”的 UI,例如颜色选择器和十六进制输入、日期范围和快捷筛选、JSON 表单和原始文本。
状态管理原则
- Model 保持纯净,View 持有 UI 状态,Entity 管理生命周期。
- 父传子用
Entity<T>或初始化参数,子传父用事件。 - 不要把
Context<T>存进结构体。 - 订阅要保存
Subscription。 - 异步任务里优先使用
WeakEntity或容错的Entity::update。 cx.notify()只在状态真的影响渲染时调用。
把这些规则守住,GPUI 项目会比传统回调式 GUI 更容易扩展。