use crate::{Font, TextLayoutInfo, TextSpanAccess, TextSpanComponent};
use bevy_asset::Handle;
use bevy_color::Color;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{prelude::*, reflect::ReflectComponent};
use bevy_reflect::prelude::*;
use bevy_utils::{default, once};
use core::fmt::{Debug, Formatter};
use core::str::from_utf8;
use cosmic_text::{Buffer, Metrics};
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use tracing::warn;
#[derive(Deref, DerefMut, Debug, Clone)]
pub struct CosmicBuffer(pub Buffer);
impl Default for CosmicBuffer {
fn default() -> Self {
Self(Buffer::new_empty(Metrics::new(20.0, 20.0)))
}
}
#[derive(Debug, Copy, Clone, Reflect)]
#[reflect(Debug, Clone)]
pub struct TextEntity {
pub entity: Entity,
pub depth: usize,
}
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Debug, Default, Clone)]
pub struct ComputedTextBlock {
#[reflect(ignore, clone)]
pub(crate) buffer: CosmicBuffer,
pub(crate) entities: SmallVec<[TextEntity; 1]>,
pub(crate) needs_rerender: bool,
}
impl ComputedTextBlock {
pub fn entities(&self) -> &[TextEntity] {
&self.entities
}
pub fn needs_rerender(&self) -> bool {
self.needs_rerender
}
pub fn buffer(&self) -> &CosmicBuffer {
&self.buffer
}
}
impl Default for ComputedTextBlock {
fn default() -> Self {
Self {
buffer: CosmicBuffer::default(),
entities: SmallVec::default(),
needs_rerender: true,
}
}
}
#[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 new_with_justify(justify: Justify) -> Self {
Self::default().with_justify(justify)
}
pub fn new_with_linebreak(linebreak: LineBreak) -> Self {
Self::default().with_linebreak(linebreak)
}
pub fn new_with_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)]
pub struct TextSpan(pub String);
impl TextSpan {
pub fn new(text: impl Into<String>) -> Self {
Self(text.into())
}
}
impl TextSpanComponent for TextSpan {}
impl TextSpanAccess for TextSpan {
fn read_span(&self) -> &str {
self.as_str()
}
fn write_span(&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,
}
impl From<Justify> for cosmic_text::Align {
fn from(justify: Justify) -> Self {
match justify {
Justify::Left => cosmic_text::Align::Left,
Justify::Center => cosmic_text::Align::Center,
Justify::Right => cosmic_text::Align::Right,
Justify::Justified => cosmic_text::Align::Justified,
}
}
}
#[derive(Component, Clone, Debug, Reflect, PartialEq)]
#[reflect(Component, Default, Debug, Clone)]
pub struct TextFont {
pub font: Handle<Font>,
pub font_size: f32,
pub weight: FontWeight,
pub font_smoothing: FontSmoothing,
pub font_features: FontFeatures,
}
impl TextFont {
pub fn from_font_size(font_size: f32) -> Self {
Self::default().with_font_size(font_size)
}
pub fn with_font(mut self, font: Handle<Font>) -> Self {
self.font = font;
self
}
pub const fn with_font_size(mut self, font_size: f32) -> Self {
self.font_size = font_size;
self
}
pub const fn with_font_smoothing(mut self, font_smoothing: FontSmoothing) -> Self {
self.font_smoothing = font_smoothing;
self
}
}
impl From<Handle<Font>> for TextFont {
fn from(font: Handle<Font>) -> Self {
Self { font, ..default() }
}
}
impl Default for TextFont {
fn default() -> Self {
Self {
font: Default::default(),
font_size: 20.0,
weight: FontWeight::NORMAL,
font_features: FontFeatures::default(),
font_smoothing: Default::default(),
}
}
}
#[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 cosmic_text::Weight {
fn from(value: FontWeight) -> Self {
cosmic_text::Weight(value.clamp().0)
}
}
#[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"ordn");
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 cosmic_text::FontFeatures {
fn from(font_features: &FontFeatures) -> Self {
cosmic_text::FontFeatures {
features: font_features
.features
.iter()
.map(|(tag, value)| cosmic_text::Feature {
tag: cosmic_text::FeatureTag::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(crate) fn eval(self, font_size: f32) -> f32 {
match self {
LineHeight::Px(px) => px,
LineHeight::RelativeToFont(scale) => scale * font_size,
}
}
}
impl Default for LineHeight {
fn default() -> Self {
LineHeight::RelativeToFont(1.2)
}
}
#[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,
}
pub fn detect_text_needs_rerender<Root: Component>(
changed_roots: Query<
Entity,
(
Or<(
Changed<Root>,
Changed<TextFont>,
Changed<TextLayout>,
Changed<LineHeight>,
Changed<Children>,
)>,
With<Root>,
With<TextFont>,
With<TextLayout>,
),
>,
changed_spans: Query<
(Entity, Option<&ChildOf>, Has<TextLayout>),
(
Or<(
Changed<TextSpan>,
Changed<TextFont>,
Changed<LineHeight>,
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, core::any::type_name::<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 (that have {}); this warning only prints once",
entity, core::any::type_name::<Root>()));
}
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,
core::any::type_name::<Root>()
));
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,
core::any::type_name::<Root>()
));
break;
};
parent = next_child_of.parent();
}
}
}
#[derive(Component, Debug, Copy, Clone, Default, Reflect, PartialEq)]
#[reflect(Component, Default, Debug, Clone, PartialEq)]
pub enum FontHinting {
#[default]
Disabled,
Enabled,
}
impl From<FontHinting> for cosmic_text::Hinting {
fn from(value: FontHinting) -> Self {
match value {
FontHinting::Disabled => cosmic_text::Hinting::Disabled,
FontHinting::Enabled => cosmic_text::Hinting::Enabled,
}
}
}