Bevy UI 的 em/rem 字号单位:Val::Em、Val::Rem 迁移与实战指南(0.19 → 0.20)
Bevy UI 在 0.20 中引入了 em 与 rem 两种相对字号单位,使 Val 的解析结果能够跟随字体大小缩放,这对"设计时定死像素、运行期调整字号"的可访问性与多设备适配场景非常有用。本文以官方迁移指南 val_em_and_rem.md 为主体,结合 release notes 与仓库源码,完整讲解新增的 Val::Em / Val::Rem 变体、受影响的 resolve 方法签名、EmSize 组件的生命周期,以及 GridTrack、FontSize::eval 等周边 API 的迁移方式。
为什么需要 em 和 rem
em 表示"当前节点字号的倍数",rem 表示"根字号(全局基准字号)的倍数"。两者的数据来源不同:
em:相对于设置该值的节点自身的字号,由节点实体上的EmSize组件提供;rem:相对于全局的根字号,由既有的RemSize资源提供。
典型场景是:你希望 UI 在作者态下布局合理,但又能在运行期整体放大或缩小文字(例如无障碍功能,或在不同分辨率/设备上的适配)。若尺寸全部用 px 硬编码,改字号后布局会失衡;用 em/rem 表达,尺寸就能随字号一起伸缩。
官方给出的最小示例(bsn! 语法):
bsn! {
Node { width: em(10) }
Text("Hello")
TextFont { font_size: FontSize::Rem(1.5) }
}
另外注意一个行为变化:默认字号从 px(20) 变为 rem(1)。如果你从不修改 RemSize,这只是无副作用的等价替换;但一旦你调整了 RemSize,默认文字会随之缩放。
Val 新增变体与字符串解析
在 geometry.rs 中,Val 枚举新增了两个变体(见 L58-L67):
/// Set this value as a multiple of the node's own font size.
///
/// `1em` is the node's font size, so `Val::Em(2.0)` is twice that.
/// Resolved from the node's [`EmSize`] component.
Em(f32),
/// Set this value as a multiple of the root font size.
///
/// Unlike [`Val::Em`] this ignores the node's own font size, so it means the same
/// length anywhere in the hierarchy. Resolved from the [`RemSize`] resource.
Rem(f32),
Val::Em解析时读取节点自身的EmSize组件;Val::Rem解析时读取全局RemSize资源,因此在层级树任何位置含义都一致。
构造这两个变体的辅助函数 em() 和 rem(),与既有的 px()、percent()、vw()、vh() 并列。Val 还实现了 FromStr(L89-L136),现在也能把 "1.5rem"、"2em" 之类的字符串解析为对应变体,这在从样式表/序列化数据构造 Val 时可以直接受益。
字号相关的类型定义位于 bevy_text/src/text.rs:
pub const DEFAULT_REM_SIZE_PX: f32 = 20.0; // L868
pub struct RemSize(pub f32); // L875
pub struct EmSize(pub f32); // L898
DEFAULT_REM_SIZE_PX 为 20.0,正好与 0.19 的默认字号 px(20) 保持一致,这也是默认字号改为 rem(1) 后行为等价的原因。
迁移点一:resolve 系列方法新增两个参数
由于解析 Val 现在需要"节点字号"和"根字号"两个基准,以下方法的签名都追加了 em_size: EmSize 与 rem_size: RemSize 两个参数:
Val::resolveVal2::resolveUiPosition::resolveCornerRadius::resolveRadialGradientShape::resolveUiTransform::compute_affineBorderRadius::resolve
调用方式的变化:
// 0.19
let physical = val.resolve(scale_factor, physical_base_value, physical_target_size)?;
// 0.20
let physical = val.resolve(
scale_factor,
physical_base_value,
physical_target_size,
em_size,
rem_size,
)?;
在源码中可以确认 Val::resolve 的新签名(geometry.rs L483-L488)以及 Val2::resolve(L1190-L1195)都已带上 em_size: EmSize(其后跟随 rem_size)参数;UiTransform::compute_affine 与 RadialGradientShape::resolve 同样如此(见 ui_transform.rs L69、gradients.rs L615)。
迁移点二:从 ComputedNode 取 em_size / rem_size
如果你是在布局完成之后、针对一个已存在的节点来解析 Val(例如自定义 box_shadow 逻辑),不必自己去追踪字号——ComputedNode 新增了两个字段,记录了排版时实际使用的字号值:
em_sizerem_size
它们的默认值为 EmSize(DEFAULT_REM_SIZE_PX) 与 RemSize(DEFAULT_REM_SIZE_PX)(见 ui_node.rs L388-L389)。对已有节点做 Val 解析时,直接从该节点的 ComputedNode 上取这两个字段传入 resolve 即可。
迁移点三:Node 现在要求 EmSize 组件
这是 0.20 中最需要理解的一点:Node 现在要求实体上存在 EmSize(定义在 bevy_text,并在 bevy_ui::prelude 中重导出),它作为该节点字号,供 Val::Em 解析使用。其生命周期规则如下:
- 节点带有
TextFont时:EmSize由TextFont派生;当TextFont、RemSize或渲染目标信息发生变化时会被重新计算。你手动设置的值会保留到下一次重算为止。 - 节点没有
TextFont时:EmSize完全由你自己设置,框架不会去动它;默认值为DEFAULT_REM_SIZE_PX(20px)。它与默认RemSize数值一致,但不会跟随RemSize的变化。 EmSize的层级传播是应用层的责任,bevy_ui不负责。也就是说,如果你希望子节点继承父节点的字号,需要自己在 App 里做传播系统。release notes 也明确写着:"EmSizeis derived fromTextFontwhen one is on the same entity; propagating it down the hierarchy is left to your app."
迁移点四:GridTrack 的 em/rem 构造器
网格轨道也支持了这两种单位,新增了构造函数与对应的 sizing function 变体:
GridTrack::em、GridTrack::remRepeatedGridTrack::em、RepeatedGridTrack::rem
它们底层使用新增的 MinTrackSizingFunction::Em、MinTrackSizingFunction::Rem、MaxTrackSizingFunction::Em、MaxTrackSizingFunction::Rem 变体。这两个变体同样出现在 ui_node.rs L1856-L1891 的 min/max sizing function 枚举中,用于按字号倍数定义网格轨道的上下限尺寸。
迁移点五:FontSize::eval 改为接收 RemSize
FontSize::eval 的参数类型从裸 f32 变为 RemSize 结构体:
// 0.19
let size = font_size.eval(logical_viewport_size, rem_size_px);
// 0.20
let size = font_size.eval(logical_viewport_size, RemSize(rem_size_px));
这是一个类型层面的小改动:把根字号包装进 RemSize 后,字号求值的接口与布局解析接口的单位来源保持了一致的类型表达。
迁移检查清单
升级 0.19 → 0.20 时,可以按以下顺序排查编译错误:
- 找到所有
Val::resolve/Val2::resolve等调用,补上em_size与rem_size两个参数;针对已排版节点的解析,优先从ComputedNode的em_size/rem_size字段取值; - 为携带
Node的实体补上EmSize组件——有TextFont的节点会被框架自动派生/重算,没有TextFont的节点(如纯容器)需要自行设置默认值; - 检查 App 中是否需要新增
EmSize向下传播的系统(官方明确这是应用的责任); - 更新
FontSize::eval调用,把裸f32包成RemSize(f32); - 网格轨道需要字号单位时,改用
GridTrack::em/GridTrack::rem等构造函数; - 若有依赖"默认字号恒为 20px"的逻辑,注意默认字号现在是
rem(1)——只有在修改RemSize后才会与旧的硬编码 20px 产生差异。
小结
0.20 的 em/rem 支持让 Bevy UI 的尺寸系统从"像素与视口比例"扩展到了"字号比例":Val::Em 随节点自身 EmSize 缩放,Val::Rem 随全局 RemSize 缩放,且 em/rem 辅助函数、字符串解析、GridTrack 构造器与 bsn! 语法均已就位。迁移成本集中在 resolve 系列方法的签名变化与 EmSize 组件的引入上;只要理解"EmSize 由 TextFont 派生但层级传播交给应用"这一规则,升级路径就非常直接。相关源码入口:geometry.rs、ui_node.rs、text.rs、ui_transform.rs、gradients.rs。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00