Skip to content

Commit ffc62c1

Browse files
ickshonpemockersf
andauthored
text_system split (#7779)
# Objective `text_system` runs before the UI layout is calculated and the size of the text node is determined, so it cannot correctly shape the text to fit the layout, and has no way of determining if the text needs to be wrapped. The function `text_constraint` attempts to determine the size of the node from the local size constraints in the `Style` component. It can't be made to work, you have to compute the whole layout to get the correct size. A simple example of where this fails completely is a text node set to stretch to fill the empty space adjacent to a node with size constraints set to `Val::Percent(50.)`. The text node will take up half the space, even though its size constraints are `Val::Auto` Also because the `text_system` queries for changes to the `Style` component, when a style value is changed that doesn't affect the node's geometry the text is recomputed unnecessarily. Querying on changes to `Node` is not much better. The UI layout is changed to fit the `CalculatedSize` of the text, so the size of the node is changed and so the text and UI layout get recalculated multiple times from a single change to a `Text`. Also, the `MeasureFunc` doesn't work at all, it doesn't have enough information to fit the text correctly and makes no attempt. Fixes #7663, #6717, #5834, #1490, ## Solution Split the `text_system` into two functions: * `measure_text_system` which calculates the size constraints for the text node and runs before `UiSystem::Flex` * `text_system` which runs after `UiSystem::Flex` and generates the actual text. * Fix the `MeasureFunc` calculations. --- Text wrapping in main: <img width="961" alt="Capturemain" src="https://user-images.githubusercontent.com/27962798/220425740-4fe4bf46-24fb-4685-a1cf-bc01e139e72d.PNG"> With this PR: <img width="961" alt="captured_wrap" src="https://user-images.githubusercontent.com/27962798/220425807-949996b0-f127-4637-9f33-56a6da944fb0.PNG"> ## Changelog * Removed the previous fields from `CalculatedSize`. `CalculatedSize` now contains a boxed `Measure`. * Added `measurement` module to `bevy_ui`. * Added the method `create_text_measure` to `TextPipeline`. * Added a new system `measure_text_system` that runs before `UiSystem::Flex` that creates a `MeasureFunc` for the text. * Rescheduled `text_system` to run after `UiSystem::Flex`. * Added a trait `Measure`. A `Measure` is used to compute the size of a UI node when the size of that node is based on its content. * Added `ImageMeasure` and `TextMeasure` which implement `Measure`. * Added a new component `UiImageSize` which is used by `update_image_calculated_size_system` to track image size changes. * Added a `UiImageSize` component to `ImageBundle`. ## Migration Guide `ImageBundle` has a new component `UiImageSize` which contains the size of the image bundle's texture and is updated automatically by `update_image_calculated_size_system` --------- Co-authored-by: François <mockersf@gmail.com>
1 parent 328347f commit ffc62c1

File tree

10 files changed

+464
-147
lines changed

10 files changed

+464
-147
lines changed

crates/bevy_text/src/pipeline.rs

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use bevy_render::texture::Image;
77
use bevy_sprite::TextureAtlas;
88
use bevy_utils::HashMap;
99

10-
use glyph_brush_layout::{FontId, SectionText};
10+
use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText};
1111

