use crate::{Font, TextBrush, TextLayoutInfo, TextSection};
use bevy_asset::Handle;
use bevy_color::Color;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{prelude::*, reflect::ReflectComponent};
use bevy_math::Vec2;
use bevy_reflect::prelude::*;
use bevy_utils::{default, once};
use core::fmt::{Debug, Formatter};
use core::str::from_utf8;
use parley::setting::Tag;
use parley::{FontFeature, FontVariation, Layout};
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use smol_str::SmolStr;
use tracing::warn;
#[derive(Debug, Copy, Clone, Reflect)]
#[reflect(Debug, Clone)]
pub struct TextEntity {
pub entity: Entity,
pub depth: usize,
pub font_smoothing: FontSmoothing,
}
#[derive(Component, Clone, Reflect)]
#[reflect(Component, Debug, Default, Clone)]
pub struct ComputedTextBlock {
#[reflect(ignore, clone)]
pub(crate) layout: Layout<TextBrush>,
pub(crate) entities: SmallVec<[TextEntity; 1]>,
pub(crate) needs_rerender: bool,
pub(crate) uses_viewport_sizes: bool,
pub(crate) uses_rem_sizes: bool,
}
impl Debug for ComputedTextBlock {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ComputedTextBlock")
.field("layout", &"Layout(..)")
.field("entities", &self.entities)
.field("needs_rerender", &self.needs_rerender)
.field("uses_viewport_sizes", &self.uses_viewport_sizes)
.field("uses_rem_sizes", &self.uses_rem_sizes)
.finish()
}
}
impl ComputedTextBlock {
pub fn entities(&self) -> &[TextEntity] {
&self.entities
}
pub fn needs_rerender(
&self,
is_viewport_size_changed: bool,
is_rem_size_changed: bool,
) -> bool {
self.needs_rerender
|| (is_viewport_size_changed && self.uses_viewport_sizes)
|| (is_rem_size_changed && self.uses_rem_sizes)
}
pub fn buffer(&self) -> &Layout<TextBrush> {
&self.layout
}
}
impl Default for ComputedTextBlock {
fn default() -> Self {
Self {
layout: Layout::new(),
entities: SmallVec::default(),
needs_rerender: true,
uses_rem_sizes: false,
uses_viewport_sizes: false,
}
}
}
#[derive(Component, Debug, Copy, Clone, Default, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
#[require(ComputedTextBlock, TextLayoutInfo)]
pub struct TextLayout {
pub justify: Justify,
pub linebreak: LineBreak,
}
impl TextLayout {
pub const fn new(justify: Justify, linebreak: LineBreak) -> Self {
Self { justify, linebreak }
}
pub fn justify(justify: Justify) -> Self {
Self::default().with_justify(justify)
}
pub fn linebreak(linebreak: LineBreak) -> Self {
Self::default().with_linebreak(linebreak)
}
pub fn no_wrap() -> Self {
Self::default().with_no_wrap()
}
pub const fn with_justify(mut self, justify: Justify) -> Self {
self.justify = justify;
self
}
pub const fn with_linebreak(mut self, linebreak: LineBreak) -> Self {
self.linebreak = linebreak;
self
}
pub const fn with_no_wrap(mut self) -> Self {
self.linebreak = LineBreak::NoWrap;
self
}
}
#[derive(Component, Debug, Default, Clone, Deref, DerefMut, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
#[require(TextFont, TextColor, LineHeight, LetterSpacing)]
pub struct TextSpan(pub String);
impl TextSpan {
pub fn new(text: impl Into<String>) -> Self {
Self(text.into())
}
}
impl TextSection for TextSpan {
fn get_text(&self) -> &str {
self.as_str()
}
fn get_text_mut(&mut self) -> &mut String {
&mut *self
}
}
impl From<&str> for TextSpan {
fn from(value: &str) -> Self {
Self(String::from(value))
}
}
impl From<String> for TextSpan {
fn from(value: String) -> Self {
Self(value)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)]
#[reflect(Serialize, Deserialize, Clone, PartialEq, Hash)]
#[doc(alias = "JustifyText")]
pub enum Justify {
#[default]
Left,
Center,
Right,
Justified,
Start,
End,
}
impl From<Justify> for parley::Alignment {
fn from(justify: Justify) -> Self {
match justify {
Justify::Start => parley::Alignment::Start,
Justify::End => parley::Alignment::End,
Justify::Left => parley::Alignment::Left,
Justify::Center => parley::Alignment::Center,
Justify::Right => parley::Alignment::Right,
Justify::Justified => parley::Alignment::Justify,
}
}
}
#[derive(Clone, Debug, Reflect, PartialEq, FromTemplate)]
pub enum FontSource {
#[default]
Handle(Handle<Font>),
Family(SmolStr),
Serif,
SansSerif,
Cursive,
Fantasy,
Monospace,
SystemUi,
UiSerif,
UiSansSerif,
UiMonospace,
UiRounded,
Emoji,
Math,
FangSong,
}
impl Default for FontSource {
fn default() -> Self {
Self::Handle(Handle::default())
}
}
impl From<Handle<Font>> for FontSource {
fn from(handle: Handle<Font>) -> Self {
Self::Handle(handle)
}
}
impl From<&Handle<Font>> for FontSource {
fn from(handle: &Handle<Font>) -> Self {
Self::Handle(handle.clone())
}
}
impl From<SmolStr> for FontSource {
fn from(family: SmolStr) -> Self {
FontSource::Family(family)
}
}
impl From<&str> for FontSource {
fn from(family: &str) -> Self {
FontSource::Family(family.into())
}
}
#[derive(Component, Clone, Debug, Reflect, PartialEq, FromTemplate)]
#[reflect(Component, Default, Debug, Clone)]
pub struct TextFont {
pub font: FontSource,
pub font_size: FontSize,
pub weight: FontWeight,
pub width: FontWidth,
pub style: FontStyle,
pub font_smoothing: FontSmoothing,
pub font_features: FontFeatures,
pub font_variations: FontVariations,
}
impl TextFont {
pub fn from_font_size(font_size: impl Into<FontSize>) -> Self {
Self::default().with_font_size(font_size)
}
pub fn from_font_weight(weight: impl Into<FontWeight>) -> Self {
Self::default().with_font_weight(weight)
}
pub fn with_font(mut self, font: Handle<Font>) -> Self {
self.font = FontSource::Handle(font);
self
}
pub fn with_family(mut self, family: impl Into<SmolStr>) -> Self {
self.font = FontSource::Family(family.into());
self
}
pub fn with_font_size(mut self, font_size: impl Into<FontSize>) -> Self {
self.font_size = font_size.into();
self
}
pub const fn with_font_smoothing(mut self, font_smoothing: FontSmoothing) -> Self {
self.font_smoothing = font_smoothing;
self
}
pub fn with_font_weight(mut self, weight: impl Into<FontWeight>) -> Self {
self.weight = weight.into();
self
}
}
impl<T: Into<FontSource>> From<T> for TextFont {
fn from(source: T) -> Self {
Self {
font: source.into(),
..default()
}
}
}
impl Default for TextFont {
fn default() -> Self {
Self {
font: Default::default(),
font_size: FontSize::from(20.),
style: FontStyle::Normal,
weight: FontWeight::NORMAL,
width: FontWidth::NORMAL,
font_features: FontFeatures::default(),
font_variations: FontVariations::default(),
font_smoothing: Default::default(),
}
}
}
#[derive(Component, Copy, Clone, Debug, Reflect)]
pub enum FontSize {
Px(f32),
Vw(f32),
Vh(f32),
VMin(f32),
VMax(f32),
Rem(f32),
}
impl FontSize {
pub fn eval(
self,
logical_viewport_size: Vec2,
rem_size: f32,
) -> f32 {
match self {
FontSize::Px(s) => s,
FontSize::Vw(s) => logical_viewport_size.x * s / 100.,
FontSize::Vh(s) => logical_viewport_size.y * s / 100.,
FontSize::VMin(s) => logical_viewport_size.min_element() * s / 100.,
FontSize::VMax(s) => logical_viewport_size.max_element() * s / 100.,
FontSize::Rem(s) => rem_size * s,
}
}
}
impl PartialEq for FontSize {
fn eq(&self, other: &Self) -> bool {
match (*self, *other) {
(Self::Px(l), Self::Px(r))
| (Self::Vw(l), Self::Vw(r))
| (Self::Vh(l), Self::Vh(r))
| (Self::VMin(l), Self::VMin(r))
| (Self::VMax(l), Self::VMax(r))
| (Self::Rem(l), Self::Rem(r)) => l == r,
_ => false,
}
}
}
impl core::ops::Mul<f32> for FontSize {
type Output = FontSize;
fn mul(self, rhs: f32) -> Self::Output {
match self {
FontSize::Px(v) => FontSize::Px(v * rhs),
FontSize::Vw(v) => FontSize::Vw(v * rhs),
FontSize::Vh(v) => FontSize::Vh(v * rhs),
FontSize::VMin(v) => FontSize::VMin(v * rhs),
FontSize::VMax(v) => FontSize::VMax(v * rhs),
FontSize::Rem(v) => FontSize::Rem(v * rhs),
}
}
}
impl core::ops::Mul<FontSize> for f32 {
type Output = FontSize;
fn mul(self, rhs: FontSize) -> Self::Output {
rhs * self
}
}
impl Default for FontSize {
fn default() -> Self {
Self::Px(20.)
}
}
impl From<f32> for FontSize {
fn from(value: f32) -> Self {
Self::Px(value)
}
}
#[derive(Resource, Copy, Clone, Debug, PartialEq, Deref, DerefMut)]
pub struct RemSize(pub f32);
impl Default for RemSize {
fn default() -> Self {
Self(20.)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect)]
pub struct FontWeight(pub u16);
impl FontWeight {
pub const THIN: FontWeight = FontWeight(100);
pub const EXTRA_LIGHT: FontWeight = FontWeight(200);
pub const LIGHT: FontWeight = FontWeight(300);
pub const NORMAL: FontWeight = FontWeight(400);
pub const MEDIUM: FontWeight = FontWeight(500);
pub const SEMIBOLD: FontWeight = FontWeight(600);
pub const BOLD: FontWeight = FontWeight(700);
pub const EXTRA_BOLD: FontWeight = FontWeight(800);
pub const BLACK: FontWeight = FontWeight(900);
pub const EXTRA_BLACK: FontWeight = FontWeight(950);
pub const DEFAULT: FontWeight = Self::NORMAL;
pub const fn clamp(mut self) -> Self {
if self.0 == 0 {
self = Self::DEFAULT;
} else if 1000 < self.0 {
self.0 = 1000;
}
Self(self.0)
}
}
impl Default for FontWeight {
fn default() -> Self {
Self::DEFAULT
}
}
impl From<FontWeight> for parley::style::FontWeight {
fn from(value: FontWeight) -> Self {
parley::style::FontWeight::new(value.clamp().0 as f32)
}
}
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Debug, Hash, Reflect)]
pub struct FontWidth(u16);
impl FontWidth {
pub const ULTRA_CONDENSED: Self = Self(1);
pub const EXTRA_CONDENSED: Self = Self(2);
pub const CONDENSED: Self = Self(3);
pub const SEMI_CONDENSED: Self = Self(4);
pub const NORMAL: Self = Self(5);
pub const SEMI_EXPANDED: Self = Self(6);
pub const EXPANDED: Self = Self(7);
pub const EXTRA_EXPANDED: Self = Self(8);
pub const ULTRA_EXPANDED: Self = Self(9);
}
impl Default for FontWidth {
fn default() -> Self {
Self::NORMAL
}
}
impl From<FontWidth> for parley::FontWidth {
fn from(value: FontWidth) -> Self {
match value.0 {
1 => parley::FontWidth::ULTRA_CONDENSED,
2 => parley::FontWidth::EXTRA_CONDENSED,
3 => parley::FontWidth::CONDENSED,
4 => parley::FontWidth::SEMI_CONDENSED,
6 => parley::FontWidth::SEMI_EXPANDED,
7 => parley::FontWidth::EXPANDED,
8 => parley::FontWidth::EXTRA_EXPANDED,
9 => parley::FontWidth::ULTRA_EXPANDED,
_ => parley::FontWidth::NORMAL,
}
}
}
#[derive(Clone, Copy, Default, PartialEq, Debug, Reflect)]
pub enum FontStyle {
#[default]
Normal,
Italic,
Oblique(Option<f32>),
}
impl From<FontStyle> for parley::FontStyle {
fn from(value: FontStyle) -> Self {
match value {
FontStyle::Normal => parley::FontStyle::Normal,
FontStyle::Italic => parley::FontStyle::Italic,
FontStyle::Oblique(value) => parley::FontStyle::Oblique(value),
}
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect)]
pub struct FontFeatureTag([u8; 4]);
impl FontFeatureTag {
pub const STANDARD_LIGATURES: FontFeatureTag = FontFeatureTag::new(b"liga");
pub const CONTEXTUAL_LIGATURES: FontFeatureTag = FontFeatureTag::new(b"clig");
pub const DISCRETIONARY_LIGATURES: FontFeatureTag = FontFeatureTag::new(b"dlig");
pub const CONTEXTUAL_ALTERNATES: FontFeatureTag = FontFeatureTag::new(b"calt");
pub const STYLISTIC_ALTERNATES: FontFeatureTag = FontFeatureTag::new(b"salt");
pub const SMALL_CAPS: FontFeatureTag = FontFeatureTag::new(b"smcp");
pub const CAPS_TO_SMALL_CAPS: FontFeatureTag = FontFeatureTag::new(b"c2sc");
pub const SWASH: FontFeatureTag = FontFeatureTag::new(b"swsh");
pub const TITLING_ALTERNATES: FontFeatureTag = FontFeatureTag::new(b"titl");
pub const FRACTIONS: FontFeatureTag = FontFeatureTag::new(b"frac");
pub const ORDINALS: FontFeatureTag = FontFeatureTag::new(b"ordn");
pub const SLASHED_ZERO: FontFeatureTag = FontFeatureTag::new(b"zero");
pub const SUPERSCRIPT: FontFeatureTag = FontFeatureTag::new(b"sups");
pub const SUBSCRIPT: FontFeatureTag = FontFeatureTag::new(b"subs");
pub const OLDSTYLE_FIGURES: FontFeatureTag = FontFeatureTag::new(b"onum");
pub const LINING_FIGURES: FontFeatureTag = FontFeatureTag::new(b"lnum");
pub const PROPORTIONAL_FIGURES: FontFeatureTag = FontFeatureTag::new(b"pnum");
pub const TABULAR_FIGURES: FontFeatureTag = FontFeatureTag::new(b"tnum");
pub const WEIGHT: FontFeatureTag = FontFeatureTag::new(b"wght");
pub const WIDTH: FontFeatureTag = FontFeatureTag::new(b"wdth");
pub const SLANT: FontFeatureTag = FontFeatureTag::new(b"slnt");
pub const fn new(src: &[u8; 4]) -> Self {
Self(*src)
}
}
impl Debug for FontFeatureTag {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match from_utf8(&self.0) {
Ok(s) => write!(f, "FontFeatureTag(\"{}\")", s),
Err(_) => write!(f, "FontFeatureTag({:?})", self.0),
}
}
}
#[derive(Clone, Debug, Default, Reflect, PartialEq)]
pub struct FontFeatures {
features: Vec<(FontFeatureTag, u32)>,
}
impl FontFeatures {
pub fn builder() -> FontFeaturesBuilder {
FontFeaturesBuilder::default()
}
}
#[derive(Clone, Default)]
pub struct FontFeaturesBuilder {
features: Vec<(FontFeatureTag, u32)>,
}
impl FontFeaturesBuilder {
pub fn enable(self, feature_tag: FontFeatureTag) -> Self {
self.set(feature_tag, 1)
}
pub fn set(mut self, feature_tag: FontFeatureTag, value: u32) -> Self {
self.features.push((feature_tag, value));
self
}
pub fn build(self) -> FontFeatures {
FontFeatures {
features: self.features,
}
}
}
impl<T> From<T> for FontFeatures
where
T: IntoIterator<Item = FontFeatureTag>,
{
fn from(value: T) -> Self {
FontFeatures {
features: value.into_iter().map(|x| (x, 1)).collect(),
}
}
}
impl From<&FontFeatures> for parley::style::FontFeatures<'static> {
fn from(font_features: &FontFeatures) -> Self {
parley::style::FontFeatures::List(
font_features
.features
.iter()
.map(|(tag, value)| FontFeature {
tag: Tag::new(&tag.0),
value: *value as u16,
})
.collect(),
)
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect)]
pub struct FontVariationTag([u8; 4]);
impl FontVariationTag {
pub const WEIGHT: FontVariationTag = FontVariationTag::new(b"wght");
pub const WIDTH: FontVariationTag = FontVariationTag::new(b"wdth");
pub const SLANT: FontVariationTag = FontVariationTag::new(b"slnt");
pub const OPTICAL_SIZE: FontVariationTag = FontVariationTag::new(b"opsz");
pub const fn new(src: &[u8; 4]) -> Self {
Self(*src)
}
}
impl Debug for FontVariationTag {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match from_utf8(&self.0) {
Ok(s) => write!(f, "FontVariationTag(\"{}\")", s),
Err(_) => write!(f, "FontVariationTag({:?})", self.0),
}
}
}
#[derive(Clone, Debug, Default, Reflect, PartialEq)]
pub struct FontVariations {
variations: Vec<(FontVariationTag, f32)>,
}
impl FontVariations {
pub fn builder() -> FontVariationsBuilder {
FontVariationsBuilder::default()
}
}
#[derive(Clone, Default)]
pub struct FontVariationsBuilder {
variations: Vec<(FontVariationTag, f32)>,
}
impl FontVariationsBuilder {
pub fn set(mut self, tag: FontVariationTag, value: f32) -> Self {
self.variations.push((tag, value));
self
}
pub fn build(self) -> FontVariations {
FontVariations {
variations: self.variations,
}
}
}
impl From<&FontVariations> for parley::style::FontVariations<'static> {
fn from(font_variations: &FontVariations) -> Self {
parley::style::FontVariations::List(
font_variations
.variations
.iter()
.map(|(tag, value)| FontVariation {
tag: Tag::new(&tag.0),
value: *value,
})
.collect(),
)
}
}
#[derive(Component, Debug, Clone, Copy, PartialEq, Reflect)]
#[reflect(Component, Debug, Clone, PartialEq)]
pub enum LineHeight {
Px(f32),
RelativeToFont(f32),
}
impl LineHeight {
pub fn eval(self) -> parley::LineHeight {
match self {
LineHeight::Px(px) => parley::LineHeight::Absolute(px),
LineHeight::RelativeToFont(scale) => parley::LineHeight::FontSizeRelative(scale),
}
}
}
impl Default for LineHeight {
fn default() -> Self {
LineHeight::RelativeToFont(1.2)
}
}
#[derive(Component, Debug, Clone, Copy, PartialEq, Reflect)]
#[reflect(Component, Default, Debug, Clone, PartialEq)]
pub enum LetterSpacing {
Px(f32),
Rem(f32),
}
impl LetterSpacing {
pub(crate) fn eval(self, rem_size: f32) -> f32 {
match self {
LetterSpacing::Px(px) => px,
LetterSpacing::Rem(rem) => rem * rem_size,
}
}
}
impl Default for LetterSpacing {
fn default() -> Self {
Self::Px(0.0)
}
}
#[derive(Component, Copy, Clone, Debug, Deref, DerefMut, Reflect, PartialEq)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct TextColor(pub Color);
impl Default for TextColor {
fn default() -> Self {
Self::WHITE
}
}
impl<T: Into<Color>> From<T> for TextColor {
fn from(color: T) -> Self {
Self(color.into())
}
}
impl TextColor {
pub const BLACK: Self = TextColor(Color::BLACK);
pub const WHITE: Self = TextColor(Color::WHITE);
}
#[derive(Component, Copy, Clone, Debug, Deref, DerefMut, Reflect, PartialEq)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct TextBackgroundColor(pub Color);
impl Default for TextBackgroundColor {
fn default() -> Self {
Self(Color::BLACK)
}
}
impl<T: Into<Color>> From<T> for TextBackgroundColor {
fn from(color: T) -> Self {
Self(color.into())
}
}
impl TextBackgroundColor {
pub const BLACK: Self = TextBackgroundColor(Color::BLACK);
pub const WHITE: Self = TextBackgroundColor(Color::WHITE);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize)]
#[reflect(Serialize, Deserialize, Clone, PartialEq, Hash, Default)]
pub enum LineBreak {
#[default]
WordBoundary,
AnyCharacter,
WordOrCharacter,
NoWrap,
}
#[derive(Component, Copy, Clone, Debug, Reflect, Default, Serialize, Deserialize)]
#[reflect(Serialize, Deserialize, Clone, Default)]
pub struct Strikethrough;
#[derive(Component, Copy, Clone, Debug, Deref, DerefMut, Reflect, PartialEq)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct StrikethroughColor(pub Color);
impl Default for StrikethroughColor {
fn default() -> Self {
Self(Color::WHITE)
}
}
impl<T: Into<Color>> From<T> for StrikethroughColor {
fn from(color: T) -> Self {
Self(color.into())
}
}
#[derive(Component, Copy, Clone, Debug, Reflect, Default, Serialize, Deserialize)]
#[reflect(Serialize, Deserialize, Clone, Default)]
pub struct Underline;
#[derive(Component, Copy, Clone, Debug, Deref, DerefMut, Reflect, PartialEq)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct UnderlineColor(pub Color);
impl Default for UnderlineColor {
fn default() -> Self {
Self(Color::WHITE)
}
}
impl<T: Into<Color>> From<T> for UnderlineColor {
fn from(color: T) -> Self {
Self(color.into())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize)]
#[reflect(Serialize, Deserialize, Clone, PartialEq, Hash, Default)]
#[doc(alias = "antialiasing")]
#[doc(alias = "pixelated")]
pub enum FontSmoothing {
None,
#[default]
AntiAliased,
}
#[derive(Component, Debug, Copy, Clone, Default, Reflect, PartialEq, Hash, Eq)]
#[reflect(Component, Default, Debug, Clone, PartialEq)]
pub enum FontHinting {
#[default]
Disabled,
Enabled,
}
impl FontHinting {
pub fn is_enabled(self) -> bool {
matches!(self, FontHinting::Enabled)
}
}
pub fn detect_text_needs_rerender(
changed_roots: Query<
Entity,
(
Or<(
Changed<TextFont>,
Changed<TextLayout>,
Changed<LineHeight>,
Changed<LetterSpacing>,
Changed<Children>,
)>,
With<TextFont>,
With<TextLayout>,
),
>,
changed_spans: Query<
(Entity, Option<&ChildOf>, Has<TextLayout>),
(
Or<(
Changed<TextSpan>,
Changed<TextFont>,
Changed<LineHeight>,
Changed<LetterSpacing>,
Changed<Children>,
Changed<ChildOf>, // Included to detect broken text block hierarchies.
Added<TextLayout>,
)>,
With<TextSpan>,
With<TextFont>,
),
>,
mut computed: Query<(
Option<&ChildOf>,
Option<&mut ComputedTextBlock>,
Has<TextSpan>,
)>,
) {
for root in changed_roots.iter() {
let Ok((_, Some(mut computed), _)) = computed.get_mut(root) else {
once!(warn!("found entity {} with a root text component but no ComputedTextBlock; this warning only \
prints once", root));
continue;
};
computed.needs_rerender = true;
}
for (entity, maybe_span_child_of, has_text_block) in changed_spans.iter() {
if has_text_block {
once!(warn!("found entity {} with a TextSpan that has a TextLayout, which should only be on root \
text entities; this warning only prints once",
entity));
}
let Some(span_child_of) = maybe_span_child_of else {
once!(warn!(
"found entity {} with a TextSpan that has no parent; it should have an ancestor \
with a root text component; this warning only prints once",
entity
));
continue;
};
let mut parent: Entity = span_child_of.parent();
loop {
let Ok((maybe_child_of, maybe_computed, has_span)) = computed.get_mut(parent) else {
once!(warn!("found entity {} with a TextSpan that is part of a broken hierarchy with a ChildOf \
component that points at non-existent entity {}; this warning only prints once",
entity, parent));
break;
};
if let Some(mut computed) = maybe_computed {
computed.needs_rerender = true;
break;
}
if !has_span {
once!(warn!("found entity {} with a TextSpan that has an ancestor ({}) that does not have a text \
span component or a ComputedTextBlock component; this warning only prints once",
entity, parent));
break;
}
let Some(next_child_of) = maybe_child_of else {
once!(warn!(
"found entity {} with a TextSpan that has no ancestor with the root text \
component; this warning only prints once",
entity
));
break;
};
parent = next_child_of.parent();
}
}
}