From 2804722213d5664dbb363f7f278bfba74fef5e12 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 27 Nov 2025 11:23:20 +0000 Subject: [PATCH 1/7] Simplify the `buffer_dimensions` function --- crates/bevy_text/src/pipeline.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index cd40cdab2a011..21abde7b3a61e 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -14,6 +14,7 @@ use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; +use tracing::info_span; use crate::{ add_glyph_to_atlas, error::TextError, get_glyph_atlas_info, ComputedTextBlock, Font, @@ -606,9 +607,13 @@ impl TextMeasureInfo { ) -> Vec2 { // Note that this arbitrarily adjusts the buffer layout. We assume the buffer is always 'refreshed' // whenever a canonical state is required. + + let _measure_span = info_span!("measure_span", name = "measure_span").entered(); computed .buffer .set_size(&mut font_system.0, bounds.width, bounds.height); + + let _dim_span = info_span!("dim_measure_span", name = "dim_measure_span").entered(); buffer_dimensions(&computed.buffer) } } @@ -678,13 +683,12 @@ fn get_attrs<'a>( /// Calculate the size of the text area for the given buffer. fn buffer_dimensions(buffer: &Buffer) -> Vec2 { - let (width, height) = buffer - .layout_runs() - .map(|run| (run.line_w, run.line_height)) - .reduce(|(w1, h1), (w2, h2)| (w1.max(w2), h1 + h2)) - .unwrap_or((0.0, 0.0)); - - Vec2::new(width, height).ceil() + let mut size = Vec2::ZERO; + for run in buffer.layout_runs() { + size.x = size.x.max(run.line_w); + size.y += run.line_height; + } + size.ceil() } /// Discards stale data cached in `FontSystem`. From e51a0e396bd46485403a94af41b631e618b443a2 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 27 Nov 2025 14:10:55 +0000 Subject: [PATCH 2/7] Clean up --- crates/bevy_text/src/pipeline.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 21abde7b3a61e..0289b78dcdfa9 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -14,7 +14,6 @@ use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; -use tracing::info_span; use crate::{ add_glyph_to_atlas, error::TextError, get_glyph_atlas_info, ComputedTextBlock, Font, @@ -607,13 +606,9 @@ impl TextMeasureInfo { ) -> Vec2 { // Note that this arbitrarily adjusts the buffer layout. We assume the buffer is always 'refreshed' // whenever a canonical state is required. - - let _measure_span = info_span!("measure_span", name = "measure_span").entered(); computed .buffer .set_size(&mut font_system.0, bounds.width, bounds.height); - - let _dim_span = info_span!("dim_measure_span", name = "dim_measure_span").entered(); buffer_dimensions(&computed.buffer) } } From ea19712aad827aec9a76b0dc6327aa60ff0275d0 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 28 Nov 2025 12:33:46 +0000 Subject: [PATCH 3/7] * Renmoved `bevy_ui::widget::queue_text`. * Added `render_text` method to `TextPipeline` that only updates the `TextLayoutInfo` with updating the text's `CosmicBuffer`. --- crates/bevy_text/src/pipeline.rs | 185 ++++++++++++++++++++++++++++++ crates/bevy_ui/src/widget/text.rs | 125 +++++++------------- 2 files changed, 227 insertions(+), 83 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 0289b78dcdfa9..81fafd9473d96 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -513,6 +513,191 @@ impl TextPipeline { .cloned() .map(|(id, _)| id) } + + /// Update TextLayoutInfo for rendering + pub fn render_text<'a>( + &mut self, + layout_info: &mut TextLayoutInfo, + text_spans: impl Iterator, + scale_factor: f64, + font_atlas_set: &mut FontAtlasSet, + texture_atlases: &mut Assets, + textures: &mut Assets, + computed: &mut ComputedTextBlock, + font_system: &mut CosmicFontSystem, + swash_cache: &mut SwashCache, + bounds: TextBounds, + ) -> Result<(), TextError> { + layout_info.glyphs.clear(); + layout_info.run_geometry.clear(); + layout_info.size = Default::default(); + + self.glyph_info.clear(); + + for (_entity, _depth, _text, text_font, _color, _line_height) in text_spans { + self.glyph_info.push(( + text_font.font.id(), + text_font.font_smoothing, + text_font.font_size, + 0., + 0., + 0., + )); + + let glyph_info = self.glyph_info.last_mut().unwrap(); + + let Some((id, _)) = self.map_handle_to_font_id.get(&glyph_info.0) else { + continue; + }; + let weight = font_system + .db() + .face(*id) + .map(|f| f.weight) + .unwrap_or(cosmic_text::Weight::NORMAL); + if let Some(font) = font_system.get_font(*id, weight) { + let swash = font.as_swash(); + let metrics = swash.metrics(&[]); + let upem = metrics.units_per_em as f32; + let scalar = glyph_info.2 * scale_factor as f32 / upem; + glyph_info.3 = (metrics.strikeout_offset * scalar).round(); + glyph_info.4 = (metrics.stroke_size * scalar).round().max(1.); + glyph_info.5 = (metrics.underline_offset * scalar).round(); + } + } + + let buffer = &mut computed.buffer; + buffer.set_size(font_system, bounds.width, bounds.height); + let box_size = buffer_dimensions(buffer); + + let result = buffer.layout_runs().try_for_each(|run| { + let mut current_section: Option = None; + let mut start = 0.; + let mut end = 0.; + let result = run + .glyphs + .iter() + .map(move |layout_glyph| (layout_glyph, run.line_y, run.line_i)) + .try_for_each(|(layout_glyph, line_y, line_i)| { + match current_section { + Some(section) => { + if section != layout_glyph.metadata { + 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, + }); + start = end.max(layout_glyph.x); + current_section = Some(layout_glyph.metadata); + } + end = layout_glyph.x + layout_glyph.w; + } + None => { + current_section = Some(layout_glyph.metadata); + start = layout_glyph.x; + end = start + layout_glyph.w; + } + } + + let mut temp_glyph; + let span_index = layout_glyph.metadata; + let font_id = self.glyph_info[span_index].0; + let font_smoothing = self.glyph_info[span_index].1; + + let layout_glyph = if font_smoothing == FontSmoothing::None { + // If font smoothing is disabled, round the glyph positions and sizes, + // effectively discarding all subpixel layout. + temp_glyph = layout_glyph.clone(); + temp_glyph.x = temp_glyph.x.round(); + temp_glyph.y = temp_glyph.y.round(); + temp_glyph.w = temp_glyph.w.round(); + temp_glyph.x_offset = temp_glyph.x_offset.round(); + temp_glyph.y_offset = temp_glyph.y_offset.round(); + temp_glyph.line_height_opt = temp_glyph.line_height_opt.map(f32::round); + + &temp_glyph + } else { + layout_glyph + }; + + let physical_glyph = layout_glyph.physical((0., 0.), 1.); + + let font_atlases = font_atlas_set + .entry(FontAtlasKey( + font_id, + physical_glyph.cache_key.font_size_bits, + font_smoothing, + )) + .or_default(); + + let atlas_info = get_glyph_atlas_info(font_atlases, physical_glyph.cache_key) + .map(Ok) + .unwrap_or_else(|| { + add_glyph_to_atlas( + font_atlases, + texture_atlases, + textures, + &mut font_system.0, + &mut swash_cache.0, + layout_glyph, + font_smoothing, + ) + })?; + + let texture_atlas = texture_atlases.get(atlas_info.texture_atlas).unwrap(); + let location = atlas_info.location; + let glyph_rect = texture_atlas.textures[location.glyph_index]; + let left = location.offset.x as f32; + let top = location.offset.y as f32; + let glyph_size = UVec2::new(glyph_rect.width(), glyph_rect.height()); + + // offset by half the size because the origin is center + let x = glyph_size.x as f32 / 2.0 + left + physical_glyph.x as f32; + let y = + line_y.round() + physical_glyph.y as f32 - top + glyph_size.y as f32 / 2.0; + + let position = Vec2::new(x, y); + + let pos_glyph = PositionedGlyph { + position, + size: glyph_size.as_vec2(), + atlas_info, + span_index, + byte_index: layout_glyph.start, + byte_length: layout_glyph.end - layout_glyph.start, + line_index: line_i, + }; + layout_info.glyphs.push(pos_glyph); + Ok(()) + }); + if let Some(section) = current_section { + 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 + }); + + // Check result. + result?; + + layout_info.size = box_size; + Ok(()) + } } /// Render information for a corresponding text block. diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index d604bc6977cd9..53ea05d19e392 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -327,74 +327,6 @@ pub fn measure_text_system( } } -#[inline] -fn queue_text( - entity: Entity, - fonts: &Assets, - text_pipeline: &mut TextPipeline, - font_atlas_set: &mut FontAtlasSet, - texture_atlases: &mut Assets, - textures: &mut Assets, - scale_factor: f32, - inverse_scale_factor: f32, - block: &TextLayout, - node: Ref, - mut text_flags: Mut, - text_layout_info: Mut, - computed: &mut ComputedTextBlock, - text_reader: &mut TextUiReader, - font_system: &mut CosmicFontSystem, - swash_cache: &mut SwashCache, -) { - // Skip the text node if it is waiting for a new measure func - if text_flags.needs_measure_fn { - return; - } - - let physical_node_size = if block.linebreak == LineBreak::NoWrap { - // With `NoWrap` set, no constraints are placed on the width of the text. - TextBounds::UNBOUNDED - } else { - // `scale_factor` is already multiplied by `UiScale` - TextBounds::new(node.unrounded_size.x, node.unrounded_size.y) - }; - - let text_layout_info = text_layout_info.into_inner(); - match text_pipeline.queue_text( - text_layout_info, - fonts, - text_reader.iter(entity), - scale_factor.into(), - block, - physical_node_size, - font_atlas_set, - texture_atlases, - textures, - computed, - font_system, - swash_cache, - ) { - Err(TextError::NoSuchFont) => { - // There was an error processing the text layout, try again next frame - text_flags.needs_recompute = true; - } - Err( - e @ (TextError::FailedToAddGlyph(_) - | TextError::FailedToGetGlyphImage(_) - | TextError::MissingAtlasLayout - | TextError::MissingAtlasTexture - | TextError::InconsistentAtlasState), - ) => { - panic!("Fatal error when processing text: {e}."); - } - Ok(()) => { - text_layout_info.scale_factor = scale_factor; - text_layout_info.size *= inverse_scale_factor; - text_flags.needs_recompute = false; - } - } -} - /// Updates the layout and size information for a UI text node on changes to the size value of its [`Node`] component, /// or when the `needs_recompute` field of [`TextNodeFlags`] is set to true. /// This information is computed by the [`TextPipeline`] and then stored in [`TextLayoutInfo`]. @@ -405,7 +337,6 @@ fn queue_text( /// It does not modify or observe existing ones. The exception is when adding new glyphs to a [`bevy_text::FontAtlas`]. pub fn text_system( mut textures: ResMut>, - fonts: Res>, mut texture_atlases: ResMut>, mut font_atlas_set: ResMut, mut text_pipeline: ResMut, @@ -421,26 +352,54 @@ pub fn text_system( mut font_system: ResMut, mut swash_cache: ResMut, ) { - for (entity, node, block, text_layout_info, text_flags, mut computed) in &mut text_query { + for (entity, node, block, text_layout_info, mut text_flags, mut computed) in &mut text_query { if node.is_changed() || text_flags.needs_recompute { - queue_text( - entity, - &fonts, - &mut text_pipeline, + // Skip the text node if it is waiting for a new measure func + if text_flags.needs_measure_fn { + return; + } + + let physical_node_size = if block.linebreak == LineBreak::NoWrap { + // With `NoWrap` set, no constraints are placed on the width of the text. + TextBounds::UNBOUNDED + } else { + // `scale_factor` is already multiplied by `UiScale` + TextBounds::new(node.unrounded_size.x, node.unrounded_size.y) + }; + + let scale_factor = node.inverse_scale_factor().recip().into(); + let text_layout_info = text_layout_info.into_inner(); + match text_pipeline.render_text( + text_layout_info, + text_reader.iter(entity), + scale_factor, &mut font_atlas_set, &mut texture_atlases, &mut textures, - node.inverse_scale_factor.recip(), - node.inverse_scale_factor, - block, - node, - text_flags, - text_layout_info, - computed.as_mut(), - &mut text_reader, + &mut computed, &mut font_system, &mut swash_cache, - ); + physical_node_size, + ) { + Err(TextError::NoSuchFont) => { + // There was an error processing the text layout, try again next frame + text_flags.needs_recompute = true; + } + Err( + e @ (TextError::FailedToAddGlyph(_) + | TextError::FailedToGetGlyphImage(_) + | TextError::MissingAtlasLayout + | TextError::MissingAtlasTexture + | TextError::InconsistentAtlasState), + ) => { + panic!("Fatal error when processing text: {e}."); + } + Ok(()) => { + text_layout_info.scale_factor = scale_factor as f32; + text_layout_info.size *= node.inverse_scale_factor(); + text_flags.needs_recompute = false; + } + } } } } From ae00e19c79921dbb60b34e2c3e2aaeba25553345 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 28 Nov 2025 13:29:44 +0000 Subject: [PATCH 4/7] renamed `render_text` to `update_text_layout_info` --- crates/bevy_text/src/pipeline.rs | 2 +- crates/bevy_ui/src/widget/text.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 81fafd9473d96..a4b3d8ccbf098 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -515,7 +515,7 @@ impl TextPipeline { } /// Update TextLayoutInfo for rendering - pub fn render_text<'a>( + pub fn update_text_layout_info<'a>( &mut self, layout_info: &mut TextLayoutInfo, text_spans: impl Iterator, diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 53ea05d19e392..6b8da9a0a8432 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -369,7 +369,7 @@ pub fn text_system( let scale_factor = node.inverse_scale_factor().recip().into(); let text_layout_info = text_layout_info.into_inner(); - match text_pipeline.render_text( + match text_pipeline.update_text_layout_info( text_layout_info, text_reader.iter(entity), scale_factor, From aad15d8bb06732b024e9460fa646858828b2296a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 28 Nov 2025 13:41:03 +0000 Subject: [PATCH 5/7] just use `&mut` instead of `into_inner` in `text_system` --- crates/bevy_ui/src/widget/text.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 6b8da9a0a8432..87993db509f19 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -352,13 +352,15 @@ pub fn text_system( mut font_system: ResMut, mut swash_cache: ResMut, ) { - for (entity, node, block, text_layout_info, mut text_flags, mut computed) in &mut text_query { + for (entity, node, block, mut text_layout_info, mut text_flags, mut computed) in &mut text_query + { if node.is_changed() || text_flags.needs_recompute { // Skip the text node if it is waiting for a new measure func if text_flags.needs_measure_fn { return; } + let scale_factor = node.inverse_scale_factor().recip().into(); let physical_node_size = if block.linebreak == LineBreak::NoWrap { // With `NoWrap` set, no constraints are placed on the width of the text. TextBounds::UNBOUNDED @@ -367,10 +369,8 @@ pub fn text_system( TextBounds::new(node.unrounded_size.x, node.unrounded_size.y) }; - let scale_factor = node.inverse_scale_factor().recip().into(); - let text_layout_info = text_layout_info.into_inner(); match text_pipeline.update_text_layout_info( - text_layout_info, + &mut text_layout_info, text_reader.iter(entity), scale_factor, &mut font_atlas_set, From 36d74af2f1489165fccd5c1d6818bf02606d4923 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 28 Nov 2025 14:02:15 +0000 Subject: [PATCH 6/7] Fixed doc comment --- crates/bevy_text/src/pipeline.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index a4b3d8ccbf098..7499fa0bc7090 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -514,7 +514,7 @@ impl TextPipeline { .map(|(id, _)| id) } - /// Update TextLayoutInfo for rendering + /// Update [`TextLayoutInfo`] with the new [`PositionedGlyph`] layout. pub fn update_text_layout_info<'a>( &mut self, layout_info: &mut TextLayoutInfo, From 5787b0fed6e724f8ef7f9f42646539be0e7a1590 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 28 Nov 2025 14:54:49 +0000 Subject: [PATCH 7/7] Clean up: - Replaced `TextReader` param with a query for `TextFont` - Instead of pushing the span info and then retrieving it again with last_mut, push it at the end of the loop. - Removed `Entity` from `text_system`'s `text_query`. --- crates/bevy_text/src/pipeline.rs | 55 ++++++++++++++++--------------- crates/bevy_ui/src/widget/text.rs | 8 ++--- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 7499fa0bc7090..a3a8a69137f3d 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -4,8 +4,11 @@ use bevy_asset::{AssetId, Assets}; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - component::Component, entity::Entity, reflect::ReflectComponent, resource::Resource, - system::ResMut, + component::Component, + entity::Entity, + reflect::ReflectComponent, + resource::Resource, + system::{Query, ResMut}, }; use bevy_image::prelude::*; use bevy_log::{once, warn}; @@ -518,7 +521,7 @@ impl TextPipeline { pub fn update_text_layout_info<'a>( &mut self, layout_info: &mut TextLayoutInfo, - text_spans: impl Iterator, + text_font_query: Query<&'a TextFont>, scale_factor: f64, font_atlas_set: &mut FontAtlasSet, texture_atlases: &mut Assets, @@ -534,35 +537,33 @@ impl TextPipeline { self.glyph_info.clear(); - for (_entity, _depth, _text, text_font, _color, _line_height) in text_spans { - self.glyph_info.push(( + for text_font in text_font_query.iter_many(computed.entities.iter().map(|e| e.entity)) { + let mut section_info = ( text_font.font.id(), text_font.font_smoothing, text_font.font_size, - 0., - 0., - 0., - )); - - let glyph_info = self.glyph_info.last_mut().unwrap(); + 0.0, + 0.0, + 0.0, + ); - let Some((id, _)) = self.map_handle_to_font_id.get(&glyph_info.0) else { - continue; - }; - let weight = font_system - .db() - .face(*id) - .map(|f| f.weight) - .unwrap_or(cosmic_text::Weight::NORMAL); - if let Some(font) = font_system.get_font(*id, weight) { - let swash = font.as_swash(); - let metrics = swash.metrics(&[]); - let upem = metrics.units_per_em as f32; - let scalar = glyph_info.2 * scale_factor as f32 / upem; - glyph_info.3 = (metrics.strikeout_offset * scalar).round(); - glyph_info.4 = (metrics.stroke_size * scalar).round().max(1.); - glyph_info.5 = (metrics.underline_offset * scalar).round(); + if let Some((id, _)) = self.map_handle_to_font_id.get(§ion_info.0) { + let weight = font_system + .db() + .face(*id) + .map(|f| f.weight) + .unwrap_or(cosmic_text::Weight::NORMAL); + if let Some(font) = font_system.get_font(*id, weight) { + let swash = font.as_swash(); + let metrics = swash.metrics(&[]); + let upem = metrics.units_per_em as f32; + let scalar = section_info.2 * scale_factor as f32 / upem; + section_info.3 = (metrics.strikeout_offset * scalar).round(); + section_info.4 = (metrics.stroke_size * scalar).round().max(1.); + section_info.5 = (metrics.underline_offset * scalar).round(); + } } + self.glyph_info.push(section_info); } let buffer = &mut computed.buffer; diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 87993db509f19..8c05bdde1a520 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -341,19 +341,17 @@ pub fn text_system( mut font_atlas_set: ResMut, mut text_pipeline: ResMut, mut text_query: Query<( - Entity, Ref, &TextLayout, &mut TextLayoutInfo, &mut TextNodeFlags, &mut ComputedTextBlock, )>, - mut text_reader: TextUiReader, + text_font_query: Query<&TextFont>, mut font_system: ResMut, mut swash_cache: ResMut, ) { - for (entity, node, block, mut text_layout_info, mut text_flags, mut computed) in &mut text_query - { + for (node, block, mut text_layout_info, mut text_flags, mut computed) in &mut text_query { if node.is_changed() || text_flags.needs_recompute { // Skip the text node if it is waiting for a new measure func if text_flags.needs_measure_fn { @@ -371,7 +369,7 @@ pub fn text_system( match text_pipeline.update_text_layout_info( &mut text_layout_info, - text_reader.iter(entity), + text_font_query, scale_factor, &mut font_atlas_set, &mut texture_atlases,