Skip to content
Open
190 changes: 188 additions & 2 deletions crates/bevy_text/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<TextureAtlasLayout>,
textures: &mut Assets<Image>,
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(&section_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<usize> = 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.
Expand Down
127 changes: 42 additions & 85 deletions crates/bevy_ui/src/widget/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,74 +327,6 @@ pub fn measure_text_system(
}
}

#[inline]
fn queue_text(
entity: Entity,
fonts: &Assets<Font>,
text_pipeline: &mut TextPipeline,
font_atlas_set: &mut FontAtlasSet,
texture_atlases: &mut Assets<TextureAtlasLayout>,
textures: &mut Assets<Image>,
scale_factor: f32,
inverse_scale_factor: f32,
block: &TextLayout,
node: Ref<ComputedNode>,
mut text_flags: Mut<TextNodeFlags>,
text_layout_info: Mut<TextLayoutInfo>,
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`].
Expand All @@ -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<Assets<Image>>,
fonts: Res<Assets<Font>>,
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
mut font_atlas_set: ResMut<FontAtlasSet>,
mut text_pipeline: ResMut<TextPipeline>,
mut text_query: Query<(
Entity,
Ref<ComputedNode>,
&TextLayout,
&mut TextLayoutInfo,
&mut TextNodeFlags,
&mut ComputedTextBlock,
)>,
mut text_reader: TextUiReader,
text_font_query: Query<&TextFont>,
mut font_system: ResMut<CosmicFontSystem>,
mut swash_cache: ResMut<SwashCache>,
) {
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;
}
}
}
}
}