use crate::{
ComputedNode, ComputedUiRenderTargetInfo, ContentSize, FixedMeasure, Measure, MeasureArgs,
Node, NodeMeasure,
};
use bevy_asset::Assets;
use bevy_color::Color;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
change_detection::DetectChanges,
component::Component,
entity::Entity,
query::With,
reflect::ReflectComponent,
system::{Query, Res, ResMut},
world::Ref,
};
use bevy_image::prelude::*;
use bevy_log::warn_once;
use bevy_math::Vec2;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_text::{
ComputedTextBlock, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, LineBreak, LineHeight,
RemSize, ScaleCx, TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo,
TextMeasureInfo, TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter,
};
use taffy::style::AvailableSpace;
use tracing::error;
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
pub struct TextNodeFlags {
needs_measure_fn: bool,
needs_recompute: bool,
}
impl Default for TextNodeFlags {
fn default() -> Self {
Self {
needs_measure_fn: true,
needs_recompute: true,
}
}
}
#[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect, PartialEq)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
#[require(
Node,
TextLayout,
TextFont,
TextColor,
LineHeight,
TextNodeFlags,
ContentSize,
FontHinting::Disabled
)]
pub struct Text(pub String);
impl Text {
pub fn new(text: impl Into<String>) -> Self {
Self(text.into())
}
}
impl TextRoot for Text {}
impl TextSpanAccess for Text {
fn read_span(&self) -> &str {
self.as_str()
}
fn write_span(&mut self) -> &mut String {
&mut *self
}
}
impl From<&str> for Text {
fn from(value: &str) -> Self {
Self(String::from(value))
}
}
impl From<String> for Text {
fn from(value: String) -> Self {
Self(value)
}
}
#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)]
#[reflect(Component, Default, Debug, Clone, PartialEq)]
pub struct TextShadow {
pub offset: Vec2,
pub color: Color,
}
impl Default for TextShadow {
fn default() -> Self {
Self {
offset: Vec2::splat(4.),
color: Color::linear_rgba(0., 0., 0., 0.75),
}
}
}
pub type TextUiReader<'w, 's> = TextReader<'w, 's, Text>;
pub type TextUiWriter<'w, 's> = TextWriter<'w, 's, Text>;
pub struct TextMeasure {
pub info: TextMeasureInfo,
}
impl TextMeasure {
#[inline]
pub const fn needs_buffer(height: Option<f32>, available_width: AvailableSpace) -> bool {
height.is_none() && matches!(available_width, AvailableSpace::Definite(_))
}
}
impl Measure for TextMeasure {
fn measure(&mut self, measure_args: MeasureArgs, _style: &taffy::Style) -> Vec2 {
let MeasureArgs {
width,
height,
available_width,
buffer,
font_system,
..
} = measure_args;
let x = width.unwrap_or_else(|| match available_width {
AvailableSpace::Definite(x) => {
x.max(self.info.min.x).min(self.info.max.x)
}
AvailableSpace::MinContent => self.info.min.x,
AvailableSpace::MaxContent => self.info.max.x,
});
height
.map_or_else(
|| match available_width {
AvailableSpace::Definite(_) => {
if let Some(buffer) = buffer {
self.info.compute_size(
TextBounds::new_horizontal(x),
buffer,
font_system,
)
} else {
error!("text measure failed, buffer is missing");
Vec2::default()
}
}
AvailableSpace::MinContent => Vec2::new(x, self.info.min.y),
AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y),
},
|y| Vec2::new(x, y),
)
.ceil()
}
}
pub fn measure_text_system(
fonts: Res<Assets<Font>>,
mut text_query: Query<
(
Entity,
Ref<TextLayout>,
&mut ContentSize,
&mut TextNodeFlags,
&mut ComputedTextBlock,
Ref<ComputedUiRenderTargetInfo>,
&ComputedNode,
Ref<FontHinting>,
),
With<Node>,
>,
mut text_reader: TextUiReader,
mut text_pipeline: ResMut<TextPipeline>,
mut font_system: ResMut<FontCx>,
mut layout_cx: ResMut<LayoutCx>,
rem_size: Res<RemSize>,
) {
for (
entity,
block,
mut content_size,
mut text_flags,
mut computed,
computed_target,
computed_node,
hinting,
) in &mut text_query
{
if !(1e-5
< (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs()
|| computed.needs_rerender(computed_target.is_changed(), rem_size.is_changed())
|| text_flags.needs_measure_fn
|| content_size.is_added()
|| hinting.is_changed())
{
continue;
}
match text_pipeline.create_text_measure(
entity,
fonts.as_ref(),
text_reader.iter(entity),
computed_target.scale_factor,
&block,
computed.as_mut(),
&mut font_system,
&mut layout_cx,
*hinting,
computed_target.logical_size(),
rem_size.0,
) {
Ok(measure) => {
if block.linebreak == LineBreak::NoWrap {
content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max }));
} else {
content_size.set(NodeMeasure::Text(TextMeasure { info: measure }));
}
text_flags.needs_measure_fn = false;
text_flags.needs_recompute = true;
}
Err(
TextError::NoSuchFont
| TextError::NoSuchFontFamily(_)
| TextError::DegenerateScaleFactor,
) => {
text_flags.needs_measure_fn = true;
}
Err(
e @ (TextError::FailedToAddGlyph(_)
| TextError::FailedToGetGlyphImage(_)
| TextError::MissingAtlasLayout
| TextError::MissingAtlasTexture
| TextError::InconsistentAtlasState),
) => {
panic!("Fatal error when processing text: {e}.");
}
};
}
}
pub fn text_system(
mut textures: ResMut<Assets<Image>>,
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
mut font_atlas_set: ResMut<FontAtlasSet>,
mut text_pipeline: ResMut<TextPipeline>,
mut text_query: Query<(
Ref<ComputedNode>,
&TextLayout,
&mut TextLayoutInfo,
&mut TextNodeFlags,
&mut ComputedTextBlock,
&FontHinting,
)>,
mut scale_cx: ResMut<ScaleCx>,
) {
for (node, block, mut text_layout_info, mut text_flags, mut computed, hinting) in
&mut text_query
{
if node.is_changed() || text_flags.needs_recompute {
if text_flags.needs_measure_fn {
continue;
}
let physical_node_size = if block.linebreak == LineBreak::NoWrap {
TextBounds::UNBOUNDED
} else {
TextBounds::new(node.unrounded_size.x, node.unrounded_size.y)
};
match text_pipeline.update_text_layout_info(
&mut text_layout_info,
&mut font_atlas_set,
&mut texture_atlases,
&mut textures,
&mut computed,
&mut scale_cx,
physical_node_size,
block.justify,
*hinting,
) {
Err(
TextError::NoSuchFont
| TextError::NoSuchFontFamily(_)
| TextError::DegenerateScaleFactor,
) => {
text_flags.needs_recompute = true;
}
Err(e @ TextError::FailedToGetGlyphImage(_)) => {
warn_once!("{e}.");
text_flags.needs_recompute = false;
text_layout_info.clear();
}
Err(
e @ (TextError::FailedToAddGlyph(_)
| TextError::MissingAtlasLayout
| TextError::MissingAtlasTexture
| TextError::InconsistentAtlasState),
) => {
panic!("Fatal error when processing text: {e}.");
}
Ok(()) => {
text_layout_info.scale_factor = node.inverse_scale_factor().recip();
text_layout_info.size *= node.inverse_scale_factor();
text_flags.needs_recompute = false;
}
}
}
}
}