Style

GPUI 在构建 Element 时设置样式。Styled trait 提供布局、间距、颜色、边框和文字的链式方法。许多名称有意对应 Tailwind CSS utility:flex items-center gap-2 px-3 在 Rust 中写成 .flex().items_center().gap_2().px_3()。可以借助这套词汇阅读和编写 GPUI 布局,但传入的是带类型的 Rust 值,而非 CSS class。

use gpui_kit::*;
use gpui_kit::component::ActiveTheme as _;

div()
    .flex()
    .items_center()
    .gap_2()
    .px_3()
    .py_2()
    .bg(cx.theme().background)
    .text_color(cx.theme().foreground)
    .child("Search results")

链上的每次调用都会取得元素并返回元素。render 可以根据当前状态构建新树;需要长期保存的应用状态应放在 Entity 或带 key 的元素状态中。样式链描述本帧的外观,并非样式表或长期保存的组件实例。逐帧构建组件的方式见 RenderOnce。

#从第一个布局开始

先找出负责可用空间的区域,再决定哪个子元素宽度固定、哪个可以伸展。用下面的完整程序替换 examples/hello_world/src/main.rs,然后在仓库根目录运行 cargo run -p hello_world:

use gpui_kit::*;
use gpui_kit::component::{h_flex, v_flex, ActiveTheme as _};
use gpui_kit::prelude::FluentBuilder as _;

struct StyleExample;

impl Render for StyleExample {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let compact = window.viewport_size().width < px(600.);
        let layout = if compact { v_flex() } else { h_flex() };
        let documents = (1..=60).map(|number| {
            div().p_2().child(format!("Document {number:02}"))
        });

        layout
            .items_stretch()
            .size_full()
            .bg(cx.theme().background)
            .text_color(cx.theme().foreground)
            .child(
                v_flex()
                    .when(!compact, |nav| nav.w_64().flex_shrink_0())
                    .p_3()
                    .gap_2()
                    .child("Navigation")
                    .child("Overview · Documents"),
            )
            .child(
                v_flex()
                    .flex_1()
                    .min_w_0()
                    .min_h_0()
                    .child(h_flex().p_3().child("Documents"))
                    .child(
                        v_flex()
                            .id("document-list")
                            .flex_1()
                            .min_h_0()
                            .overflow_y_scroll()
                            .child(v_flex().p_3().gap_2().children(documents)),
                    ),
            )
    }
}

fn main() {
    gpui_kit::application().run(|cx| {
        gpui_kit::init(cx);
        gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| {
            cx.new(|_| StyleExample)
        })
        .expect("failed to open window");
    });
}

窗口较宽时,导航区域以固定的 w_64() 宽度位于左侧,文档面板占用剩余空间。把内容区域缩小到 600 逻辑像素以下:同一个导航区域会移到文档面板上方,而文档列表仍可独立滚动。滚到 Document 60,再向两个方向调整窗口宽度;列表滚动时,标题栏应保持在原位。这里的阈值是在视图 render 时求值的 Rust 条件,不是 Tailwind 响应式前缀。这个小练习中的导航只是示意文本;真实应用应提供可操作的导航控件。

h_flex() 建立横向布局,并默认让子元素沿交叉轴居中。.items_stretch() 覆盖这个默认值,让两个面板占满整行高度。v_flex() 建立纵向布局,子元素默认沿宽度方向拉伸。宽窗口中的导航面板保持固定宽度,文档面板占用剩余宽度。滚动区域占据标题栏下方的剩余高度。窗口大小为最外层的 .size_full() 提供确定高度,min_h_0() 则允许内部弹性区域收缩成滚动视口。

#确定尺寸该由谁负责

需求设置位置原因
同级元素之间的距离在父元素上设置 .gap_3()gap 分隔子元素,不会在容器外边缘增加内边距。
面板内部的留白在面板上设置 .p_3()padding 把内容向内推,并参与面板的布局尺寸。
固定侧栏与弹性内容侧栏 .w_64().flex_shrink_0();内容 .flex_1().min_w_0()侧栏保持宽度,内容可收缩到小于文字自然宽度。
标题栏下方的滚动内容有确定高度的纵向容器;滚动子元素 .flex_1().min_h_0()子元素可缩进剩余高度,从而形成真正的滚动视口。
父元素宽度的一半在子元素上设置 .w(relative(0.5))布局时,比例按相关父尺寸解析。

