Skip to content

Commit

Permalink
split up TextStyle (#15857)
Browse files Browse the repository at this point in the history
# Objective

Currently text is recomputed unnecessarily on any changes to its color,
which is extremely expensive.

## Solution
Split up `TextStyle` into two separate components `TextFont` and
`TextColor`.

## Testing

I added this system to `many_buttons`:
```rust
fn set_text_colors_changed(mut colors: Query<&mut TextColor>) {
    for mut text_color in colors.iter_mut() {
        text_color.set_changed();
    }
}
```

reports ~4fps on main, ~50fps with this PR.

## Migration Guide
`TextStyle` has been renamed to `TextFont` and its `color` field has
been moved to a separate component named `TextColor` which newtypes
`Color`.
  • Loading branch information
ickshonpe authored Oct 13, 2024
1 parent 6521e75 commit 6f7d0e5
Show file tree
Hide file tree
Showing 83 changed files with 752 additions and 641 deletions.
16 changes: 10 additions & 6 deletions crates/bevy_dev_tools/src/fps_overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use bevy_ecs::{
};
use bevy_hierarchy::{BuildChildren, ChildBuild};
use bevy_render::view::Visibility;
use bevy_text::{Font, TextSpan, TextStyle};
use bevy_text::{Font, TextColor, TextFont, TextSpan};
use bevy_ui::{
node_bundles::NodeBundle,
widget::{Text, UiTextWriter},
Expand Down Expand Up @@ -62,20 +62,22 @@ impl Plugin for FpsOverlayPlugin {
#[derive(Resource, Clone)]
pub struct FpsOverlayConfig {
/// Configuration of text in the overlay.
pub text_config: TextStyle,
pub text_config: TextFont,
/// Color of text in the overlay.
pub text_color: Color,
/// Displays the FPS overlay if true.
pub enabled: bool,
}

impl Default for FpsOverlayConfig {
fn default() -> Self {
FpsOverlayConfig {
text_config: TextStyle {
text_config: TextFont {
font: Handle::<Font>::default(),
font_size: 32.0,
color: Color::WHITE,
..default()
},
text_color: Color::WHITE,
enabled: true,
}
}
Expand All @@ -102,6 +104,7 @@ fn setup(mut commands: Commands, overlay_config: Res<FpsOverlayConfig>) {
p.spawn((
Text::new("FPS: "),
overlay_config.text_config.clone(),
TextColor(overlay_config.text_color),
FpsText,
))
.with_child((TextSpan::default(), overlay_config.text_config.clone()));
Expand All @@ -128,9 +131,10 @@ fn customize_text(
mut writer: UiTextWriter,
) {
for entity in &query {
writer.for_each_style(entity, |mut style| {
*style = overlay_config.text_config.clone();
writer.for_each_font(entity, |mut font| {
*font = overlay_config.text_config.clone();
});
writer.for_each_color(entity, |mut color| color.0 = overlay_config.text_color);
}
}

Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_text/src/font_atlas_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ pub struct FontAtlasKey(pub u32, pub FontSmoothing);
/// A `FontAtlasSet` is an [`Asset`].
///
/// There is one `FontAtlasSet` for each font:
/// - When a [`Font`] is loaded as an asset and then used in [`TextStyle`](crate::TextStyle),
/// - When a [`Font`] is loaded as an asset and then used in [`TextFont`](crate::TextFont),
/// a `FontAtlasSet` asset is created from a weak handle to the `Font`.
/// - ~When a font is loaded as a system font, and then used in [`TextStyle`](crate::TextStyle),
/// - ~When a font is loaded as a system font, and then used in [`TextFont`](crate::TextFont),
/// a `FontAtlasSet` asset is created and stored with a strong handle to the `FontAtlasSet`.~
/// (*Note that system fonts are not currently supported by the `TextPipeline`.*)
///
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_text/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ pub use text_access::*;
pub mod prelude {
#[doc(hidden)]
pub use crate::{
Font, JustifyText, LineBreak, Text2d, TextError, TextLayout, TextReader2d, TextSpan,
TextStyle, TextWriter2d,
Font, JustifyText, LineBreak, Text2d, TextColor, TextError, TextFont, TextLayout,
TextReader2d, TextSpan, TextWriter2d,
};
}

Expand Down
68 changes: 38 additions & 30 deletions crates/bevy_text/src/pipeline.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use alloc::sync::Arc;

use bevy_asset::{AssetId, Assets};
use bevy_color::Color;
use bevy_ecs::{
component::Component,
entity::Entity,
Expand All @@ -17,7 +18,7 @@ use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap};

use crate::{
error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, JustifyText,
LineBreak, PositionedGlyph, TextBounds, TextEntity, TextLayout, TextStyle, YAxisOrientation,
LineBreak, PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, YAxisOrientation,
};

/// A wrapper resource around a [`cosmic_text::FontSystem`]
Expand Down Expand Up @@ -70,7 +71,7 @@ pub struct TextPipeline {
/// Buffered vec for collecting spans.
///
/// See [this dark magic](https://users.rust-lang.org/t/how-to-cache-a-vectors-capacity/94478/10).
spans_buffer: Vec<(usize, &'static str, &'static TextStyle, FontFaceInfo)>,
spans_buffer: Vec<(usize, &'static str, &'static TextFont, FontFaceInfo)>,
/// Buffered vec for collecting info for glyph assembly.
glyph_info: Vec<(AssetId<Font>, FontSmoothing)>,
}
Expand All @@ -83,7 +84,7 @@ impl TextPipeline {
pub fn update_buffer<'a>(
&mut self,
fonts: &Assets<Font>,
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextStyle)>,
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
linebreak: LineBreak,
justify: JustifyText,
bounds: TextBounds,
Expand All @@ -96,22 +97,22 @@ impl TextPipeline {
// Collect span information into a vec. This is necessary because font loading requires mut access
// to FontSystem, which the cosmic-text Buffer also needs.
let mut font_size: f32 = 0.;
let mut spans: Vec<(usize, &str, &TextStyle, FontFaceInfo)> =
let mut spans: Vec<(usize, &str, &TextFont, FontFaceInfo, Color)> =
core::mem::take(&mut self.spans_buffer)
.into_iter()
.map(|_| -> (usize, &str, &TextStyle, FontFaceInfo) { unreachable!() })
.map(|_| -> (usize, &str, &TextFont, FontFaceInfo, Color) { unreachable!() })
.collect();

computed.entities.clear();

for (span_index, (entity, depth, span, style)) in text_spans.enumerate() {
for (span_index, (entity, depth, span, text_font, color)) in text_spans.enumerate() {
// Return early if a font is not loaded yet.
if !fonts.contains(style.font.id()) {
if !fonts.contains(text_font.font.id()) {
spans.clear();
self.spans_buffer = spans
.into_iter()
.map(
|_| -> (usize, &'static str, &'static TextStyle, FontFaceInfo) {
|_| -> (usize, &'static str, &'static TextFont, FontFaceInfo) {
unreachable!()
},
)
Expand All @@ -124,17 +125,21 @@ impl TextPipeline {
computed.entities.push(TextEntity { entity, depth });

// Get max font size for use in cosmic Metrics.
font_size = font_size.max(style.font_size);
font_size = font_size.max(text_font.font_size);

// Load Bevy fonts into cosmic-text's font system.
let face_info =
load_font_to_fontdb(style, font_system, &mut self.map_handle_to_font_id, fonts);
let face_info = load_font_to_fontdb(
text_font,
font_system,
&mut self.map_handle_to_font_id,
fonts,
);

// Save spans that aren't zero-sized.
if scale_factor <= 0.0 || style.font_size <= 0.0 {
if scale_factor <= 0.0 || text_font.font_size <= 0.0 {
continue;
}
spans.push((span_index, span, style, face_info));
spans.push((span_index, span, text_font, face_info, color));
}

let line_height = font_size * 1.2;
Expand All @@ -151,12 +156,14 @@ impl TextPipeline {
// The section index is stored in the metadata of the spans, and could be used
// to look up the section the span came from and is not used internally
// in cosmic-text.
let spans_iter = spans.iter().map(|(span_index, span, style, font_info)| {
(
*span,
get_attrs(*span_index, style, font_info, scale_factor),
)
});
let spans_iter = spans
.iter()
.map(|(span_index, span, text_font, font_info, color)| {
(
*span,
get_attrs(*span_index, text_font, *color, font_info, scale_factor),
)
});

// Update the buffer.
let buffer = &mut computed.buffer;
Expand Down Expand Up @@ -186,7 +193,7 @@ impl TextPipeline {
spans.clear();
self.spans_buffer = spans
.into_iter()
.map(|_| -> (usize, &'static str, &'static TextStyle, FontFaceInfo) { unreachable!() })
.map(|_| -> (usize, &'static str, &'static TextFont, FontFaceInfo) { unreachable!() })
.collect();

Ok(())
Expand All @@ -201,7 +208,7 @@ impl TextPipeline {
&mut self,
layout_info: &mut TextLayoutInfo,
fonts: &Assets<Font>,
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextStyle)>,
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
scale_factor: f64,
layout: &TextLayout,
bounds: TextBounds,
Expand All @@ -222,8 +229,8 @@ impl TextPipeline {
// Extract font ids from the iterator while traversing it.
let mut glyph_info = core::mem::take(&mut self.glyph_info);
glyph_info.clear();
let text_spans = text_spans.inspect(|(_, _, _, style)| {
glyph_info.push((style.font.id(), style.font_smoothing));
let text_spans = text_spans.inspect(|(_, _, _, text_font, _)| {
glyph_info.push((text_font.font.id(), text_font.font_smoothing));
});

let update_result = self.update_buffer(
Expand Down Expand Up @@ -335,7 +342,7 @@ impl TextPipeline {
&mut self,
entity: Entity,
fonts: &Assets<Font>,
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextStyle)>,
text_spans: impl Iterator<Item = (Entity, usize, &'a str, &'a TextFont, Color)>,
scale_factor: f64,
layout: &TextLayout,
computed: &mut ComputedTextBlock,
Expand Down Expand Up @@ -427,12 +434,12 @@ impl TextMeasureInfo {
}

fn load_font_to_fontdb(
style: &TextStyle,
text_font: &TextFont,
font_system: &mut cosmic_text::FontSystem,
map_handle_to_font_id: &mut HashMap<AssetId<Font>, (cosmic_text::fontdb::ID, Arc<str>)>,
fonts: &Assets<Font>,
) -> FontFaceInfo {
let font_handle = style.font.clone();
let font_handle = text_font.font.clone();
let (face_id, family_name) = map_handle_to_font_id
.entry(font_handle.id())
.or_insert_with(|| {
Expand Down Expand Up @@ -461,10 +468,11 @@ fn load_font_to_fontdb(
}
}

/// Translates [`TextStyle`] to [`Attrs`].
/// Translates [`TextFont`] to [`Attrs`].
fn get_attrs<'a>(
span_index: usize,
style: &TextStyle,
text_font: &TextFont,
color: Color,
face_info: &'a FontFaceInfo,
scale_factor: f64,
) -> Attrs<'a> {
Expand All @@ -474,8 +482,8 @@ fn get_attrs<'a>(
.stretch(face_info.stretch)
.style(face_info.style)
.weight(face_info.weight)
.metrics(Metrics::relative(style.font_size, 1.2).scale(scale_factor as f32))
.color(cosmic_text::Color(style.color.to_linear().as_u32()));
.metrics(Metrics::relative(text_font.font_size, 1.2).scale(scale_factor as f32))
.color(cosmic_text::Color(color.to_linear().as_u32()));
attrs
}

Expand Down
Loading

0 comments on commit 6f7d0e5

Please sign in to comment.