首页
/ Bevy 0.20 迁移指南:TextFont 默认字号改为 FontSize::Rem(1.),让 RemSize 真正成为根字号

Bevy 0.20 迁移指南:TextFont 默认字号改为 FontSize::Rem(1.),让 RemSize 真正成为根字号

2026-09-05 10:16:24作者:舒璇辛Bertina

本文基于 Bevy 仓库中的官方迁移文档 default_font_size_is_rem.md 展开,讲清 0.20 版本中 TextFont::default() 默认字号从 FontSize::Px(20.) 变为 FontSize::Rem(1.) 的动机与行为差异,并结合 bevy_textbevy_ui 的源码说明 RemSize 资源如何参与字号求值与重排,最后给出"保持固定字号"与"跟随根字号缩放"两种迁移写法。读完后你能判断自己的文本代码在 0.20 下是否需要改动,以及改动点应该写在哪里。

一、变更内容:默认字号从固定 20px 变为 1rem

迁移文档给出的核心变更只有一句话:

TextFont::default() now uses FontSize::Rem(1.) instead of FontSize::Px(20.), so that the RemSize resource actually sets the default font size.

也就是说:

  • 旧行为(0.19 及以前):没有显式设置字号的文本,字号被硬编码为 20 逻辑像素(FontSize::Px(20.)),与 RemSize 资源无关;
  • 新行为(0.20):默认字号改为 FontSize::Rem(1.),即"1 个根字号单位",由 RemSize 资源决定。由于 RemSize 的默认值恰好是 20 逻辑像素,所以在默认配置下渲染结果完全一致,但从此以后,凡是保持默认字号的文本,都会随 RemSize 的修改而整体缩放。

文档给出的迁移建议是:如果你希望文本大小固定不变,就显式设置像素字号:

// 0.20
TextFont {
    font_size: FontSize::Px(20.),
    ..default()
}