w_full() 和 h_full() 表示填满可用尺寸。百分比高度仍要求上层有确定高度。最小值和最大值限制最终尺寸;它们不会让没有高度约束的滚动区域自动形成视口。横向布局中的单行长标题可在标题容器上组合 .flex_1().min_w_0().truncate()。.truncate() 只改变文字溢出方式,无法让不肯收缩的同级元素腾出宽度。

#选择裁剪、滚动或定位

.overflow_hidden() 裁剪内容,但不会让内容滚动。在有状态的元素上,.overflow_y_scroll() 可在高度受限后启用纵向滚动。GPUI Kit 的 .overflow_y_scrollbar() 会加入可见滚动条,并以原元素作为滚动区域;它来自 ScrollableElement 扩展,而非 Styled 方法。每个滚动区域应有一个明确的负责元素。如果滚动条要贴着面板边缘,内容 padding 应放在滚动区域内部。滚动归属与测量方式见编码指南。

普通 Flex 子元素占用布局空间。要让角标覆盖内容而不占一行或一列,可在容器上设置 .relative(),在角标上设置 .absolute().top_0().right_0()。偏移 setter 用于定位绝对子元素;它们不会自动将普通 Flex 子元素变成绝对定位。后面的同级元素通常绘制在前面的同级元素之上;通用 Styled 没有 z_index(...) 方法。

#主题与尺寸尺度

应用界面的颜色和圆角应从 cx.theme() 读取语义值。GPUI Kit 组件已应用正常的主题外观;实例样式可用于局部布局或有意的细化。具名间距和尺寸方法使用 rem 尺度:_1 为 0.25rem、_2 为 0.5rem、_3 为 0.75rem、_4 为 1rem。在 GPUI Kit 的 Root 中,当前主题的基础字号决定窗口的 rem 大小,因此字号或缩放变化也会改变基于 rem 的几何尺寸。命名尺度不合适时用 .gap(rems(0.625)) 等带类型的 setter;只有确实需要像素尺寸时才使用 px(...)。长度类型见几何,主题与 rem 设置见字体。

#排查布局结果

现象检查方向
窗口缩窄后导航没有移到文档上方把可绘制内容区域缩到 600 逻辑像素以下。条件在 render 中读取 window.viewport_size().width;仅改变显示器缩放倍率不会跨过这个逻辑像素阈值。
无法滚到 Document 60把指针移到文档列表上并在该区域滚动。让有高度约束的列表区域拥有 .id("document-list").overflow_y_scroll(),并在该区域及外层纵向容器上保留 .flex_1().min_h_0()。
滚动时标题栏也跟着移动确认标题栏与列表视口是同级元素,不要把标题栏放进滚动元素内部。
面板标题在顶部消失h_flex() 默认让子元素居中;让整行子元素拉伸,或让该面板占满高度。
标题溢出而没有截断在弹性子元素上用 .min_w_0() 解除最小宽度约束,并限制文字宽度。
列表越过窗口而没有滚动让祖先拥有确定高度,用 .min_h_0() 允许弹性子元素收缩,并在预期的视口上设置滚动。
滚动条缩在面板边缘以内检查哪个元素负责滚动,以及 padding 是否包在滚动元素外面。
主题缩放后布局变化重新检查基于 rem 的尺寸,以及仍沿用旧 rem 大小的测量缓存。

#常用 Styled 方法

Tailwind 名称中的连字符在 GPUI 方法中写成下划线。有对应样式概念时,第一列链接到相应的 Tailwind CSS 官方参考页,并在新窗口打开。表中列的是 GPUI 方法名;可在实现 Styled 的值上调用,例如 div().gap_2()。这些表格覆盖 Styled 中各类独立操作,以及宏生成的通用 setter;大量数字变体按下文所述的方法族归纳,不逐个占用数千行。链接说明对应的样式概念,不表示 GPUI 与浏览器的行为完全相同。

