make UI text rendering camera driven (#13697)

# Objective

- Fixes #13687 

## Solution

- Text rendering in UI is still dependent on the `PrimaryWIndow`
- implements #10559 for text rendering

There are other parts of UI that are still `PrimaryWindow` dependent, if
the changes here are OK I'll apply them everywhere.
I'm not a fan of the `EntityHashMap` here to hold the scale factors, but
it seems the quick and easy fix

## Testing

- Run example `multiple_windows` on a screen with a scale factor
different than 1, close the primary window
This commit is contained in:
François Mockers 2024-06-06 02:20:50 +02:00 committed by GitHub
parent 52215ce072
commit 3d9b1e4025
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,6 +1,9 @@
use crate::{ContentSize, FixedMeasure, Measure, Node, NodeMeasure, UiScale};
use crate::{
ContentSize, DefaultUiCamera, FixedMeasure, Measure, Node, NodeMeasure, TargetCamera, UiScale,
};
use bevy_asset::Assets;
use bevy_ecs::{
entity::{Entity, EntityHashMap},
prelude::{Component, DetectChanges},
query::With,
reflect::ReflectComponent,
@ -9,13 +12,13 @@ use bevy_ecs::{
};
use bevy_math::Vec2;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::texture::Image;
use bevy_render::{camera::Camera, texture::Image};
use bevy_sprite::TextureAtlasLayout;
use bevy_text::{
scale_value, BreakLineOn, Font, FontAtlasSets, Text, TextError, TextLayoutInfo,
TextMeasureInfo, TextPipeline, TextSettings, YAxisOrientation,
};
use bevy_window::{PrimaryWindow, Window};
use bevy_utils::Entry;
use taffy::style::AvailableSpace;
/// Text system flags
@ -112,41 +115,54 @@ fn create_text_measure(
/// A `Measure` is used by the UI's layout algorithm to determine the appropriate amount of space
/// to provide for the text given the fonts, the text itself and the constraints of the layout.
///
/// * All measures are regenerated if the primary window's scale factor or [`UiScale`] is changed.
/// * Measures are regenerated if the target camera's scale factor (or primary window if no specific target) or [`UiScale`] is changed.
/// * Changes that only modify the colors of a `Text` do not require a new `Measure`. This system
/// is only able to detect that a `Text` component has changed and will regenerate the `Measure` on
/// color changes. This can be expensive, particularly for large blocks of text, and the [`bypass_change_detection`](bevy_ecs::change_detection::DetectChangesMut::bypass_change_detection)
/// method should be called when only changing the `Text`'s colors.
pub fn measure_text_system(
mut last_scale_factor: Local<f32>,
mut last_scale_factors: Local<EntityHashMap<f32>>,
fonts: Res<Assets<Font>>,
windows: Query<&Window, With<PrimaryWindow>>,
camera_query: Query<(Entity, &Camera)>,
default_ui_camera: DefaultUiCamera,
ui_scale: Res<UiScale>,
mut text_query: Query<(Ref<Text>, &mut ContentSize, &mut TextFlags), With<Node>>,
mut text_query: Query<
(
Ref<Text>,
&mut ContentSize,
&mut TextFlags,
Option<&TargetCamera>,
),
With<Node>,
>,
) {
let window_scale_factor = windows
.get_single()
.map(|window| window.resolution.scale_factor())
.unwrap_or(1.);
let mut scale_factors: EntityHashMap<f32> = EntityHashMap::default();
let scale_factor = ui_scale.0 * window_scale_factor;
#[allow(clippy::float_cmp)]
if *last_scale_factor == scale_factor {
// scale factor unchanged, only create new measure funcs for modified text
for (text, content_size, text_flags) in &mut text_query {
if text.is_changed() || text_flags.needs_new_measure_func || content_size.is_added() {
create_text_measure(&fonts, scale_factor, text, content_size, text_flags);
}
}
} else {
// scale factor changed, create new measure funcs for all text
*last_scale_factor = scale_factor;
for (text, content_size, text_flags) in &mut text_query {
for (text, content_size, text_flags, camera) in &mut text_query {
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else {
continue;
};
let scale_factor = match scale_factors.entry(camera_entity) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => *entry.insert(
camera_query
.get(camera_entity)
.ok()
.and_then(|(_, c)| c.target_scaling_factor())
.unwrap_or(1.0)
* ui_scale.0,
),
};
if last_scale_factors.get(&camera_entity) != Some(&scale_factor)
|| text.is_changed()
|| text_flags.needs_new_measure_func
|| content_size.is_added()
{
create_text_measure(&fonts, scale_factor, text, content_size, text_flags);
}
}
*last_scale_factors = scale_factors;
}
#[allow(clippy::too_many_arguments)]
@ -219,49 +235,47 @@ fn queue_text(
#[allow(clippy::too_many_arguments)]
pub fn text_system(
mut textures: ResMut<Assets<Image>>,
mut last_scale_factor: Local<f32>,
mut last_scale_factors: Local<EntityHashMap<f32>>,
fonts: Res<Assets<Font>>,
windows: Query<&Window, With<PrimaryWindow>>,
camera_query: Query<(Entity, &Camera)>,
default_ui_camera: DefaultUiCamera,
text_settings: Res<TextSettings>,
ui_scale: Res<UiScale>,
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
mut font_atlas_sets: ResMut<FontAtlasSets>,
mut text_pipeline: ResMut<TextPipeline>,
mut text_query: Query<(Ref<Node>, &Text, &mut TextLayoutInfo, &mut TextFlags)>,
mut text_query: Query<(
Ref<Node>,
&Text,
&mut TextLayoutInfo,
&mut TextFlags,
Option<&TargetCamera>,
)>,
) {
// TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621
let window_scale_factor = windows
.get_single()
.map(|window| window.resolution.scale_factor())
.unwrap_or(1.);
let mut scale_factors: EntityHashMap<f32> = EntityHashMap::default();
let scale_factor = ui_scale.0 * window_scale_factor;
let inverse_scale_factor = scale_factor.recip();
if *last_scale_factor == scale_factor {
// Scale factor unchanged, only recompute text for modified text nodes
for (node, text, text_layout_info, text_flags) in &mut text_query {
if node.is_changed() || text_flags.needs_recompute {
queue_text(
&fonts,
&mut text_pipeline,
&mut font_atlas_sets,
&mut texture_atlases,
&mut textures,
&text_settings,
scale_factor,
inverse_scale_factor,
text,
node,
text_flags,
text_layout_info,
);
}
}
} else {
// Scale factor changed, recompute text for all text nodes
*last_scale_factor = scale_factor;
for (node, text, text_layout_info, text_flags, camera) in &mut text_query {
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else {
continue;
};
let scale_factor = match scale_factors.entry(camera_entity) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => *entry.insert(
camera_query
.get(camera_entity)
.ok()
.and_then(|(_, c)| c.target_scaling_factor())
.unwrap_or(1.0)
* ui_scale.0,
),
};
let inverse_scale_factor = scale_factor.recip();
for (node, text, text_layout_info, text_flags) in &mut text_query {
if last_scale_factors.get(&camera_entity) != Some(&scale_factor)
|| node.is_changed()
|| text_flags.needs_recompute
{
queue_text(
&fonts,
&mut text_pipeline,
@ -278,4 +292,5 @@ pub fn text_system(
);
}
}
*last_scale_factors = scale_factors;
}