Skip to content
36 changes: 16 additions & 20 deletions crates/bevy_sprite_render/src/text2d/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ pub fn extract_text2d_sprite(

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

for &(section_index, rect, _, _, _) in text_layout_info.section_geometry.iter() {
let section_entity = computed_block.entities()[section_index].entity;
for run in text_layout_info.run_geometry.iter() {
let section_entity = computed_block.entities()[run.span_index].entity;
let Ok(text_background_color) = text_background_colors_query.get(section_entity) else {
continue;
};
let render_entity = commands.spawn(TemporaryRenderEntity).id();
let offset = Vec2::new(rect.center().x, -rect.center().y);
let offset = Vec2::new(run.bounds.center().x, -run.bounds.center().y);
let transform = *global_transform
* GlobalTransform::from_translation(top_left.extend(0.))
* scaling
Expand All @@ -102,7 +102,7 @@ pub fn extract_text2d_sprite(
anchor: Vec2::ZERO,
rect: None,
scaling_mode: None,
custom_size: Some(rect.size()),
custom_size: Some(run.bounds.size()),
},
});
}
Expand Down Expand Up @@ -157,10 +157,8 @@ pub fn extract_text2d_sprite(
end += 1;
}

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

if has_strikethrough {
let render_entity = commands.spawn(TemporaryRenderEntity).id();
let offset = Vec2::new(rect.center().x, -strikethrough_y - 0.5 * stroke);
let offset = run.strikethrough_position() * Vec2::new(1., -1.);
let transform =
shadow_transform * GlobalTransform::from_translation(offset.extend(0.));
extracted_sprites.sprites.push(ExtractedSprite {
Expand All @@ -184,14 +182,14 @@ pub fn extract_text2d_sprite(
anchor: Vec2::ZERO,
rect: None,
scaling_mode: None,
custom_size: Some(Vec2::new(rect.size().x, stroke)),
custom_size: Some(run.strikethrough_size()),
},
});
}

if has_underline {
let render_entity = commands.spawn(TemporaryRenderEntity).id();
let offset = Vec2::new(rect.center().x, -underline_y - 0.5 * stroke);
let offset = run.strikethrough_position() * Vec2::new(1., -1.);
let transform =
shadow_transform * GlobalTransform::from_translation(offset.extend(0.));
extracted_sprites.sprites.push(ExtractedSprite {
Expand All @@ -206,7 +204,7 @@ pub fn extract_text2d_sprite(
anchor: Vec2::ZERO,
rect: None,
scaling_mode: None,
custom_size: Some(Vec2::new(rect.size().x, stroke)),
custom_size: Some(run.strikethrough_size()),
},
});
}
Expand Down Expand Up @@ -274,10 +272,8 @@ pub fn extract_text2d_sprite(
end += 1;
}

