bevy_rich_text3d 0.7.0

Mesh based raster rich text implementation for bevy.
Documentation
use crate::{prepare::family, MeshExport, StrokeJoin, Style, TextAlign, TextAnchor, Weight};
use bevy::asset::AssetId;
use bevy::image::Image;
use bevy::{color::Srgba, ecs::component::Component, math::Vec2};
use cosmic_text::Metrics;
use cosmic_text::{fontdb::ID, Attrs};
use std::fmt::Debug;
use std::{num::NonZeroU32, sync::Arc};

#[cfg(feature = "reflect")]
use bevy::prelude::{Reflect, ReflectComponent, ReflectDefault};

/// Default text style of a rich text component.
#[derive(Debug, Component, Clone)]
#[cfg_attr(feature = "reflect", derive(Reflect))]
#[cfg_attr(feature = "reflect", reflect(Component, Default))]
pub struct Text3dStyling {
    /// Size of the font, corresponding to world space units.
    ///
    /// Ths is cached per unique value so keep the amount of unique values of `size` low,
    /// use [`Text3dStyling::world_scale`] instead for small variations.
    pub size: f32,
    /// Name of the font, by default `"serif"`.
    ///
    /// For `"serif"`, `"sans-serif"`, `"monospace"`, `"cursive"` and `"fantasy"`,
    /// use one of the default fonts set in `cosmic_text`.
    pub font: Arc<str>,
    /// Style of the font, i.e. italic.
    pub style: Style,
    /// Weight of the font, i.e. bold.
    pub weight: Weight,
    /// Horizontal alignment of the font.
    pub align: TextAlign,
    /// Where local `[0, 0]` is inside the text block's Aabb.
    pub anchor: TextAnchor,
    /// Height of a line multiplied by font size, by default `1.0`.
    pub line_height: f32,
    /// Color of fill.
    pub color: Srgba,
    /// Color of stroke.
    pub stroke_color: Srgba,
    /// If not set, will not render the fill of the font, making it hollow.
    pub fill: bool,
    /// If set, renders a stroke or outline of the font.
    ///
    /// The value represents a percentage of the font size and should be
    /// in `1..10` for hollow text and `1..20` for outline.
    ///
    /// Ths is cached per unique value so be sure not to use too many of them.
    pub stroke: Option<NonZeroU32>,
    /// If true, render stroke in front.
    pub stroke_in_front: bool,
    /// The shape of the stroke line joins, usually [`StrokeJoin::Round`].
    pub stroke_join: StrokeJoin,
    /// Sets the distance between different layers, i.e. stroke and fill.
    ///
    /// `0.0` likely works for transparent render modes and opaque2d, but
    /// not for opaque3d.
    pub layer_offset: f32,
    /// Determines what to extract as uv1.
    pub export: MeshExport,
    /// Tab in terms of spaces, default 4.
    pub tab_width: u16,
    /// If set, overwrite the size of `em` in the generated mesh.
    ///
    /// By default the mesh size is relative to [`Text3dStyling::size`], which is equivalent to `Some((size, size))`.
    pub world_scale: Option<Vec2>,
    /// If `Some`, render a text shadow.
    pub text_shadow: Option<(Srgba, Vec2)>,
}

impl Default for Text3dStyling {
    fn default() -> Self {
        Self {
            size: 16.,
            color: Srgba::WHITE,
            font: Default::default(),
            style: Default::default(),
            weight: Default::default(),
            align: Default::default(),
            anchor: TextAnchor::CENTER,
            stroke_color: Srgba::WHITE,
            fill: true,
            stroke: Default::default(),
            line_height: 1.0,
            layer_offset: 0.01,
            stroke_in_front: false,
            stroke_join: StrokeJoin::Round,
            export: MeshExport::None,
            tab_width: 4,
            world_scale: None,
            text_shadow: None,
        }
    }
}

/// Size of a segment.
#[derive(Debug, Clone, Copy)]
pub enum SegmentSize {
    Flat(f32),
    Multiply(f32),
}

/// Text style of a segment.
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "reflect", derive(Reflect))]
pub struct SegmentStyle {
    pub font: Option<Arc<str>>,
    pub size: Option<SegmentSize>,
    pub fill_color: Option<Srgba>,
    pub stroke_color: Option<Srgba>,
    pub fill: Option<bool>,
    pub stroke: Option<NonZeroU32>,
    pub weight: Option<Weight>,
    pub style: Option<Style>,
    pub underline: Option<bool>,
    pub strikethrough: Option<bool>,
    /// Can be referenced by [`GlyphMeta::MagicNumber`](crate::GlyphMeta::MagicNumber).
    pub magic_number: Option<f32>,
}

impl SegmentStyle {
    pub fn as_attr<'t>(&'t self, base: &'t Text3dStyling) -> Attrs<'t> {
        let family_name = self.font.as_ref().map(Arc::as_ref).unwrap_or(&base.font);
        let family = family(family_name);

        match self.size {
            Some(SegmentSize::Flat(size)) => {
                Attrs::new().metrics(Metrics::new(size, size * base.line_height))
            }
            Some(SegmentSize::Multiply(size)) => {
                let size = base.size * size;
                Attrs::new().metrics(Metrics::new(size, size * base.line_height))
            }
            None => Attrs::new(),
        }
        .weight(self.weight.unwrap_or(base.weight).into())
        .style(self.style.unwrap_or(base.style).into())
        .family(family)
    }

    pub fn join(&self, other: Self) -> Self {
        SegmentStyle {
            font: other.font.or_else(|| self.font.clone()),
            size: other.size.or(self.size),
            fill_color: other.fill_color.or(self.fill_color),
            stroke_color: other.stroke_color.or(self.stroke_color),
            fill: other.fill.or(self.fill),
            stroke: other.stroke.or(self.stroke),
            weight: other.weight.or(self.weight),
            underline: other.underline.or(self.underline),
            strikethrough: other.strikethrough.or(self.strikethrough),
            style: other.style.or(self.style),
            magic_number: other.magic_number.or(self.magic_number),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GlyphEntry {
    Glyph {
        font: ID,
        glyph_id: GlyphTextureOf,
        join: StrokeJoin,
        /// Multiplied by scale factor.
        real_size: FloatDecimal,
        weight: Weight,
        stroke: Option<NonZeroU32>,
    },
    Image(AssetId<Image>),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GlyphTextureOf {
    Id(u16),
    UnderlineTexture,
    StrikethroughTexture,
}

impl From<u16> for GlyphTextureOf {
    fn from(id: u16) -> Self {
        GlyphTextureOf::Id(id)
    }
}

/// A floating point hashmap key
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct FloatDecimal(i32);

impl FloatDecimal {
    pub fn new(input: f32) -> Self {
        Self((input * 100.).round() as i32)
    }
}

impl Debug for FloatDecimal {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let value = self.0 as f32 / 100.;
        f.debug_tuple("FloatDecimal").field(&value).finish()
    }
}