#显示与可见性

方法说明
block使用块布局。
flex使用 Flexbox 布局。
grid使用 Grid 布局。
hidden从布局和绘制中移除元素。
invisible保留布局空间,但不绘制元素。
visible保留布局,并恢复绘制。

#Flexbox 与 Grid

方法说明
flex_row沿横向排列 Flex 子项。
flex_col沿纵向排列 Flex 子项。
flex_wrap允许 Flex 子项换行。
flex_nowrap让 Flex 子项保持在同一行。
items_start让子项沿交叉轴向起点对齐。
items_center让子项沿交叉轴居中。
items_stretch让子项沿交叉轴拉伸。
self_center让当前子项沿父容器的交叉轴居中。
self_stretch让当前子项沿父容器的交叉轴拉伸。
justify_center让子项沿主轴居中。
justify_between把主轴剩余空间分配到子项之间。
content_between沿交叉轴分配换行后的各行。
flex_basis设置带类型的主轴初始尺寸。
flex_1以零 Flex basis 伸长或收缩。
flex_auto以自动 basis 为起点伸长或收缩。
flex_grow_1允许 Flex 子项伸长。
flex_shrink_0阻止 Flex 子项收缩。
grid_cols设置指定列数的 Grid 模板。
grid_rows设置指定行数的 Grid 模板。
col_span跨越指定数量的 Grid 列。
row_span跨越指定数量的 Grid 行。
flex_row_reverse反向排列横向子项。
flex_col_reverse反向排列纵向子项。
flex_wrap_reverse反向排列换行后的 Flex 行。
items_end让子项沿交叉轴向终点对齐。
items_baseline按文字基线对齐子项。
self_start让当前子项沿交叉轴向起点对齐。
self_end让当前子项沿交叉轴向终点对齐。
self_flex_start让当前子项向 Flex 起点对齐。
self_flex_end让当前子项向 Flex 终点对齐。
self_baseline按文字基线对齐当前子项。
justify_start让子项向主轴起点聚集。
justify_end让子项向主轴终点聚集。
justify_around在子项周围分配空间。
justify_evenly沿主轴均匀分配间距。
content_normal使用默认的交叉轴行排列。
content_start让换行后的各行向交叉轴起点聚集。
content_center让换行后的各行沿交叉轴居中。
content_end让换行后的各行向交叉轴终点聚集。
content_around在换行后的各行周围分配空间。
content_evenly在换行后的各行之间均匀分配空间。
content_stretch沿交叉轴拉伸换行后的各行。
flex_initial使用自动 basis,可收缩但不伸长。
flex_none同时禁止 Flex 伸长与收缩。
flex_grow设置数字形式的 Flex 伸长系数。
flex_grow_0禁止 Flex 伸长。
flex_shrink设置数字形式的 Flex 收缩系数。
flex_shrink_1允许 Flex 收缩。
grid_cols_min_content以 min-content 为最小值创建列。
grid_cols_max_content以 max-content 为约束创建列。
grid_rows_min_content以 min-content 为最小值创建行。
grid_rows_max_content以 max-content 为约束创建行。
col_start设置 Grid 列起始线。
col_start_auto自动选择列起始位置。
col_end设置 Grid 列结束线。
col_end_auto自动选择列结束位置。
col_span_full跨越完整的 Grid 列范围。
row_start设置 Grid 行起始线。
row_start_auto自动选择行起始位置。
row_end设置 Grid 行结束线。
row_end_auto自动选择行结束位置。
row_span_full跨越完整的 Grid 行范围。

#间距与尺寸

