富文本:一块文本,各管各的妆
剧本的排版规矩比字幕复杂:角色名烫金加粗,舞台提示小一号、灰着,台词正色。要是一段文字只能一副行头,就得拆成三个 Text2d 再手动对位——排版引擎白养了。
bevy_text 的答案是文本段(TextSpan):一块文本是一棵小树——根上挂 Text2d(兼任第一段),子实体挂 TextSpan,每段是一个独立实体、各配各的 TextFont 与 TextColor;排版按整块算(换行、对齐、地界都看根上的 TextLayout 与 TextBounds),妆容按段算。秋白改词那页手稿,正好五脏俱全:
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2d);
let regular = asset_server.load("fonts/book-sans-sc-regular.otf");
let bold = asset_server.load("fonts/book-sans-sc-bold.otf");
// 一块文本 = 根 Text2d + 一串 TextSpan 子实体。
// 排版按整块算,但每一段各管各的字体、字号、颜色、装饰
commands.spawn((
Text2d::new("阿燕"),
TextFont {
font: FontSource::Handle(bold.clone()),
font_size: FontSize::Px(36.0),
..default()
},
TextColor(Color::srgb(0.91, 0.72, 0.29)),
TextLayout::justify(Justify::Center),
// 阴影是整块字的事,加在根上
Text2dShadow::default(),
children![
(
TextSpan::new("(提灯,望江)\n"),
TextFont {
font: FontSource::Handle(regular.clone()),
font_size: FontSize::Px(22.0),
..default()
},
TextColor(Color::srgb(0.55, 0.57, 0.62)),
),
(
TextSpan::new("夜渡无人,"),
TextFont {
font: FontSource::Handle(regular.clone()),
font_size: FontSize::Px(36.0),
..default()
},
TextColor::WHITE,
),
(
// 改掉的旧词:划线作废,颜色压暗
TextSpan::new("孤舟"),
TextFont {
font: FontSource::Handle(regular.clone()),
font_size: FontSize::Px(36.0),
..default()
},
TextColor(Color::srgb(0.45, 0.45, 0.48)),
Strikethrough,
),
(
// 换上的新词:加粗、标红、底下画金线
TextSpan::new("秋水"),
TextFont {
font: FontSource::Handle(bold.clone()),
font_size: FontSize::Px(36.0),
..default()
},
TextColor(Color::srgb(0.86, 0.32, 0.28)),
Underline,
UnderlineColor(Color::srgb(0.91, 0.72, 0.29)),
),
(
TextSpan::new("自横。"),
TextFont {
font: FontSource::Handle(regular.clone()),
font_size: FontSize::Px(36.0),
..default()
},
TextColor::WHITE,
),
(
// 落款用默认字体(ASCII 够用),加一块底色当签条
TextSpan::new("\nACT II - draft 3"),
TextFont::from_font_size(18.0),
TextColor(Color::srgb(0.75, 0.86, 0.92)),
TextBackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.55)),
),
],
));
println!("秋白:‘孤舟’划了,换‘秋水’——水比船有戏。");
}Listing 16-9:根管开头,六个 TextSpan 子实体各管一段——字体、字号、颜色、装饰互不干涉(examples/listing-16-09.rs)
cargo run -p ch16-text --example listing-16-09秋白:‘孤舟’划了,换‘秋水’——水比船有戏。
Figure 16-10:一棵文本树的全套妆容——五种字色、三种字号、两副字模、删除线、下划线、底色与阴影
这棵树上每类零件的归属,是富文本不出怪相的关键:
- 逐段生效(挂在根或任意 span 上,只管自己那段):
TextFont、TextColor——每个 span 都要自己配。span 不继承根的字体:TextSpan的 required components 给它补的是默认的TextFont,也就是那副只认 ASCII 的内置字模。中文 span 忘了指字体,那一段就单独变豆腐——整块其他段都正常,就它不正常,现场相当迷惑;Underline/Strikethrough——下划线与删除线,标记组件,挂谁谁有线。线色默认随字色,想分开配就加UnderlineColor/StrikethroughColor(Listing 16-9 的“秋水”:红字配金线);TextBackgroundColor——这一段文字的底色块,签条、高亮都是它。
- 整块生效(只认根上的):
TextLayout、TextBounds、Anchor、Transform——块只有一个排版、一个地界、一枚钉子;Text2dShadow——整块字的影子,offset定方向(默认右下 4 像素)、color定色。它是Text2d专属(UI 文本另有一个TextShadow,第 28 章见)。
改词的“孤舟”划掉、“秋水”顶上,两段比肩而立——这种“保留修改痕迹”的排版,三个独立 Text2d 拼起来要对齐到天荒地老,spans 里只是两个相邻子实体。
span 不能独闯:
TextSpan只有作为Text2d(或 UIText)实体的后代才有意义——它自己没有TextLayout,单独 spawn 出去没有任何东西负责排它,引擎会在日志里点名警告。读写一棵树:运行时想改某一段,沿着
Children爬树取&mut TextSpan当然可以,但 bevy_text 备了专门的系统参数Text2dReader/Text2dWriter——按“块内第几段”直接存取:0 号是根,1 号起是 span 按树序排队。下一节的连击牌就用它。
行头到这里配齐了。出戏之前还剩一个本质问题:这些字到底活在哪个世界?