diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 0289b78dcdfa9..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}; @@ -513,6 +516,189 @@ impl TextPipeline { .cloned() .map(|(id, _)| id) } + + /// Update [`TextLayoutInfo`] with the new [`PositionedGlyph`] layout. + pub fn update_text_layout_info<'a>( + &mut self, + layout_info: &mut TextLayoutInfo, + text_font_query: Query<&'a TextFont>, + 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 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.0, + 0.0, + ); + + 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; + 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..8c05bdde1a520 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,42 +337,67 @@ 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, 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, text_layout_info, 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 { - 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 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 + } else { + // `scale_factor` is already multiplied by `UiScale` + TextBounds::new(node.unrounded_size.x, node.unrounded_size.y) + }; + + match text_pipeline.update_text_layout_info( + &mut text_layout_info, + text_font_query, + 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; + } + } } } }