Skip to content

Commit f2bbf8f

Browse files
authored
RunGeometry (#21656)
# Objective Follow up to the underline and strikethrough PRs: * Replace the quintuples stored in `TextLayoutInfo::section_geometry` with a struct with named fields. * Rename `TextLayoutInfo::section_geometry` because "section" in our terminology implies a one-to-one correspondence with text entities. * Add some basic helpers to construct the underline and strikethrough lines. * Seperate the thickness values for underline and strikethrough. This is needed for an API that allows users to set custom thicknesses. ## Solution * New struct `RunGeometry`. `RunGeometry` holds the bounds and decoration geometry for each text run (a contiguous sequence of glyphs on a line that share text attributes). It has helper methods for placing underline and strikethrough. * Rename the `section_geometry` field to `run_geometry` and make it a `Vec<RunGeometry>` * `RunGeometry` has seperate `underline_thickness` and `strikethrough_thickness` values.
1 parent 8d1bf6e commit f2bbf8f

File tree

4 files changed

+110
-71
lines changed

4 files changed

+110
-71
lines changed

crates/bevy_sprite_render/src/text2d/mod.rs

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,13 @@ pub fn extract_text2d_sprite(
7979

8080
let top_left = (Anchor::TOP_LEFT.0 - anchor.as_vec()) * size;
8181

82-
for &(section_index, rect, _, _, _) in text_layout_info.section_geometry.iter() {
83-
let section_entity = computed_block.entities()[section_index].entity;
82+
for run in text_layout_info.run_geometry.iter() {
83+
let section_entity = computed_block.entities()[run.span_index].entity;
8484
let Ok(text_background_color) = text_background_colors_query.get(section_entity) else {
8585
continue;
8686
};
8787
let render_entity = commands.spawn(TemporaryRenderEntity).id();
88-
let offset = Vec2::new(rect.center().x, -rect.center().y);
88+
let offset = Vec2::new(run.bounds.center().x, -run.bounds.center().y);
8989
let transform = *global_transform
9090
* GlobalTransform::from_translation(top_left.extend(0.))
9191
* scaling
@@ -102,7 +102,7 @@ pub fn extract_text2d_sprite(
102102
anchor: Vec2::ZERO,
103103
rect: None,
104104
scaling_mode: None,
105-
custom_size: Some(rect.size()),
105+
custom_size: Some(run.bounds.size()),
106106
},
107107
});
108108
}
@@ -157,10 +157,8 @@ pub fn extract_text2d_sprite(
157157
end += 1;
158158
}
159159

160-
for &(section_index, rect, strikethrough_y, stroke, underline_y) in
161-
text_layout_info.section_geometry.iter()
162-
{
163-
let section_entity = computed_block.entities()[section_index].entity;
160+
for run in text_layout_info.run_geometry.iter() {
161+
let section_entity = computed_block.entities()[run.span_index].entity;
164162
let Ok((_, has_strikethrough, has_underline, _, _)) =
165163
decoration_query.get(section_entity)
166164
else {
@@ -169,7 +167,7 @@ pub fn extract_text2d_sprite(
169167

170168
if has_strikethrough {
171169
let render_entity = commands.spawn(TemporaryRenderEntity).id();
172-
let offset = Vec2::new(rect.center().x, -strikethrough_y - 0.5 * stroke);
170+
let offset = run.strikethrough_position() * Vec2::new(1., -1.);
173171
let transform =
174172
shadow_transform * GlobalTransform::from_translation(offset.extend(0.));
175173
extracted_sprites.sprites.push(ExtractedSprite {
@@ -184,14 +182,14 @@ pub fn extract_text2d_sprite(
184182
anchor: Vec2::ZERO,
185183
rect: None,
186184
scaling_mode: None,
187-
custom_size: Some(Vec2::new(rect.size().x, stroke)),
185+
custom_size: Some(run.strikethrough_size()),
188186
},
189187
});
190188
}
191189

192190
if has_underline {
193191
let render_entity = commands.spawn(TemporaryRenderEntity).id();
194-
let offset = Vec2::new(rect.center().x, -underline_y - 0.5 * stroke);
192+
let offset = run.strikethrough_position() * Vec2::new(1., -1.);
195193
let transform =
196194
shadow_transform * GlobalTransform::from_translation(offset.extend(0.));
197195
extracted_sprites.sprites.push(ExtractedSprite {
@@ -206,7 +204,7 @@ pub fn extract_text2d_sprite(
206204
anchor: Vec2::ZERO,
207205
rect: None,
208206
scaling_mode: None,
209-
custom_size: Some(Vec2::new(rect.size().x, stroke)),
207+
custom_size: Some(run.strikethrough_size()),
210208
},
211209
});
212210
}
@@ -274,10 +272,8 @@ pub fn extract_text2d_sprite(
274272
end += 1;
275273
}
276274

277-
for &(section_index, rect, strikethrough_y, stroke, underline_y) in
278-
text_layout_info.section_geometry.iter()
279-
{
280-
let section_entity = computed_block.entities()[section_index].entity;
275+
for run in text_layout_info.run_geometry.iter() {
276+
let section_entity = computed_block.entities()[run.span_index].entity;
281277
let Ok((
282278
text_color,
283279
has_strike_through,
@@ -294,7 +290,7 @@ pub fn extract_text2d_sprite(
294290
.unwrap_or(text_color.0)
295291
.to_linear();
296292
let render_entity = commands.spawn(TemporaryRenderEntity).id();
297-
let offset = Vec2::new(rect.center().x, -strikethrough_y - 0.5 * stroke);
293+
let offset = run.strikethrough_position() * Vec2::new(1., -1.);
298294
let transform = *global_transform
299295
* GlobalTransform::from_translation(top_left.extend(0.))
300296
* scaling
@@ -311,7 +307,7 @@ pub fn extract_text2d_sprite(
311307
anchor: Vec2::ZERO,
312308
rect: None,
313309
scaling_mode: None,
314-
custom_size: Some(Vec2::new(rect.size().x, stroke)),
310+
custom_size: Some(run.strikethrough_size()),
315311
},
316312
});
317313
}
@@ -322,7 +318,7 @@ pub fn extract_text2d_sprite(
322318
.unwrap_or(text_color.0)
323319
.to_linear();
324320
let render_entity = commands.spawn(TemporaryRenderEntity).id();
325-
let offset = Vec2::new(rect.center().x, -underline_y - 0.5 * stroke);
321+
let offset = run.underline_position() * Vec2::new(1., -1.);
326322
let transform = *global_transform
327323
* GlobalTransform::from_translation(top_left.extend(0.))
328324
* scaling
@@ -339,7 +335,7 @@ pub fn extract_text2d_sprite(
339335
anchor: Vec2::ZERO,
340336
rect: None,
341337
scaling_mode: None,
342-
custom_size: Some(Vec2::new(rect.size().x, stroke)),
338+
custom_size: Some(run.underline_size()),
343339
},
344340
});
345341
}

crates/bevy_text/src/pipeline.rs

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ impl TextPipeline {
269269
swash_cache: &mut SwashCache,
270270
) -> Result<(), TextError> {
271271
layout_info.glyphs.clear();
272-
layout_info.section_geometry.clear();
272+
layout_info.run_geometry.clear();
273273
layout_info.size = Default::default();
274274

275275
// Clear this here at the focal point of text rendering to ensure the field's lifecycle has strong boundaries.
@@ -336,18 +336,20 @@ impl TextPipeline {
336336
match current_section {
337337
Some(section) => {
338338
if section != layout_glyph.metadata {
339-
layout_info.section_geometry.push((
340-
section,
341-
Rect::new(
339+
layout_info.run_geometry.push(RunGeometry {
340+
span_index: section,
341+
bounds: Rect::new(
342342
start,
343343
run.line_top,
344344
end,
345345
run.line_top + run.line_height,
346346
),
347-
(run.line_y - self.glyph_info[section].3).round(),
348-
self.glyph_info[section].4,
349-
(run.line_y - self.glyph_info[section].5).round(),
350-
));
347+
strikethrough_y: (run.line_y - self.glyph_info[section].3)
348+
.round(),
349+
strikethrough_thickness: self.glyph_info[section].4,
350+
underline_y: (run.line_y - self.glyph_info[section].5).round(),
351+
underline_thickness: self.glyph_info[section].4,
352+
});
351353
start = end.max(layout_glyph.x);
352354
current_section = Some(layout_glyph.metadata);
353355
}
@@ -432,13 +434,14 @@ impl TextPipeline {
432434
Ok(())
433435
});
434436
if let Some(section) = current_section {
435-
layout_info.section_geometry.push((
436-
section,
437-
Rect::new(start, run.line_top, end, run.line_top + run.line_height),
438-
(run.line_y - self.glyph_info[section].3).round(),
439-
self.glyph_info[section].4,
440-
(run.line_y - self.glyph_info[section].5).round(),
441-
));
437+
layout_info.run_geometry.push(RunGeometry {
438+
span_index: section,
439+
bounds: Rect::new(start, run.line_top, end, run.line_top + run.line_height),
440+
strikethrough_y: (run.line_y - self.glyph_info[section].3).round(),
441+
strikethrough_thickness: self.glyph_info[section].4,
442+
underline_y: (run.line_y - self.glyph_info[section].5).round(),
443+
underline_thickness: self.glyph_info[section].4,
444+
});
442445
}
443446

444447
result
@@ -518,13 +521,63 @@ pub struct TextLayoutInfo {
518521
pub scale_factor: f32,
519522
/// Scaled and positioned glyphs in screenspace
520523
pub glyphs: Vec<PositionedGlyph>,
521-
/// Geometry of each text segment: (section index, bounding rect, strikethrough offset, stroke thickness, underline offset)
522-
/// A text section spanning more than one line will have multiple segments.
523-
pub section_geometry: Vec<(usize, Rect, f32, f32, f32)>,
524+
/// Geometry of each text run used to render text decorations like background colors, strikethrough, and underline.
525+
/// A run in `bevy_text` is a contiguous sequence of glyphs on a line that share the same text attributes like font,
526+
/// font size, and line height. A text entity that extends over multiple lines will have multiple corresponding runs.
527+
///
528+
/// The coordinates are unscaled and relative to the top left corner of the text layout.
529+
pub run_geometry: Vec<RunGeometry>,
524530
/// The glyphs resulting size
525531
pub size: Vec2,
526532
}
527533

534+
/// Geometry of a text run used to render text decorations like background colors, strikethrough, and underline.
535+
/// A run in `bevy_text` is a contiguous sequence of glyphs on a line that share the same text attributes like font,
536+
/// font size, and line height.
537+
#[derive(Default, Debug, Clone, Reflect)]
538+
pub struct RunGeometry {
539+
/// The index of the text entity in [`ComputedTextBlock`] that this run belongs to.
540+
pub span_index: usize,
541+
/// Bounding box around the text run
542+
pub bounds: Rect,
543+
/// Y position of the strikethrough in the text layout.
544+
pub strikethrough_y: f32,
545+
/// Strikethrough stroke thickness.
546+
pub strikethrough_thickness: f32,
547+
/// Y position of the underline in the text layout.
548+
pub underline_y: f32,
549+
/// Underline stroke thickness.
550+
pub underline_thickness: f32,
551+
}
552+
553+
impl RunGeometry {
554+
/// Returns the center of the strikethrough in the text layout.
555+
pub fn strikethrough_position(&self) -> Vec2 {
556+
Vec2::new(
557+
self.bounds.center().x,
558+
self.strikethrough_y + 0.5 * self.strikethrough_thickness,
559+
)
560+
}
561+
562+
/// Returns the size of the strikethrough.
563+
pub fn strikethrough_size(&self) -> Vec2 {
564+
Vec2::new(self.bounds.size().x, self.strikethrough_thickness)
565+
}
566+
567+
/// Get the center of the underline in the text layout.
568+
pub fn underline_position(&self) -> Vec2 {
569+
Vec2::new(
570+
self.bounds.center().x,
571+
self.underline_y + 0.5 * self.underline_thickness,
572+
)
573+
}
574+
575+
/// Returns the size of the underline.
576+
pub fn underline_size(&self) -> Vec2 {
577+
Vec2::new(self.bounds.size().x, self.underline_thickness)
578+
}
579+
}
580+
528581
/// Size information for a corresponding [`ComputedTextBlock`] component.
529582
///
530583
/// Generated via [`TextPipeline::create_text_measure`].

crates/bevy_ui_render/src/lib.rs

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,10 +1094,8 @@ pub fn extract_text_shadows(
10941094
end += 1;
10951095
}
10961096

1097-
for &(section_index, rect, strikethrough_y, stroke, underline_y) in
1098-
text_layout_info.section_geometry.iter()
1099-
{
1100-
let section_entity = computed_block.entities()[section_index].entity;
1097+
for run in text_layout_info.run_geometry.iter() {
1098+
let section_entity = computed_block.entities()[run.span_index].entity;
11011099
let Ok((has_strikethrough, has_underline)) = text_decoration_query.get(section_entity)
11021100
else {
11031101
continue;
@@ -1111,15 +1109,12 @@ pub fn extract_text_shadows(
11111109
image: AssetId::default(),
11121110
extracted_camera_entity,
11131111
transform: node_transform
1114-
* Affine2::from_translation(Vec2::new(
1115-
rect.center().x,
1116-
strikethrough_y + 0.5 * stroke,
1117-
)),
1112+
* Affine2::from_translation(run.strikethrough_position()),
11181113
item: ExtractedUiItem::Node {
11191114
color: shadow.color.into(),
11201115
rect: Rect {
11211116
min: Vec2::ZERO,
1122-
max: Vec2::new(rect.size().x, stroke),
1117+
max: run.strikethrough_size(),
11231118
},
11241119
atlas_scaling: None,
11251120
flip_x: false,
@@ -1139,16 +1134,12 @@ pub fn extract_text_shadows(
11391134
clip: clip.map(|clip| clip.clip),
11401135
image: AssetId::default(),
11411136
extracted_camera_entity,
1142-
transform: node_transform
1143-
* Affine2::from_translation(Vec2::new(
1144-
rect.center().x,
1145-
underline_y + 0.5 * stroke,
1146-
)),
1137+
transform: node_transform * Affine2::from_translation(run.underline_position()),
11471138
item: ExtractedUiItem::Node {
11481139
color: shadow.color.into(),
11491140
rect: Rect {
11501141
min: Vec2::ZERO,
1151-
max: Vec2::new(rect.size().x, stroke),
1142+
max: run.underline_size(),
11521143
},
11531144
atlas_scaling: None,
11541145
flip_x: false,
@@ -1213,10 +1204,8 @@ pub fn extract_text_decorations(
12131204
let transform =
12141205
Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size());
12151206

1216-
for &(section_index, rect, strikethrough_y, stroke, underline_y) in
1217-
text_layout_info.section_geometry.iter()
1218-
{
1219-
let section_entity = computed_block.entities()[section_index].entity;
1207+
for run in text_layout_info.run_geometry.iter() {
1208+
let section_entity = computed_block.entities()[run.span_index].entity;
12201209
let Ok((
12211210
(text_background_color, maybe_strikethrough, maybe_underline),
12221211
text_color,
@@ -1234,12 +1223,12 @@ pub fn extract_text_decorations(
12341223
clip: clip.map(|clip| clip.clip),
12351224
image: AssetId::default(),
12361225
extracted_camera_entity,
1237-
transform: transform * Affine2::from_translation(rect.center()),
1226+
transform: transform * Affine2::from_translation(run.bounds.center()),
12381227
item: ExtractedUiItem::Node {
12391228
color: text_background_color.0.to_linear(),
12401229
rect: Rect {
12411230
min: Vec2::ZERO,
1242-
max: rect.size(),
1231+
max: run.bounds.size(),
12431232
},
12441233
atlas_scaling: None,
12451234
flip_x: false,
@@ -1264,16 +1253,12 @@ pub fn extract_text_decorations(
12641253
clip: clip.map(|clip| clip.clip),
12651254
image: AssetId::default(),
12661255
extracted_camera_entity,
1267-
transform: transform
1268-
* Affine2::from_translation(Vec2::new(
1269-
rect.center().x,
1270-
strikethrough_y + 0.5 * stroke,
1271-
)),
1256+
transform: transform * Affine2::from_translation(run.strikethrough_position()),
12721257
item: ExtractedUiItem::Node {
12731258
color,
12741259
rect: Rect {
12751260
min: Vec2::ZERO,
1276-
max: Vec2::new(rect.size().x, stroke),
1261+
max: run.strikethrough_size(),
12771262
},
12781263
atlas_scaling: None,
12791264
flip_x: false,
@@ -1298,16 +1283,12 @@ pub fn extract_text_decorations(
12981283
clip: clip.map(|clip| clip.clip),
12991284
image: AssetId::default(),
13001285
extracted_camera_entity,
1301-
transform: transform
1302-
* Affine2::from_translation(Vec2::new(
1303-
rect.center().x,
1304-
underline_y + 0.5 * stroke,
1305-
)),
1286+
transform: transform * Affine2::from_translation(run.underline_position()),
13061287
item: ExtractedUiItem::Node {
13071288
color,
13081289
rect: Rect {
13091290
min: Vec2::ZERO,
1310-
max: Vec2::new(rect.size().x, stroke),
1291+
max: run.underline_size(),
13111292
},
13121293
atlas_scaling: None,
13131294
flip_x: false,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
title: "`TextLayoutInfo`'s `section_rects` field has been replaced with `run_geometry`"
3+
pull_requests: []
4+
---
5+
6+
`TextLayoutInfo`'s `section_rects` field has been removed.
7+
In its place is a new field `run_geometry` that contains the non-glyph layout geometry for a run of glyphs: the run's span index, bounding rectangle, underline position and thickness, and strikethrough position and thickness. A run in `bevy_text` is a contiguous sequence of glyphs on the same line that share the same text attributes like font, font size, and line height. The coordinates stored in `run_geometry` are unscaled and relative to the top left corner of the text layout.
8+
9+
Unlike the tuples of `section_rects`, `RunGeometry` does not include an `Entity` id. To find the corresponding text entity, call the `entities` method on the root text entity’s `ComputedTextBlock` component and use the `span_index` to index into the returned slice.

0 commit comments

Comments
 (0)