for &(section_index, rect, strikethrough_y, stroke, underline_y) in
text_layout_info.section_geometry.iter()
{
let section_entity = computed_block.entities()[section_index].entity;
for run in text_layout_info.run_geometry.iter() {
let section_entity = computed_block.entities()[run.span_index].entity;
let Ok((
text_color,
has_strike_through,
Expand All @@ -294,7 +290,7 @@ pub fn extract_text2d_sprite(
.unwrap_or(text_color.0)
.to_linear();
let render_entity = commands.spawn(TemporaryRenderEntity).id();
let offset = Vec2::new(rect.center().x, -strikethrough_y - 0.5 * stroke);
let offset = run.strikethrough_position() * Vec2::new(1., -1.);
let transform = *global_transform
* GlobalTransform::from_translation(top_left.extend(0.))
* scaling
Expand All @@ -311,7 +307,7 @@ pub fn extract_text2d_sprite(
anchor: Vec2::ZERO,
rect: None,
scaling_mode: None,
custom_size: Some(Vec2::new(rect.size().x, stroke)),
custom_size: Some(run.strikethrough_size()),
},
});
}
Expand All @@ -322,7 +318,7 @@ pub fn extract_text2d_sprite(
.unwrap_or(text_color.0)
.to_linear();
let render_entity = commands.spawn(TemporaryRenderEntity).id();
let offset = Vec2::new(rect.center().x, -underline_y - 0.5 * stroke);
let offset = run.underline_position() * Vec2::new(1., -1.);
let transform = *global_transform
* GlobalTransform::from_translation(top_left.extend(0.))
* scaling
Expand All @@ -339,7 +335,7 @@ pub fn extract_text2d_sprite(
anchor: Vec2::ZERO,
rect: None,
scaling_mode: None,
custom_size: Some(Vec2::new(rect.size().x, stroke)),
custom_size: Some(run.underline_size()),
},
});
}
Expand Down
89 changes: 71 additions & 18 deletions crates/bevy_text/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ impl TextPipeline {
swash_cache: &mut SwashCache,
) -> Result<(), TextError> {
layout_info.glyphs.clear();
layout_info.section_geometry.clear();
layout_info.run_geometry.clear();
layout_info.size = Default::default();

// Clear this here at the focal point of text rendering to ensure the field's lifecycle has strong boundaries.
Expand Down Expand Up @@ -336,18 +336,20 @@ impl TextPipeline {
match current_section {
Some(section) => {
if section != layout_glyph.metadata {
layout_info.section_geometry.push((
section,
Rect::new(
layout_info.run_geometry.push(RunGeometry {
span_index: section,
bounds: Rect::new(
start,
run.line_top,
end,
run.line_top + run.line_height,
),
(run.line_y - self.glyph_info[section].3).round(),
self.glyph_info[section].4,
(run.line_y - self.glyph_info[section].5).round(),
));
strikethrough_y: (run.line_y - self.glyph_info[section].3)
.round(),
strikethrough_thickness: self.glyph_info[section].4,
underline_y: (run.line_y - self.glyph_info[section].5).round(),
underline_thickness: self.glyph_info[section].4,
});
start = end.max(layout_glyph.x);
current_section = Some(layout_glyph.metadata);
}
Expand Down Expand Up @@ -432,13 +434,14 @@ impl TextPipeline {
Ok(())
});
if let Some(section) = current_section {
layout_info.section_geometry.push((
section,
Rect::new(start, run.line_top, end, run.line_top + run.line_height),
(run.line_y - self.glyph_info[section].3).round(),
self.glyph_info[section].4,
(run.line_y - self.glyph_info[section].5).round(),
));
layout_info.run_geometry.push(RunGeometry {
span_index: section,
bounds: Rect::new(start, run.line_top, end, run.line_top + run.line_height),
strikethrough_y: (run.line_y - self.glyph_info[section].3).round(),
strikethrough_thickness: self.glyph_info[section].4,
underline_y: (run.line_y - self.glyph_info[section].5).round(),
underline_thickness: self.glyph_info[section].4,
});
}

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

/// Geometry of a text run used to render text decorations like background colors, strikethrough, and underline.
/// A run in `bevy_text` is a contiguous sequence of glyphs on a line that share the same text attributes like font,
/// font size, and line height.
#[derive(Default, Debug, Clone, Reflect)]
pub struct RunGeometry {
/// The index of the text entity in [`ComputedTextBlock`] that this run belongs to.
pub span_index: usize,
/// Bounding box around the text run
pub bounds: Rect,
/// Y position of the strikethrough in the text layout.
pub strikethrough_y: f32,
/// Strikethrough stroke thickness.
pub strikethrough_thickness: f32,
/// Y position of the underline in the text layout.
pub underline_y: f32,
/// Underline stroke thickness.
pub underline_thickness: f32,
}

impl RunGeometry {
/// Returns the center of the strikethrough in the text layout.
pub fn strikethrough_position(&self) -> Vec2 {
Vec2::new(
self.bounds.center().x,
self.strikethrough_y + 0.5 * self.strikethrough_thickness,
)
}

/// Returns the size of the strikethrough.
pub fn strikethrough_size(&self) -> Vec2 {
Vec2::new(self.bounds.size().x, self.strikethrough_thickness)
}

/// Get the center of the underline in the text layout.
pub fn underline_position(&self) -> Vec2 {
Vec2::new(
self.bounds.center().x,
self.underline_y + 0.5 * self.underline_thickness,
)
}

/// Returns the size of the underline.
pub fn underline_size(&self) -> Vec2 {
Vec2::new(self.bounds.size().x, self.underline_thickness)
}
}

/// Size information for a corresponding [`ComputedTextBlock`] component.
///
/// Generated via [`TextPipeline::create_text_measure`].
Expand Down
47 changes: 14 additions & 33 deletions crates/bevy_ui_render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1094,10 +1094,8 @@ pub fn extract_text_shadows(
end += 1;
}

for &(section_index, rect, strikethrough_y, stroke, underline_y) in
text_layout_info.section_geometry.iter()
{
let section_entity = computed_block.entities()[section_index].entity;
for run in text_layout_info.run_geometry.iter() {
let section_entity = computed_block.entities()[run.span_index].entity;
let Ok((has_strikethrough, has_underline)) = text_decoration_query.get(section_entity)
else {
continue;
Expand All @@ -1111,15 +1109,12 @@ pub fn extract_text_shadows(
image: AssetId::default(),
extracted_camera_entity,
transform: node_transform
* Affine2::from_translation(Vec2::new(
rect.center().x,
strikethrough_y + 0.5 * stroke,
)),
* Affine2::from_translation(run.strikethrough_position()),
item: ExtractedUiItem::Node {
color: shadow.color.into(),
rect: Rect {
min: Vec2::ZERO,
max: Vec2::new(rect.size().x, stroke),
max: run.strikethrough_size(),
},
atlas_scaling: None,
flip_x: false,
Expand All @@ -1139,16 +1134,12 @@ pub fn extract_text_shadows(
clip: clip.map(|clip| clip.clip),
image: AssetId::default(),
extracted_camera_entity,
transform: node_transform
* Affine2::from_translation(Vec2::new(
rect.center().x,
underline_y + 0.5 * stroke,
)),
transform: node_transform * Affine2::from_translation(run.underline_position()),
item: ExtractedUiItem::Node {
color: shadow.color.into(),
rect: Rect {
min: Vec2::ZERO,
max: Vec2::new(rect.size().x, stroke),
max: run.underline_size(),
},
atlas_scaling: None,
flip_x: false,
Expand Down Expand Up @@ -1213,10 +1204,8 @@ pub fn extract_text_decorations(
let transform =
Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size());

for &(section_index, rect, strikethrough_y, stroke, underline_y) in
text_layout_info.section_geometry.iter()
{
let section_entity = computed_block.entities()[section_index].entity;
for run in text_layout_info.run_geometry.iter() {
let section_entity = computed_block.entities()[run.span_index].entity;
let Ok((
(text_background_color, maybe_strikethrough, maybe_underline),
text_color,
Expand All @@ -1234,12 +1223,12 @@ pub fn extract_text_decorations(
clip: clip.map(|clip| clip.clip),
image: AssetId::default(),
extracted_camera_entity,
transform: transform * Affine2::from_translation(rect.center()),
transform: transform * Affine2::from_translation(run.bounds.center()),
item: ExtractedUiItem::Node {
color: text_background_color.0.to_linear(),
rect: Rect {
min: Vec2::ZERO,
max: rect.size(),
max: run.bounds.size(),
},
atlas_scaling: None,
flip_x: false,
Expand All @@ -1264,16 +1253,12 @@ pub fn extract_text_decorations(
clip: clip.map(|clip| clip.clip),
image: AssetId::default(),
extracted_camera_entity,
transform: transform
* Affine2::from_translation(Vec2::new(
rect.center().x,
strikethrough_y + 0.5 * stroke,
)),
transform: transform * Affine2::from_translation(run.strikethrough_position()),
item: ExtractedUiItem::Node {
color,
rect: Rect {
min: Vec2::ZERO,
max: Vec2::new(rect.size().x, stroke),
max: run.strikethrough_size(),
},
atlas_scaling: None,
flip_x: false,
Expand All @@ -1298,16 +1283,12 @@ pub fn extract_text_decorations(
clip: clip.map(|clip| clip.clip),
image: AssetId::default(),
extracted_camera_entity,
transform: transform
* Affine2::from_translation(Vec2::new(
rect.center().x,
underline_y + 0.5 * stroke,
)),
transform: transform * Affine2::from_translation(run.underline_position()),
item: ExtractedUiItem::Node {
color,
rect: Rect {
min: Vec2::ZERO,
max: Vec2::new(rect.size().x, stroke),
max: run.underline_size(),
},
atlas_scaling: None,
flip_x: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: "`TextLayoutInfo`'s `section_rects` field has been replaced with `run_geometry`"
pull_requests: []
---

`TextLayoutInfo`'s `section_rects` field has been removed.
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.

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.