diff --git a/crates/bevy_sprite_render/src/text2d/mod.rs b/crates/bevy_sprite_render/src/text2d/mod.rs index 138d513a4a130..2cb857e502c48 100644 --- a/crates/bevy_sprite_render/src/text2d/mod.rs +++ b/crates/bevy_sprite_render/src/text2d/mod.rs @@ -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 @@ -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()), }, }); } @@ -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 { @@ -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 { @@ -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 { @@ -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()), }, }); } @@ -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, @@ -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 @@ -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()), }, }); } @@ -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 @@ -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()), }, }); } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 21b63149468a1..6dcc72ebd4abc 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -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. @@ -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); } @@ -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 @@ -518,13 +521,63 @@ pub struct TextLayoutInfo { pub scale_factor: f32, /// Scaled and positioned glyphs in screenspace pub glyphs: Vec, - /// 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, /// 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`]. diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 92a4f5b2e1d72..3e8a63fd72ef2 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -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; @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, diff --git a/release-content/migration-guides/text_layout_info_section_rects_is_replaced_by_run_geometry.md b/release-content/migration-guides/text_layout_info_section_rects_is_replaced_by_run_geometry.md new file mode 100644 index 0000000000000..bbbdfc76cb43c --- /dev/null +++ b/release-content/migration-guides/text_layout_info_section_rects_is_replaced_by_run_geometry.md @@ -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.