这个改动是 Bevy 文本子系统 rem/em 单位支持的一部分,与同 PR(#25231)中的另一篇迁移文档 val_em_and_rem.md 描述的 Val::Em / Val::Rem 变体、EmSize 组件、FontSize::eval 签名变更共同构成了 0.20 的"根字号"体系。

二、源码印证:TextFont::default() 现在返回 Rem(1.)

crates/bevy_text/src/text.rs 中,TextFontDefault 实现已经落实了这一变更:

impl Default for TextFont {
    fn default() -> Self {
        Self {
            font: Default::default(),
            font_size: FontSize::Rem(1.),
            style: FontStyle::Normal,
            weight: FontWeight::NORMAL,
            width: FontWidth::NORMAL,
            font_features: FontFeatures::default(),
            font_variations: FontVariations::default(),
            font_smoothing: Default::default(),
        }
    }
}

需要注意一个容易混淆的细节:TextFont 的默认字号变了,但 FontSize 枚举自身的 Default 实现仍然是 Px(20.),见 crates/bevy_text/src/text.rs

impl Default for FontSize {
    fn default() -> Self {
        Self::Px(20.)
    }
}

因此,直接调用 TextFont::default()(绝大多数 UI/2D 文本路径都走这里)得到的是 Rem(1.);而单独使用 FontSize::default()..Default::default() 展开 font_size 字段的地方得到的仍是 Px(20.)。二者不一致正是这类迁移的常见"坑点",升级时建议全局搜索 FontSize::default()TextFont::default() 确认行为是否符合预期。

三、RemSize:根字号资源的定义、注册与求值

3.1 资源定义与默认值

RemSize 定义在 crates/bevy_text/src/text.rs

/// Default [`RemSize`] and [`EmSize`], in logical pixels
pub const DEFAULT_REM_SIZE_PX: f32 = 20.0;

/// The root font size, in logical pixels.
///
/// `Rem` units always resolve against this one global resource, including
/// `FontSize::Rem`, `LetterSpacing::Rem` and `Val::Rem`.
#[derive(Resource, Copy, Clone, Debug, PartialEq, Deref, DerefMut, Reflect)]
pub struct RemSize(pub f32);

impl Default for RemSize {
    fn default() -> Self {
        Self(DEFAULT_REM_SIZE_PX)
    }
}

要点:

  • RemSize(pub f32) 是单一全局 Resource,单位为逻辑像素;默认值 20.0 对应常量 DEFAULT_REM_SIZE_PX
  • 文档注释明确了它的作用域:FontSize::RemLetterSpacing::RemVal::Rem 三种 rem 单位都只解析到这一个全局资源
  • 该资源由 TextPlugin 在插件构建时注册(init_resource::<RemSize>()),见 crates/bevy_text/src/lib.rs。也就是说,只要你的应用启用了文本功能,RemSize 就可用,无需手动注册。

3.2 FontSize::eval:rem 如何变成像素

FontSize 是文本字号的枚举,支持 PxVwVhVMinVMaxRem 六种单位,定义见 crates/bevy_text/src/text.rs。求值入口是 eval 方法(crates/bevy_text/src/text.rs):

impl FontSize {
    /// Evaluate the font size to a value in logical pixels
    pub fn eval(
        self,
        // Viewport size in logical pixels
        logical_viewport_size: Vec2,
        // Base Rem size in logical pixels
        rem_size: RemSize,
    ) -> f32 {
        match self {
            FontSize::Px(s) => s,
            FontSize::Vw(s) => logical_viewport_size.x * s / 100.,
            FontSize::Vh(s) => logical_viewport_size.y * s / 100.,
            FontSize::VMin(s) => logical_viewport_size.min_element() * s / 100.,
            FontSize::VMax(s) => logical_viewport_size.max_element() * s / 100.,
            FontSize::Rem(s) => rem_size.0 * s,
        }
    }
}

FontSize::Rem(s) 的求值就是 rem_size.0 * sRem(1.) 在当前默认配置下等于 20.0 * 1.0 = 20 逻辑像素,与旧版的 Px(20.) 数值相同——这就是迁移文档所说"renders identically"(渲染结果一致)的数学依据。

另一个值得注意的签名变化:0.20 中 eval 的第二个参数从裸 f32 改成了 RemSize 类型。这一点在 val_em_and_rem.md 中有专门记录:

// 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));

如果你自己封装了字号计算逻辑,这是需要跟着改的调用点。

3.3 渲染管线中的重排机制

默认字号变成 Rem 之后,"修改 RemSize 能全局缩放文本"这一行为是如何生效的?从源码结构看,答案在文本管线与重排标记里:

  1. TextPipeline::update_buffer 在遍历每个文本 span 时,如果发现字号是 rem 系单位,就把该文本块的 uses_rem_sizes 标记置为 true,见 crates/bevy_text/src/pipeline.rs
match text_font.font_size {
    crate::FontSize::Vw(_)
    | crate::FontSize::Vh(_)
    | crate::FontSize::VMin(_)
    | crate::FontSize::VMax(_) => computed.uses_viewport_sizes = true,
    crate::FontSize::Rem(_) => computed.uses_rem_sizes = true,
    _ => (),
}

随后字号通过 text_font.font_size.eval(logical_viewport_size, base_rem_size) 求值(crates/bevy_text/src/pipeline.rs)。

  1. ComputedTextBlock 持有 uses_rem_sizes 标志(crates/bevy_text/src/text.rs),其 needs_rerender 方法会在 RemSize 发生变化且该文本块使用了 rem 单位时返回 true,从而触发重新排版(crates/bevy_text/src/text.rs):
pub fn needs_rerender(
    &self,
    is_viewport_size_changed: bool,
    is_rem_size_changed: bool,
) -> bool {
    self.needs_rerender
        || (is_viewport_size_changed && self.uses_viewport_sizes)
        || (is_rem_size_changed && self.uses_rem_sizes)
}

这个设计的实际效果是:只有使用了 rem(或视口)单位的文本才会因 RemSize 变化而重排,纯 Px 文本不受影响,避免不必要的全量重排开销。这也再次说明:0.20 之后"默认字号"的文本天然进入了 rem 依赖集合,而显式写成 Px 的文本则被排除在外——这正是迁移文档推荐"固定尺寸请显式写 Px"的底层原因。

四、对 UI 布局与文本输入的影响

RemSize 不只影响字号本身,它已经贯穿 UI 布局链路:

4.1 UI 节点的 font_size 转换

crates/bevy_ui/src/geometry.rs 中,ValFontSize 的转换显示:未显式设置字号的 UI 节点(Val::Auto)会被转成 FontSize::Rem(1.)Val::Em(em) 也会被归一化为 FontSize::Rem(em)(em 相对于本节点字号解析,先折算成 rem 表达):

Val::Auto => FontSize::Rem(1.),
Val::Px(px) => FontSize::Px(px),
Val::Percent(percent) => FontSize::Rem(percent / 100.),
Val::Rem(rem) => FontSize::Rem(rem),
Val::Em(em) => FontSize::Rem(em), // see comment above

也就是说,没有手动设置字号的 UI 文本,在 0.20 中会统一走 rem 路径,这是本次迁移影响面最大的场景。

4.2 文本输入组件的重排条件

bevy_ui 的文本输入布局系统同样感知了这次变化。crates/bevy_ui/src/widget/text_input_layout.rs 中的重排条件明确加入了"字号是 rem 且 RemSize 已变化"的分支:

|| matches!(text_font.font_size, FontSize::Rem(_)) && rem_size.is_changed()

并在光标宽度等计算中直接用 text_font.font_size.eval(target.logical_size(), *rem_size) 求值(同文件 L515)。所以基于 TextInput 的组件在 RemSize 变化时也会正确跟随缩放。

4.3 同 PR 的其他连带变更

由于 rem 求值需要"根字号 + 节点字号"两个输入,同一 PR 还带来了 val_em_and_rem.md 记录的 API 变更,做 0.20 升级时应一并检查:

  • ValVal2UiPositionCornerRadiusRadialGradientShapeUiTransform::compute_affineBorderRadiusresolve/compute_affine 方法新增 em_size: EmSizerem_size: RemSize 两个参数;
  • ComputedNode 新增 em_sizerem_size 字段,用于记录布局时实际采用的字号,方便下游(如 box shadow)复用;
  • Node 现在要求 EmSize 组件(从 bevy_text 导出、经 bevy_ui::prelude 可用),节点有 TextFont 时会在 TextFontRemSize 或渲染目标信息变化时自动重算 EmSize;没有 TextFont 时该值由应用自行维护,默认值为 DEFAULT_REM_SIZE_PX(20 逻辑像素),且不跟随 RemSize 的后续变化——需要跟随根字号的布局值应使用 Val::Rem

五、迁移实操:如何写代码

5.1 场景一:保持固定字号(推荐大多数游戏 UI)

如果你的文本尺寸是美术规格定的固定像素,按迁移文档显式写死即可:

use bevy::prelude::*;

fn setup(mut commands: Commands) {
    commands.spawn(Text2d::new("Hello Bevy 0.20")).insert(TextFont {
        font_size: FontSize::Px(20.),
        ..default()
    });
}

也可以走链式构造器 TextFont::from_font_size / with_font_size(见 crates/bevy_text/src/text.rs),例如 TextFont::from_font_size(20_f32) 会构造出 Px 语义的字号。仓库示例中的既有写法本身就是这种风格,如 examples/2d/text2d.rs 里的 font_size: FontSize::Px(50.0)examples/2d/texture_atlas.rs 中的 FontSize::Px(42.0),这些文本在 0.20 中行为不变。

5.2 场景二:让文本跟随根字号缩放

如果你希望做"全局字号调节"(例如设置面板里的 A/A+ 选项、高分辨率适配),现在只需修改 RemSize,所有保持默认(或显式 Rem)的文本会自动重排:

fn change_root_font_size(mut rem_size: ResMut<RemSize>) {
    rem_size.0 = 32.0; // 所有 Rem(1.) 的默认文本变为 32 逻辑像素
}

生效链路是:RemSize 变化 → 使用 rem 字号的文本块 needs_rerender 为 true(crates/bevy_text/src/text.rs)→ 文本管线按 rem_size.0 * s 重新求值并排版(crates/bevy_text/src/pipeline.rs)。

注意一个行为差异:在旧版本中,同样的操作对"默认文本"完全无效(因为默认就是 Px(20.));升级后同一份代码里"没写字号"的文本会开始响应 RemSize。如果你在旧代码里依赖过"默认文本不会缩放"这一事实,需要在 0.20 中显式补上 FontSize::Px

5.3 相关示例参考

  • examples/2d/text2d.rs:2D 文本示例,演示显式 FontSize::Px 的常规写法;
  • examples/ui/text/letter_spacing.rs:展示了 RemSizeLetterSpacing::Rem/LetterSpacing::Px 之间的换算方式(LetterSpacing::Rem(v / rem_size.0)),是理解"一切 rem 单位都以 RemSize 为基准"的良好参考。

六、小结

项目 0.19 及以前 0.20
TextFont::default() 的字号 FontSize::Px(20.) FontSize::Rem(1.)
FontSize::default() FontSize::Px(20.) 仍为 FontSize::Px(20.)
默认 RemSize —(对默认字号无作用) 20.0 逻辑像素,且真正决定默认字号
修改 RemSize 的影响 不影响默认字号文本 所有默认/Rem 字号文本重排缩放
保持固定尺寸的写法 不写即可 显式 TextFont { font_size: FontSize::Px(20.), ..default() }

本次变更本质上是把 Bevy 文本系统的"根字号"概念补齐:RemSize 从"仅供显式 Rem 单位使用的资源"升级为"所有默认文本字号的真正来源",并在渲染管线中通过 uses_rem_sizes 标记实现了按需重排。升级 0.20 时的检查清单很简单:确认默认字号文本是否需要固定(需要则显式写 Px);检查自定义代码中 FontSize::evalVal::resolve 等 API 调用是否补上了 RemSize/EmSize 参数;以及留意 val_em_and_rem.mdNodeEmSize 的新要求。

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

项目优选

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