use cosmic_text::Color as CosmicColor;
use crate::decoration::{TextDecoration, UnderlineStyle, StrikethroughStyle};
use crate::effects::{TextEffect, TextEffects};
use crate::font::{FontAttributes, FontStretch, FontStyle, FontWeight};
use crate::sdf::TextRenderMode;
use astrelis_core::math::Vec2;
use astrelis_render::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextAlign {
Left,
Center,
Right,
Justified,
}
impl TextAlign {
pub(crate) fn to_cosmic(self) -> cosmic_text::Align {
match self {
TextAlign::Left => cosmic_text::Align::Left,
TextAlign::Center => cosmic_text::Align::Center,
TextAlign::Right => cosmic_text::Align::Right,
TextAlign::Justified => cosmic_text::Align::Justified,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Default)]
pub enum VerticalAlign {
#[default]
Top,
Center,
Bottom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TextWrap {
None,
#[default]
Word,
Glyph,
WordOrGlyph,
}
impl TextWrap {
pub(crate) fn to_cosmic(self) -> cosmic_text::Wrap {
match self {
TextWrap::None => cosmic_text::Wrap::None,
TextWrap::Word => cosmic_text::Wrap::Word,
TextWrap::Glyph => cosmic_text::Wrap::Glyph,
TextWrap::WordOrGlyph => cosmic_text::Wrap::WordOrGlyph,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct LineBreakConfig {
pub wrap: TextWrap,
pub break_at_hyphens: bool,
_uax14_reserved: (),
}
impl LineBreakConfig {
pub fn new(wrap: TextWrap) -> Self {
Self {
wrap,
break_at_hyphens: true,
_uax14_reserved: (),
}
}
pub fn with_hyphen_breaks(mut self, allow: bool) -> Self {
self.break_at_hyphens = allow;
self
}
pub fn wrap(&self) -> TextWrap {
self.wrap
}
pub fn breaks_at_hyphens(&self) -> bool {
self.break_at_hyphens
}
}
pub(crate) fn color_to_cosmic(color: Color) -> CosmicColor {
CosmicColor::rgba(
(color.r * 255.0) as u8,
(color.g * 255.0) as u8,
(color.b * 255.0) as u8,
(color.a * 255.0) as u8,
)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TextMetrics {
pub ascent: f32,
pub descent: f32,
pub line_height: f32,
pub baseline_offset: f32,
}
pub struct Text {
content: String,
font_size: f32,
line_height: f32,
font_attrs: FontAttributes,
color: Color,
align: TextAlign,
vertical_align: VerticalAlign,
wrap: TextWrap,
max_width: Option<f32>,
max_height: Option<f32>,
letter_spacing: f32,
word_spacing: f32,
break_at_hyphens: bool,
effects: Option<TextEffects>,
render_mode: Option<TextRenderMode>,
decoration: Option<TextDecoration>,
}
impl Text {
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
font_size: 16.0,
line_height: 1.2,
font_attrs: FontAttributes::default(),
color: Color::WHITE,
align: TextAlign::Left,
vertical_align: VerticalAlign::Top,
wrap: TextWrap::Word,
max_width: None,
max_height: None,
letter_spacing: 0.0,
word_spacing: 0.0,
break_at_hyphens: true,
effects: None,
render_mode: None,
decoration: None,
}
}
pub fn size(mut self, size: f32) -> Self {
self.font_size = size;
self
}
pub fn line_height(mut self, height: f32) -> Self {
self.line_height = height;
self
}
pub fn font(mut self, family: impl Into<String>) -> Self {
self.font_attrs.family = family.into();
self
}
pub fn weight(mut self, weight: FontWeight) -> Self {
self.font_attrs.weight = weight;
self
}
pub fn style(mut self, style: FontStyle) -> Self {
self.font_attrs.style = style;
self
}
pub fn stretch(mut self, stretch: FontStretch) -> Self {
self.font_attrs.stretch = stretch;
self
}
pub fn font_attrs(mut self, attrs: FontAttributes) -> Self {
self.font_attrs = attrs;
self
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn align(mut self, align: TextAlign) -> Self {
self.align = align;
self
}
pub fn vertical_align(mut self, vertical_align: VerticalAlign) -> Self {
self.vertical_align = vertical_align;
self
}
pub fn wrap(mut self, wrap: TextWrap) -> Self {
self.wrap = wrap;
self
}
pub fn line_break(mut self, config: LineBreakConfig) -> Self {
self.wrap = config.wrap;
self.break_at_hyphens = config.break_at_hyphens;
self
}
pub fn max_width(mut self, width: f32) -> Self {
self.max_width = Some(width);
self
}
pub fn max_height(mut self, height: f32) -> Self {
self.max_height = Some(height);
self
}
pub fn letter_spacing(mut self, spacing: f32) -> Self {
self.letter_spacing = spacing;
self
}
pub fn word_spacing(mut self, spacing: f32) -> Self {
self.word_spacing = spacing;
self
}
pub fn bold(self) -> Self {
self.weight(FontWeight::Bold)
}
pub fn italic(self) -> Self {
self.style(FontStyle::Italic)
}
pub fn get_content(&self) -> &str {
&self.content
}
pub fn get_font_size(&self) -> f32 {
self.font_size
}
pub fn get_line_height(&self) -> f32 {
self.line_height
}
pub fn get_font_attrs(&self) -> &FontAttributes {
&self.font_attrs
}
pub fn get_color(&self) -> Color {
self.color
}
pub fn get_align(&self) -> TextAlign {
self.align
}
pub fn get_vertical_align(&self) -> VerticalAlign {
self.vertical_align
}
pub fn get_wrap(&self) -> TextWrap {
self.wrap
}
pub fn get_max_width(&self) -> Option<f32> {
self.max_width
}
pub fn get_max_height(&self) -> Option<f32> {
self.max_height
}
pub fn get_letter_spacing(&self) -> f32 {
self.letter_spacing
}
pub fn get_word_spacing(&self) -> f32 {
self.word_spacing
}
pub fn get_break_at_hyphens(&self) -> bool {
self.break_at_hyphens
}
pub fn with_effect(mut self, effect: TextEffect) -> Self {
let effects = self.effects.get_or_insert_with(TextEffects::new);
effects.add(effect);
self
}
pub fn with_effects(mut self, effects: TextEffects) -> Self {
self.effects = Some(effects);
self
}
pub fn with_shadow(self, offset: Vec2, color: Color) -> Self {
self.with_effect(TextEffect::shadow(offset, color))
}
pub fn with_shadow_blurred(self, offset: Vec2, blur_radius: f32, color: Color) -> Self {
self.with_effect(TextEffect::shadow_blurred(offset, blur_radius, color))
}
pub fn with_outline(self, width: f32, color: Color) -> Self {
self.with_effect(TextEffect::outline(width, color))
}
pub fn with_glow(self, radius: f32, color: Color, intensity: f32) -> Self {
self.with_effect(TextEffect::glow(radius, color, intensity))
}
pub fn render_mode(mut self, mode: TextRenderMode) -> Self {
self.render_mode = Some(mode);
self
}
pub fn sdf(self) -> Self {
self.render_mode(TextRenderMode::SDF { spread: 4.0 })
}
pub fn get_effects(&self) -> Option<&TextEffects> {
self.effects.as_ref()
}
pub fn get_render_mode(&self) -> Option<TextRenderMode> {
self.render_mode
}
pub fn has_effects(&self) -> bool {
self.effects
.as_ref()
.map(|e| e.has_enabled_effects())
.unwrap_or(false)
}
pub fn with_decoration(mut self, decoration: TextDecoration) -> Self {
self.decoration = Some(decoration);
self
}
pub fn underline(self, color: Color) -> Self {
let decoration = self.decoration.clone().unwrap_or_default()
.underline(UnderlineStyle::solid(color, 1.0));
self.with_decoration(decoration)
}
pub fn strikethrough(self, color: Color) -> Self {
let decoration = self.decoration.clone().unwrap_or_default()
.strikethrough(StrikethroughStyle::solid(color, 1.0));
self.with_decoration(decoration)
}
pub fn background_color(self, color: Color) -> Self {
let decoration = self.decoration.clone().unwrap_or_default()
.background(color);
self.with_decoration(decoration)
}
pub fn get_decoration(&self) -> Option<&TextDecoration> {
self.decoration.as_ref()
}
pub fn has_decoration(&self) -> bool {
self.decoration
.as_ref()
.map(|d| d.has_decoration())
.unwrap_or(false)
}
pub fn effective_render_mode(&self) -> TextRenderMode {
if let Some(mode) = self.render_mode {
return mode;
}
if self.has_effects() || self.font_size >= 24.0 {
TextRenderMode::SDF { spread: 4.0 }
} else {
TextRenderMode::Bitmap
}
}
}
impl Default for Text {
fn default() -> Self {
Self::new("")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_with_effect() {
use crate::effects::TextEffect;
let text = Text::new("Hello")
.with_effect(TextEffect::shadow(Vec2::new(1.0, 1.0), Color::BLACK));
assert!(text.has_effects());
assert_eq!(text.get_effects().unwrap().effects().len(), 1);
}
#[test]
fn test_text_with_shadow() {
let text = Text::new("Hello")
.with_shadow(Vec2::new(2.0, 2.0), Color::BLACK);
assert!(text.has_effects());
let effects = text.get_effects().unwrap();
assert_eq!(effects.effects().len(), 1);
}
#[test]
fn test_text_with_shadow_blurred() {
let text = Text::new("Hello")
.with_shadow_blurred(Vec2::new(2.0, 2.0), 1.5, Color::BLACK);
assert!(text.has_effects());
}
#[test]
fn test_text_with_outline() {
let text = Text::new("Hello")
.with_outline(1.0, Color::WHITE);
assert!(text.has_effects());
}
#[test]
fn test_text_with_glow() {
let text = Text::new("Hello")
.with_glow(5.0, Color::BLUE, 0.8);
assert!(text.has_effects());
}
#[test]
fn test_text_with_multiple_effects() {
let text = Text::new("Hello")
.with_shadow(Vec2::new(1.0, 1.0), Color::BLACK)
.with_outline(1.0, Color::WHITE)
.with_glow(3.0, Color::BLUE, 0.5);
assert!(text.has_effects());
let effects = text.get_effects().unwrap();
assert_eq!(effects.effects().len(), 3);
}
#[test]
fn test_text_render_mode_explicit() {
let text = Text::new("Hello")
.render_mode(TextRenderMode::SDF { spread: 6.0 });
assert_eq!(text.get_render_mode(), Some(TextRenderMode::SDF { spread: 6.0 }));
}
#[test]
fn test_text_sdf() {
let text = Text::new("Hello").sdf();
assert!(text.get_render_mode().is_some());
assert!(text.get_render_mode().unwrap().is_sdf());
}
#[test]
fn test_text_effective_render_mode_small_no_effects() {
let text = Text::new("Hello").size(12.0);
let mode = text.effective_render_mode();
assert_eq!(mode, TextRenderMode::Bitmap);
}
#[test]
fn test_text_effective_render_mode_large_no_effects() {
let text = Text::new("Hello").size(32.0);
let mode = text.effective_render_mode();
assert!(mode.is_sdf());
}
#[test]
fn test_text_effective_render_mode_small_with_effects() {
let text = Text::new("Hello")
.size(12.0)
.with_shadow(Vec2::new(1.0, 1.0), Color::BLACK);
let mode = text.effective_render_mode();
assert!(mode.is_sdf());
}
#[test]
fn test_text_effective_render_mode_explicit_overrides() {
let text = Text::new("Hello")
.size(12.0)
.with_shadow(Vec2::new(1.0, 1.0), Color::BLACK)
.render_mode(TextRenderMode::Bitmap);
let mode = text.effective_render_mode();
assert_eq!(mode, TextRenderMode::Bitmap);
}
#[test]
fn test_text_has_effects_false() {
let text = Text::new("Hello");
assert!(!text.has_effects());
}
#[test]
fn test_text_has_effects_true() {
let text = Text::new("Hello")
.with_shadow(Vec2::new(1.0, 1.0), Color::BLACK);
assert!(text.has_effects());
}
#[test]
fn test_text_has_effects_disabled() {
use crate::effects::{TextEffect, TextEffects};
let mut effects = TextEffects::new();
let mut effect = TextEffect::shadow(Vec2::new(1.0, 1.0), Color::BLACK);
effect.set_enabled(false);
effects.add(effect);
let text = Text::new("Hello").with_effects(effects);
assert!(!text.has_effects());
}
#[test]
fn test_text_builder_chaining() {
let text = Text::new("Hello World")
.size(24.0)
.color(Color::RED)
.bold()
.with_shadow(Vec2::new(2.0, 2.0), Color::BLACK)
.with_outline(1.0, Color::WHITE)
.sdf();
assert_eq!(text.get_font_size(), 24.0);
assert_eq!(text.get_color(), Color::RED);
assert!(text.has_effects());
assert!(text.get_render_mode().unwrap().is_sdf());
}
#[test]
fn test_text_effective_render_mode_boundary() {
let text_at_boundary = Text::new("Hello").size(24.0);
assert!(text_at_boundary.effective_render_mode().is_sdf());
let text_below = Text::new("Hello").size(23.9);
assert!(!text_below.effective_render_mode().is_sdf());
let text_above = Text::new("Hello").size(24.1);
assert!(text_above.effective_render_mode().is_sdf());
}
}