方法说明
gap_2行间隙和列间隙均设为 0.5rem。
gap在双轴设置带类型的间隙。
gap_x_2列间隙设为 0.5rem。
gap_y_2行间隙设为 0.5rem。
p_4四边内边距设为 1rem。
p四边设置带类型的内边距。
px_3水平内边距设为 0.75rem。
py_2垂直内边距设为 0.5rem。
mt_4上外边距设为 1rem。
m四边设置带类型的外边距。
m_auto四边外边距设为自动。
w设置带类型的宽度。
w_full填满可用宽度。
h设置带类型的高度。
h_full填满可用高度。
min_w设置带类型的最小宽度。
min_w_0允许宽度收缩到零。
max_w设置带类型的最大宽度。
size同时设置带类型的宽度与高度。
size_4宽度和高度均设为 1rem。
aspect_square保持 1:1 的宽高比。
mt设置带类型的上外边距。
mb设置带类型的下外边距。
mx设置带类型的水平外边距。
my设置带类型的垂直外边距。
ml设置带类型的左外边距。
mr设置带类型的右外边距。
px设置带类型的水平内边距。
py设置带类型的垂直内边距。
pt设置带类型的上内边距。
pb设置带类型的下内边距。
pl设置带类型的左内边距。
pr设置带类型的右内边距。
gap_x设置带类型的列间隙。
gap_y设置带类型的行间隙。
min_h设置带类型的最小高度。
max_h设置带类型的最大高度。
aspect_ratio设置数字形式的宽高比。

#定位与溢出

方法说明
relative保持正常布局位置,并作为定位祖先。
absolute用 inset 偏移,相对祖先定位。
inset四边设置带类型的偏移。
inset_0上、右、下、左偏移均设为零。
top设置带类型的上方偏移。
top_0上方偏移设为零。
overflow_hidden在双轴裁剪溢出的内容。
overflow_x_hidden只裁剪水平溢出。
overflow_y_hidden只裁剪垂直溢出。
bottom设置带类型的下方偏移。
left设置带类型的左方偏移。
right设置带类型的右方偏移。
scrollbar_width在滚动布局中预留带类型的滚动条宽度。

#颜色与边框

方法说明
bg设置带类型的背景填充。
text_color设置带类型的文字颜色。
border_1四边设置一像素边框。
border_t_1上边设置一像素边框。
border_color设置带类型的边框颜色。
border_dashed绘制虚线边框。
rounded设置带类型的圆角半径。
rounded_lg使用命名的大圆角半径。
rounded_full让圆角半径尽可能适应该元素的尺寸。
border四边设置带类型的边框宽度。
border_t设置带类型的上边框宽度。
border_b设置带类型的下边框宽度。
border_l设置带类型的左边框宽度。
border_r设置带类型的右边框宽度。
border_x设置带类型的左右边框宽度。
border_y设置带类型的上下边框宽度。
rounded_t设置带类型的上侧圆角。
rounded_b设置带类型的下侧圆角。
rounded_l设置带类型的左侧圆角。
rounded_r设置带类型的右侧圆角。
rounded_tl设置带类型的左上圆角。
rounded_tr设置带类型的右上圆角。
rounded_bl设置带类型的左下圆角。
rounded_br设置带类型的右下圆角。

#排版

方法说明
font_family按名称设置字体族。
font_weight设置带类型的字重。
italic使用斜体文字。
text_size设置带类型的字号。
text_xs使用特小字号。
text_sm使用小字号。
text_lg使用大字号。
text_left左对齐文字。
text_center让一行中的文字居中。
text_right右对齐文字。
line_height设置带类型的行高。
whitespace_normal允许文字正常换行。
whitespace_nowrap阻止文字换行。
text_ellipsis在溢出文字末尾添加省略号。
truncate裁剪单行文字并添加省略号。
line_clamp将文字限制为指定行数。
text_base使用基础字号。
text_xl使用特大字号。
text_2xl使用 2× 大字号。
text_3xl使用 3× 大字号。
text_align设置带类型的文字对齐方式。
not_italic使用非斜体文字。
underline添加文字下划线。
line_through添加文字删除线。
text_decoration_none移除文字装饰线。
text_decoration_color设置文字装饰线颜色。
text_decoration_solid使用实线文字装饰。
text_decoration_wavy使用波浪线文字装饰。
text_decoration_0把文字装饰线宽度设为零。
text_decoration_1把文字装饰线宽度设为一像素。
text_decoration_2把文字装饰线宽度设为两像素。
text_decoration_4把文字装饰线宽度设为四像素。
text_decoration_8把文字装饰线宽度设为八像素。
text_overflow设置带类型的文字溢出行为。
font_features设置 OpenType 字体特性。

