首页
/ Bevy UI 的 em/rem 字号单位:Val::Em、Val::Rem 迁移与实战指南(0.19 → 0.20)

Bevy UI 的 em/rem 字号单位:Val::Em、Val::Rem 迁移与实战指南(0.19 → 0.20)

2026-09-07 17:35:42作者:魏献源Searcher

Bevy UI 在 0.20 中引入了 emrem 两种相对字号单位,使 Val 的解析结果能够跟随字体大小缩放,这对"设计时定死像素、运行期调整字号"的可访问性与多设备适配场景非常有用。本文以官方迁移指南 val_em_and_rem.md 为主体,结合 release notes 与仓库源码,完整讲解新增的 Val::Em / Val::Rem 变体、受影响的 resolve 方法签名、EmSize 组件的生命周期,以及 GridTrackFontSize::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 还实现了 FromStrL89-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: EmSizerem_size: RemSize 两个参数:

  • Val::resolve
  • Val2::resolve
  • UiPosition::resolve
  • CornerRadius::resolve
  • RadialGradientShape::resolve
  • UiTransform::compute_affine
  • BorderRadius::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::resolveL1190-L1195)都已带上 em_size: EmSize(其后跟随 rem_size)参数;UiTransform::compute_affineRadialGradientShape::resolve 同样如此(见 ui_transform.rs L69gradients.rs L615)。

迁移点二:从 ComputedNode 取 em_size / rem_size

如果你是在布局完成之后、针对一个已存在的节点来解析 Val(例如自定义 box_shadow 逻辑),不必自己去追踪字号——ComputedNode 新增了两个字段,记录了排版时实际使用的字号值:

  • em_size
  • rem_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 解析使用。其生命周期规则如下:

  1. 节点带有 TextFontEmSizeTextFont 派生;当 TextFontRemSize 或渲染目标信息发生变化时会被重新计算。你手动设置的值会保留到下一次重算为止。
  2. 节点没有 TextFontEmSize 完全由你自己设置,框架不会去动它;默认值为 DEFAULT_REM_SIZE_PX(20px)。它与默认 RemSize 数值一致,但不会跟随 RemSize 的变化
  3. EmSize 的层级传播是应用层的责任,bevy_ui 不负责。也就是说,如果你希望子节点继承父节点的字号,需要自己在 App 里做传播系统。release notes 也明确写着:"EmSize is derived from TextFont when one is on the same entity; propagating it down the hierarchy is left to your app."

迁移点四:GridTrack 的 em/rem 构造器

网格轨道也支持了这两种单位,新增了构造函数与对应的 sizing function 变体:

  • GridTrack::emGridTrack::rem
  • RepeatedGridTrack::emRepeatedGridTrack::rem

它们底层使用新增的 MinTrackSizingFunction::EmMinTrackSizingFunction::RemMaxTrackSizingFunction::EmMaxTrackSizingFunction::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 时,可以按以下顺序排查编译错误:

  1. 找到所有 Val::resolve / Val2::resolve 等调用,补上 em_sizerem_size 两个参数;针对已排版节点的解析,优先从 ComputedNodeem_size / rem_size 字段取值;
  2. 为携带 Node 的实体补上 EmSize 组件——有 TextFont 的节点会被框架自动派生/重算,没有 TextFont 的节点(如纯容器)需要自行设置默认值;
  3. 检查 App 中是否需要新增 EmSize 向下传播的系统(官方明确这是应用的责任);
  4. 更新 FontSize::eval 调用,把裸 f32 包成 RemSize(f32)
  5. 网格轨道需要字号单位时,改用 GridTrack::em / GridTrack::rem 等构造函数;
  6. 若有依赖"默认字号恒为 20px"的逻辑,注意默认字号现在是 rem(1)——只有在修改 RemSize 后才会与旧的硬编码 20px 产生差异。

小结

0.20 的 em/rem 支持让 Bevy UI 的尺寸系统从"像素与视口比例"扩展到了"字号比例":Val::Em 随节点自身 EmSize 缩放,Val::Rem 随全局 RemSize 缩放,且 em/rem 辅助函数、字符串解析、GridTrack 构造器与 bsn! 语法均已就位。迁移成本集中在 resolve 系列方法的签名变化与 EmSize 组件的引入上;只要理解"EmSizeTextFont 派生但层级传播交给应用"这一规则,升级路径就非常直接。相关源码入口:geometry.rsui_node.rstext.rsui_transform.rsgradients.rs

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391