Skip to main content

状态管理、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 的核心规则是:不要绕开上下文去修改实体。通过 readupdate,框架才能知道状态变化发生在哪里。

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 更容易扩展。