#效果与光标

方法说明
shadow_none移除盒阴影。
shadow_sm使用命名的小盒阴影。
shadow_md使用命名的中等盒阴影。
opacity用浮点值设置不透明度。
cursor_pointer悬停时使用指向手形光标。
cursor_text悬停时使用文本插入光标。
shadow设置带类型的盒阴影列表。
shadow_2xs使用命名的 2× 特小盒阴影。
shadow_xs使用命名的特小盒阴影。
shadow_lg使用命名的大盒阴影。
shadow_xl使用命名的特大盒阴影。
shadow_2xl使用命名的 2× 特大盒阴影。
cursor设置带类型的鼠标光标。
cursor_default使用默认光标。
cursor_move使用移动光标。
cursor_not_allowed使用禁止操作光标。
cursor_context_menu使用上下文菜单光标。
cursor_crosshair使用十字光标。
cursor_vertical_text使用竖排文字光标。
cursor_alias使用别名光标。
cursor_copy使用复制光标。
cursor_no_drop使用禁止拖放光标。
cursor_grab使用抓取光标。
cursor_grabbing使用正在抓取的光标。
cursor_ew_resize使用水平调整尺寸光标。
cursor_ns_resize使用垂直调整尺寸光标。
cursor_nesw_resize使用东北至西南方向调整尺寸光标。
cursor_nwse_resize使用西北至东南方向调整尺寸光标。
cursor_col_resize使用列宽调整光标。
cursor_row_resize使用行高调整光标。
cursor_n_resize使用向上调整尺寸光标。
cursor_e_resize使用向右调整尺寸光标。
cursor_s_resize使用向下调整尺寸光标。
cursor_w_resize使用向左调整尺寸光标。

#GPUI 特有方法

这些 API 没有直接对应的 Tailwind utility。方法名不加链接,以免暗示不存在的对应关系。

方法说明
font用带类型的 GPUI Font 替换文字字体。
min_size同时设置带类型的最小宽度与高度。
max_size同时设置带类型的最大宽度与高度。
text_bg设置文字片段而非元素盒的背景颜色。
text_ellipsis_start在开头截断,保留文字末尾。
text_ellipsis_middle在中间截断,保留文字两端。
debug在 debug 构建中绘制调试轮廓。
debug_below在 debug 构建中为当前元素及符合条件的后代绘制调试轮廓。
style返回元素使用的可变 StyleRefinement;这是 trait 的底层访问方法。
text_style返回元素样式中的可变文字 refinement。
grid_location_mut访问 StyleRefinement 中可变的 Grid 位置。

宏还为尺寸(w、h、size、min_size、min_w、min_h、max_size、max_w、max_h)、间隙(gap、gap_x、gap_y)、外边距(m、mt、mb、mx、my、ml、mr)、内边距(p、pt、pb、px、py、pl、pr)和偏移(inset、top、bottom、left、right)生成方法。例如 w_64、px_3 和 top_0 都是真实存在的方法。共用的数字后缀包括 0、0p5、1、1p5、2、2p5、3、3p5、4 到 12 的每个整数,以及 16、20、24、32、40、48、56、64、72、80、96、112、128。此外还有 _px、_full 和 _1_2 等分数后缀;只有接受 auto 的方法族才生成 _auto。非 auto 值还生成 mt_neg_2 这样的 _neg_ 形式。边框各侧(border、border_t、border_b、border_l、border_r、border_x、border_y)有 0 到 12,以及 16、20、24、32 的像素宽度后缀。各侧和各角的圆角有 none、xs、sm、md、lg、xl、2xl、3xl、full 这些后缀。只有在生成的属性符合布局需要时才使用它们。

间距便捷方法使用基于 rem 的尺度:_1 为 0.25rem,_2 为 0.5rem,_3 为 0.75rem,_4 为 1rem。命名方法没有覆盖所需数值时,可使用 .gap(rems(0.625))、.w(px(240.)) 或 .w(relative(0.5)) 这类带类型的 setter。relative(0.5) 表示可用相对尺寸的一半;px(...) 表示像素。命名尺度和方法范围以 GPUI 的实现为准,不要假定每个 Tailwind class 都有对应方法。

