元素、布局与组件库
GPUI 的 UI 写法接近声明式 DSL:在 render 中根据当前状态返回元素树。你可以直接使用 div()、h_flex()、v_flex() 等基础元素,也可以使用 GPUI Component 提供的按钮、输入框、Tab、菜单、滚动区和分栏。
Render 与 IntoElement
任何实现了 Render 的视图都需要返回 impl IntoElement:
impl Render for ApiTab {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = self.theme.colors();
v_flex()
.size_full()
.bg(colors.bg)
.child(self.render_url_bar(&colors, window, cx))
.child(self.render_body(&colors, window, cx))
}
}
拆分 UI 时,辅助函数也返回 impl IntoElement:
fn render_url_bar(
&self,
colors: &ThemeColors,
_window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
h_flex()
.px_4()
.py_2()
.gap_3()
.border_b_1()
.border_color(colors.border)
.child(Button::new("method").label("GET").ghost())
.child(Input::new(&self.url_input).flex_1())
.child(Button::new("send").label("Send").primary())
}
这种拆法比一个超长 render 更容易维护,也更容易把复杂界面分成可测试的状态转换函数。
Flexbox 布局
GPUI 的布局方法高度接近 Web Flexbox:
v_flex():垂直排列。h_flex():水平排列。.flex_1():占据剩余空间。.items_center():交叉轴居中。.justify_between():主轴两端分布。.gap_2():子元素间距。.overflow_y_scrollbar():垂直滚动。
HiPoster 的总体布局可以抽象成:
v_flex()
.size_full()
.child(render_title_bar())
.child(
h_flex()
.flex_1()
.child(render_sidebar())
.child(
v_flex()
.flex_1()
.child(render_tab_bar())
.child(render_active_tab())
)
)
复杂桌面应用几乎都是这种组合:外层定方向,中间用 flex_1 分配空间,局部用 gap、padding、border 控制节奏。
常用样式链
尺寸:
div().w_full().h_full()
div().size(px(32.))
div().w(px(280.)).min_w(px(160.))
间距:
div().p_4().px_3().py_2().mt_2().gap_2()
视觉:
div()
.bg(colors.surface)
.border_1()
.border_color(colors.border)
.rounded_md()
交互:
div()
.cursor_pointer()
.hover(|style| style.bg(colors.surface))
.active(|style| style.bg(colors.sidebar))
样式链应尽量保持“布局、尺寸、视觉、行为”的顺序,长链条可以换行,避免一行里堆太多信息。
InputState 与输入框
GPUI Component 的输入框通常先创建 InputState,再在渲染时传给 Input:
let url_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Enter URL...")
.default_value(request.url.clone())
});
渲染:
Input::new(&self.url_input).flex_1()
读取:
let url = self.url_input.read(cx).value().to_string();
写入:
self.url_input.update(cx, |input, cx| {
input.set_value("https://example.com".to_string(), window, cx);
});
多行代码编辑场景可以启用 highlighter:
let body_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Request body...")
.multi_line(true)
.code_editor(Language::Json)
});
Tab、菜单与可拖拽分栏
请求面板常用 Tab 切换:
TabBar::new("request-tabs")
.w_full()
.child(
Tab::new()
.label("Params")
.selected(self.active_tab == RequestTab::Params)
.on_click(cx.listener(|this, _, _, cx| {
this.active_tab = RequestTab::Params;
cx.notify();
})),
)
下拉菜单适合方法、Body 类型、认证方式和主题选择:
Button::new("body-type")
.label(self.request.content_type.clone())
.ghost()
.dropdown_caret(true)
.dropdown_menu({
let view = cx.weak_entity();
move |menu, _, _| {
menu.item(PopupMenuItem::new("JSON").on_click(move |_, _, cx| {
view.update(cx, |this, cx| {
this.set_content_type("application/json", cx);
})
.ok();
}))
}
})
请求编辑区和响应区适合用可拖拽分栏:
v_resizable("main-split")
.child(resizable_panel().size(px(450.)).min_size(px(120.)).child(request_panel))
.child(resizable_panel().flex_1().min_size(px(160.)).child(response_panel))
渲染列表
历史记录、Header、Params、Form Data 都是列表。GPUI 里可以用 .children(iter.map(...)):
v_flex()
.children(self.history.iter().enumerate().map(|(index, item)| {
h_flex()
.id(("history-item", index))
.p_2()
.cursor_pointer()
.child(Label::new(format!("{:?}", item.request.method)))
.child(Label::new(item.request.url.as_str()).flex_1())
}))
列表项应当有稳定 id,尤其是可交互列表。简单 demo 可以用 index,真实项目若有持久化 id,优先用业务 id。
布局建议
- 优先使用
h_flex/v_flex表达结构,不要用绝对坐标堆界面。 - 每个固定格式控件都给出稳定尺寸或
min_size,避免 hover、loading 和文本变化导致布局跳动。 - 大面板用
flex_1,小控件用px()、h_、w_约束。 - 长文本用
overflow_hidden或在业务层截断,Tab 标题和历史记录尤其需要。 - 把重复结构抽成渲染函数,不要为了“组件化”过早抽象泛型控件。
到这里,界面已经能搭起来了。下一章要解决的是:状态放哪里、子组件如何告诉父组件发生了什么、异步任务回来后怎么安全更新 UI。