1212
use crate::{
1313
error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, FontAtlasSet,
@@ -54,7 +54,7 @@ impl TextPipeline {
5454
font_atlas_warning: &mut FontAtlasWarning,
5555
y_axis_orientation: YAxisOrientation,
5656
) -> Result<TextLayoutInfo, TextError> {
57-
let mut scaled_fonts = Vec::new();
57+
let mut scaled_fonts = Vec::with_capacity(sections.len());
5858
let sections = sections
5959
.iter()
6060
.map(|section| {
@@ -92,6 +92,9 @@ impl TextPipeline {
9292
for sg in &section_glyphs {
9393
let scaled_font = scaled_fonts[sg.section_index];
9494
let glyph = &sg.glyph;
95+
// The fonts use a coordinate system increasing upwards so ascent is a positive value
96+
// and descent is negative, but Bevy UI uses a downwards increasing coordinate system,
97+
// so we have to subtract from the baseline position to get the minimum and maximum values.
9598
min_x = min_x.min(glyph.position.x);
9699
min_y = min_y.min(glyph.position.y - scaled_font.ascent());
97100
max_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id));
@@ -114,4 +117,140 @@ impl TextPipeline {
114117

115118
Ok(TextLayoutInfo { glyphs, size })
116119
}
120+
121+
pub fn create_text_measure(
122+
&mut self,
123+
fonts: &Assets<Font>,
124+
sections: &[TextSection],
125+
scale_factor: f64,
126+
text_alignment: TextAlignment,
127+
linebreak_behaviour: BreakLineOn,
128+
) -> Result<TextMeasureInfo, TextError> {
129+
let mut auto_fonts = Vec::with_capacity(sections.len());
130+
let mut scaled_fonts = Vec::with_capacity(sections.len());
131+
let sections = sections
132+
.iter()
133+
.enumerate()
134+
.map(|(i, section)| {
135+
let font = fonts
136+
.get(&section.style.font)
137+
.ok_or(TextError::NoSuchFont)?;
138+
let font_size = scale_value(section.style.font_size, scale_factor);
139+
auto_fonts.push(font.font.clone());
140+
let px_scale_font = ab_glyph::Font::into_scaled(font.font.clone(), font_size);
141+
scaled_fonts.push(px_scale_font);
142+
143+
let section = TextMeasureSection {
144+
font_id: FontId(i),
145+
scale: PxScale::from(font_size),
146+
text: section.value.clone(),
147+
};
148+
149+
Ok(section)
150+
})
151+
.collect::<Result<Vec<_>, _>>()?;
152+
153+
Ok(TextMeasureInfo::new(
154+
auto_fonts,
155+
scaled_fonts,
156+
sections,
157+
text_alignment,
158+
linebreak_behaviour.into(),
159+
))
160+
}
161+
}
162+
163+
#[derive(Debug, Clone)]
164+
pub struct TextMeasureSection {
165+
pub text: String,
166+
pub scale: PxScale,
167+
pub font_id: FontId,
168+
}
169+
170+
#[derive(Debug, Clone)]
171+
pub struct TextMeasureInfo {
172+
pub fonts: Vec<ab_glyph::FontArc>,
173+
pub scaled_fonts: Vec<ab_glyph::PxScaleFont<ab_glyph::FontArc>>,
174+
pub sections: Vec<TextMeasureSection>,
175+
pub text_alignment: TextAlignment,
176+
pub linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker,
177+
pub min_width_content_size: Vec2,
178+
pub max_width_content_size: Vec2,
179+
}
180+
181+
impl TextMeasureInfo {
182+
fn new(
183+
fonts: Vec<ab_glyph::FontArc>,
184+
scaled_fonts: Vec<ab_glyph::PxScaleFont<ab_glyph::FontArc>>,
185+
sections: Vec<TextMeasureSection>,
186+
text_alignment: TextAlignment,
187+
linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker,
188+
) -> Self {
189+
let mut info = Self {
190+
fonts,
191+
scaled_fonts,
192+
sections,
193+
text_alignment,
194+
linebreak_behaviour,
195+
min_width_content_size: Vec2::ZERO,
196+
max_width_content_size: Vec2::ZERO,
197+
};
198+
199+
let section_texts = info.prepare_section_texts();
200+
let min =
201+
info.compute_size_from_section_texts(&section_texts, Vec2::new(0.0, f32::INFINITY));
202+
let max = info.compute_size_from_section_texts(
203+
&section_texts,
204+
Vec2::new(f32::INFINITY, f32::INFINITY),
205+
);
206+
info.min_width_content_size = min;
207+
info.max_width_content_size = max;
208+
info
209+
}
210+
211+
fn prepare_section_texts(&self) -> Vec<SectionText> {
212+
self.sections
213+
.iter()
214+
.map(|section| SectionText {
215+
font_id: section.font_id,
216+
scale: section.scale,
217+
text: &section.text,
218+
})
219+
.collect::<Vec<_>>()
220+
}
221+
222+
fn compute_size_from_section_texts(&self, sections: &[SectionText], bounds: Vec2) -> Vec2 {
223+
let geom = SectionGeometry {
224+
bounds: (bounds.x, bounds.y),
225+
..Default::default()
226+
};
227+
let section_glyphs = glyph_brush_layout::Layout::default()
228+
.h_align(self.text_alignment.into())
229+
.line_breaker(self.linebreak_behaviour)
230+
.calculate_glyphs(&self.fonts, &geom, sections);
231+
232+
let mut min_x: f32 = std::f32::MAX;
233+
let mut min_y: f32 = std::f32::MAX;
234+
let mut max_x: f32 = std::f32::MIN;
235+
let mut max_y: f32 = std::f32::MIN;
236+
237+
for sg in section_glyphs {
238+
let scaled_font = &self.scaled_fonts[sg.section_index];
239+
let glyph = &sg.glyph;
240+
// The fonts use a coordinate system increasing upwards so ascent is a positive value
241+
// and descent is negative, but Bevy UI uses a downwards increasing coordinate system,
242+
// so we have to subtract from the baseline position to get the minimum and maximum values.
243+
min_x = min_x.min(glyph.position.x);
244+
min_y = min_y.min(glyph.position.y - scaled_font.ascent());
245+
max_x = max_x.max(glyph.position.x + scaled_font.h_advance(glyph.id));
246+
max_y = max_y.max(glyph.position.y - scaled_font.descent());
247+
}
248+
249+
Vec2::new(max_x - min_x, max_y - min_y)
250+
}
251+
252+
pub fn compute_size(&self, bounds: Vec2) -> Vec2 {
253+
let sections = self.prepare_section_texts();
254+
self.compute_size_from_section_texts(&sections, bounds)
255+
}
117256
}

crates/bevy_text/src/text2d.rs

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use bevy_ecs::{
77
event::EventReader,
88
prelude::With,
99
reflect::ReflectComponent,
10-
system::{Commands, Local, Query, Res, ResMut},
10+
system::{Local, Query, Res, ResMut},
1111
};
1212
use bevy_math::{Vec2, Vec3};
1313
use bevy_reflect::Reflect;
@@ -72,6 +72,8 @@ pub struct Text2dBundle {
7272
pub visibility: Visibility,
7373
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering.
7474
pub computed_visibility: ComputedVisibility,
75+
/// Contains the size of the text and its glyph's position and scale data. Generated via [`TextPipeline::queue_text`]
76+
pub text_layout_info: TextLayoutInfo,
7577
}
7678

7779
pub fn extract_text2d_sprite(
@@ -147,7 +149,6 @@ pub fn extract_text2d_sprite(
147149
/// It does not modify or observe existing ones.
148150
#[allow(clippy::too_many_arguments)]
149151
pub fn update_text2d_layout(
150-
mut commands: Commands,
151152
// Text items which should be reprocessed again, generally when the font hasn't loaded yet.
152153
mut queue: Local<HashSet<Entity>>,
153154
mut textures: ResMut<Assets<Image>>,
@@ -159,12 +160,7 @@ pub fn update_text2d_layout(
159160
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
160161
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
161162
mut text_pipeline: ResMut<TextPipeline>,
162-
mut text_query: Query<(
163-
Entity,
164-
Ref<Text>,
165-
Ref<Text2dBounds>,
166-
Option<&mut TextLayoutInfo>,
167-
)>,
163+
mut text_query: Query<(Entity, Ref<Text>, Ref<Text2dBounds>, &mut TextLayoutInfo)>,
168164
) {
169165
// We need to consume the entire iterator, hence `last`
170166
let factor_changed = scale_factor_changed.iter().last().is_some();
@@ -175,7 +171,7 @@ pub fn update_text2d_layout(
175171
.map(|window| window.resolution.scale_factor())
176172
.unwrap_or(1.0);
177173

178-
for (entity, text, bounds, text_layout_info) in &mut text_query {
174+
for (entity, text, bounds, mut text_layout_info) in &mut text_query {
179175
if factor_changed || text.is_changed() || bounds.is_changed() || queue.remove(&entity) {
180176
let text_bounds = Vec2::new(
181177
scale_value(bounds.size.x, scale_factor),
@@ -204,12 +200,7 @@ pub fn update_text2d_layout(
204200
Err(e @ TextError::FailedToAddGlyph(_)) => {
205201
panic!("Fatal error when processing text: {e}.");
206202
}
207-
Ok(info) => match text_layout_info {
208-
Some(mut t) => *t = info,
209-
None => {
210-
commands.entity(entity).insert(info);
211-
}
212-
},
203+
Ok(info) => *text_layout_info = info,
213204
}
214205
}
215206
}

crates/bevy_ui/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ bevy_window = { path = "../bevy_window", version = "0.11.0-dev" }
3131
bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" }
3232

3333
# other
34-
taffy = { version = "0.3.5", default-features = false, features = ["std"] }
34+
taffy = { version = "0.3.10", default-features = false, features = ["std"] }
3535
serde = { version = "1", features = ["derive"] }
3636
smallvec = { version = "1.6", features = ["union", "const_generics"] }
3737
bytemuck = { version = "1.5", features = ["derive"] }

crates/bevy_ui/src/flex/mod.rs

Lines changed: 21 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -97,45 +97,35 @@ impl FlexSurface {
9797
&mut self,
9898
entity: Entity,
9999
style: &Style,
100-
calculated_size: CalculatedSize,
100+
calculated_size: &CalculatedSize,
101101
context: &LayoutContext,
102102
) {
103103
let taffy = &mut self.taffy;
104104
let taffy_style = convert::from_style(context, style);
105-
let scale_factor = context.scale_factor;
106-
let measure = taffy::node::MeasureFunc::Boxed(Box::new(
107-
move |constraints: Size<Option<f32>>, _available: Size<AvailableSpace>| {
108-
let mut size = Size {
109-
width: (scale_factor * calculated_size.size.x as f64) as f32,
110-
height: (scale_factor * calculated_size.size.y as f64) as f32,
111-
};
112-
match (constraints.width, constraints.height) {
113-
(None, None) => {}
114-
(Some(width), None) => {
115-
if calculated_size.preserve_aspect_ratio {
116-
size.height = width * size.height / size.width;
117-
}
118-
size.width = width;
119-
}
120-
(None, Some(height)) => {
121-
if calculated_size.preserve_aspect_ratio {
122-
size.width = height * size.width / size.height;
123-
}
124-
size.height = height;
125-
}
126-
(Some(width), Some(height)) => {
127-
size.width = width;
128-
size.height = height;
129-
}
105+
let measure = calculated_size.measure.dyn_clone();
106+
let measure_func = taffy::node::MeasureFunc::Boxed(Box::new(
107+
move |constraints: Size<Option<f32>>, available: Size<AvailableSpace>| {
108+
let size = measure.measure(
109+
constraints.width,
110+
constraints.height,
111+
available.width,
112+
available.height,
113+
);
114+
taffy::geometry::Size {
115+
width: size.x,
116+
height: size.y,
130117
}
131-
size
132118
},
133119
));
134120
if let Some(taffy_node) = self.entity_to_taffy.get(&entity) {
135121
self.taffy.set_style(*taffy_node, taffy_style).unwrap();
136-
self.taffy.set_measure(*taffy_node, Some(measure)).unwrap();
122+
self.taffy
123+
.set_measure(*taffy_node, Some(measure_func))
124+
.unwrap();
137125
} else {
138-
let taffy_node = taffy.new_leaf_with_measure(taffy_style, measure).unwrap();
126+
let taffy_node = taffy
127+
.new_leaf_with_measure(taffy_style, measure_func)
128+
.unwrap();
139129
self.entity_to_taffy.insert(entity, taffy_node);
140130
}
141131
}
@@ -307,7 +297,7 @@ pub fn flex_node_system(
307297
for (entity, style, calculated_size) in &query {
308298
// TODO: remove node from old hierarchy if its root has changed
309299
if let Some(calculated_size) = calculated_size {
310-
flex_surface.upsert_leaf(entity, style, *calculated_size, viewport_values);
300+
flex_surface.upsert_leaf(entity, style, calculated_size, viewport_values);
311301
} else {
312302
flex_surface.upsert_node(entity, style, viewport_values);
313303
}
@@ -322,7 +312,7 @@ pub fn flex_node_system(
322312
}
323313

324314
for (entity, style, calculated_size) in &changed_size_query {
325-
flex_surface.upsert_leaf(entity, style, *calculated_size, &viewport_values);
315+
flex_surface.upsert_leaf(entity, style, calculated_size, &viewport_values);
326316
}
327317

328318
// clean up removed nodes

crates/bevy_ui/src/lib.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mod ui_node;
1414
#[cfg(feature = "bevy_text")]
1515
mod accessibility;
1616
pub mod camera_config;
17+
pub mod measurement;
1718
pub mod node_bundles;
1819
pub mod update;
1920
pub mod widget;
@@ -24,17 +25,20 @@ use bevy_render::extract_component::ExtractComponentPlugin;
2425
pub use flex::*;
2526
pub use focus::*;
2627
pub use geometry::*;
28+
pub use measurement::*;
2729
pub use render::*;
2830
pub use ui_node::*;
2931

3032
#[doc(hidden)]
3133
pub mod prelude {
3234
#[doc(hidden)]
3335
pub use crate::{
34-
camera_config::*, geometry::*, node_bundles::*, ui_node::*, widget::*, Interaction, UiScale,
36+
camera_config::*, geometry::*, node_bundles::*, ui_node::*, widget::Button, widget::Label,
37+
Interaction, UiScale,
3538
};
3639
}
3740

41+
use crate::prelude::UiCameraConfig;
3842
use bevy_app::prelude::*;
3943
use bevy_ecs::prelude::*;
4044
use bevy_input::InputSystem;
@@ -43,8 +47,6 @@ use stack::ui_stack_system;
4347
pub use stack::UiStack;
4448
use update::update_clipping_system;
4549

46-
use crate::prelude::UiCameraConfig;
47-
4850
/// The basic plugin for Bevy UI
4951
#[derive(Default)]
5052
pub struct UiPlugin;
@@ -114,7 +116,7 @@ impl Plugin for UiPlugin {
114116
#[cfg(feature = "bevy_text")]
115117
app.add_systems(
116118
PostUpdate,
117-
widget::text_system
119+
widget::measure_text_system
118120
.before(UiSystem::Flex)
119121
// Potential conflict: `Assets<Image>`
120122
// In practice, they run independently since `bevy_render::camera_update_system`
@@ -149,6 +151,7 @@ impl Plugin for UiPlugin {
149151
.before(TransformSystem::TransformPropagate),
150152
ui_stack_system.in_set(UiSystem::Stack),
151153
update_clipping_system.after(TransformSystem::TransformPropagate),
154+
widget::text_system.after(UiSystem::Flex),
152155
),
153156
);
154157

0 commit comments

Comments
 (0)