#样式调用改动了什么

每个实现 Styled 的元素都提供 fn style(&mut self) -> &mut StyleRefinement。例如,.px_3() 写入相应的可选 padding 字段,.bg(...) 写入 background 字段。合并样式时,未设置的字段不会覆盖已有值。解析后的 Style 同时包含布局数据与外观数据。

Styled calls → StyleRefinement → resolved Style
                                    ├─ layout fields → Taffy → bounds
                                    └─ color, text, shadow, cursor → GPUI paint and interaction

在元素的 request_layout 阶段,GPUI 将 display、size、padding、gap、Flex 对齐、position 和 Grid 位置等布局字段连同子元素的布局 ID 交给 Taffy。Taffy 计算几何尺寸与位置。GPUI 随后在 prepaint 和 Paint 阶段使用所得 bounds 绘制并进行命中测试。Taffy 不实现 GPUI 的文字 shaping、hover listener、Action 或绘制。

StyleRefinement 本身也实现了 Styled,因此状态样式闭包可以使用相同的 utility 方法。.hover(|style| style.bg(...)) 这样的交互变体属于 InteractiveElement,需要交互型元素。条件构建方法有所不同:.when(...) 在构建本帧时决定是否追加链式步骤。

#Fluent 组合与 trait 边界

链式 API 由多个 trait 共同提供。Styled 提供样式方法,ParentElement 提供 .child(...)。InteractiveElement 提供 .id(...),返回支持 .overflow_y_scroll() 等依赖身份的方法的 Stateful<Div>;它还提供 .hover(...) 等状态样式细化。每个 IntoElement 都实现 FluentBuilder;只实现 IntoElement 不会获得样式、子元素或交互能力。缺少某个方法时,检查接收者实现了哪个 trait,以及之前的调用是否改变了其类型。

FluentBuilder 方法作用
map转换当前值,返回类型也可以改变。
when布尔条件为真时追加构建步骤。
when_else在两个返回相同 builder 类型的步骤间选择。
when_some可选值存在时追加步骤,并把值传给闭包。
when_none引用的可选值为空时追加步骤。
use gpui_kit::*;
use gpui_kit::component::ActiveTheme as _;

div()
    .id("result-row")
    .px_3()
    .py_2()
    .when(selected, |row| row.bg(cx.theme().selection))
    .when_some(subtitle, |row, text| row.child(text))
    .child(title)

条件闭包取得 builder 并返回 builder。条件在本次 render 中求值,并非订阅。.map(...) 可以返回不同类型。跨帧变化的状态应由 Entity 持有。

例如,.when(selected, ...) 在构建本帧时判断 selected;.hover(|style| ...) 则安装指针悬停时的样式细化。滚动方法需要有状态的元素,所以应先给预期的滚动负责元素设置 .id(...)。不能假定任意实现 IntoElement 的组件都接受 .child(...) 或 .bg(...);应检查该组件自己的 builder API,或用负责这些样式的 div() 包裹它。

GPUI Kit 另有 StyledExt,提供 h_flex、v_flex 和 refine_style 等不依赖主题的辅助方法;ThemeStyled 则提供 popover_style(cx) 等依赖主题的外观方法。它们是在 GPUI Styled 之上的扩展,不属于 Tailwind utility。自行实现元素时,应从 style() 返回元素的 StyleRefinement,并在布局与绘制阶段应用解析后的样式。只实现元素确实支持的子元素与交互 trait。

Tailwind 对应关系有明确范围:这里没有 CSS 选择器、层叠、响应式前缀,也不保证每个 Tailwind utility 都存在对应的 GPUI 方法。几何布局使用 GPUI 带类型的辅助方法,产品外观使用 GPUI Kit 主题 token,行为则使用明确的元素或实体 API。

CC BY 4.0 · GPUI Kit 有权授权的原创文档正文与图示可在注明署名后复制或改编。 许可条款。请署名 GPUI Kit,链接到 本页原文及 CC BY 4.0 许可,并注明是否修改。 代码示例与软件源码仍适用 Apache-2.0。既有 Apache 权利不受影响;第三方材料保留各自许可。