use crate::font::{FontAttributes, FontStyle, FontWeight};
use crate::text::{LineBreakConfig, Text, TextAlign, TextWrap, VerticalAlign};
use astrelis_render::Color;
#[derive(Debug, Clone)]
pub struct TextSpan {
pub text: String,
pub style: TextSpanStyle,
}
impl TextSpan {
pub fn new(text: impl Into<String>, style: TextSpanStyle) -> Self {
Self {
text: text.into(),
style,
}
}
}
#[derive(Debug, Clone)]
#[derive(Default)]
pub struct TextSpanStyle {
pub font_size: Option<f32>,
pub color: Option<Color>,
pub weight: Option<FontWeight>,
pub style: Option<FontStyle>,
pub font_family: Option<String>,
pub underline: bool,
pub strikethrough: bool,
pub background: Option<Color>,
pub scale: Option<f32>,
}
impl TextSpanStyle {
pub fn new() -> Self {
Self::default()
}
pub fn with_size(mut self, size: f32) -> Self {
self.font_size = Some(size);
self
}
pub fn with_color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn with_weight(mut self, weight: FontWeight) -> Self {
self.weight = Some(weight);
self
}
pub fn bold(mut self) -> Self {
self.weight = Some(FontWeight::Bold);
self
}
pub fn with_style(mut self, style: FontStyle) -> Self {
self.style = Some(style);
self
}
pub fn italic(mut self) -> Self {
self.style = Some(FontStyle::Italic);
self
}
pub fn with_family(mut self, family: impl Into<String>) -> Self {
self.font_family = Some(family.into());
self
}
pub fn with_underline(mut self, underline: bool) -> Self {
self.underline = underline;
self
}
pub fn with_strikethrough(mut self, strikethrough: bool) -> Self {
self.strikethrough = strikethrough;
self
}
pub fn with_background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
pub fn with_scale(mut self, scale: f32) -> Self {
self.scale = Some(scale);
self
}
}
#[derive(Debug, Clone)]
pub struct RichText {
spans: Vec<TextSpan>,
default_font_size: f32,
default_color: Color,
default_font_attrs: FontAttributes,
align: TextAlign,
vertical_align: VerticalAlign,
wrap: TextWrap,
break_at_hyphens: bool,
max_width: Option<f32>,
max_height: Option<f32>,
line_height: f32,
}
impl RichText {
pub fn new() -> Self {
Self {
spans: Vec::new(),
default_font_size: 16.0,
default_color: Color::WHITE,
default_font_attrs: FontAttributes::default(),
align: TextAlign::Left,
vertical_align: VerticalAlign::Top,
wrap: TextWrap::Word,
break_at_hyphens: true,
max_width: None,
max_height: None,
line_height: 1.2,
}
}
pub fn push(&mut self, text: impl Into<String>, style: TextSpanStyle) {
self.spans.push(TextSpan::new(text, style));
}
pub fn push_str(&mut self, text: impl Into<String>) {
self.spans
.push(TextSpan::new(text, TextSpanStyle::default()));
}
pub fn push_bold(&mut self, text: impl Into<String>) {
self.spans
.push(TextSpan::new(text, TextSpanStyle::default().bold()));
}
pub fn push_italic(&mut self, text: impl Into<String>) {
self.spans
.push(TextSpan::new(text, TextSpanStyle::default().italic()));
}
pub fn push_colored(&mut self, text: impl Into<String>, color: Color) {
self.spans.push(TextSpan::new(
text,
TextSpanStyle::default().with_color(color),
));
}
pub fn push_span(&mut self, span: TextSpan) {
self.spans.push(span);
}
pub fn spans(&self) -> &[TextSpan] {
&self.spans
}
pub fn set_default_font_size(&mut self, size: f32) {
self.default_font_size = size;
}
pub fn set_default_color(&mut self, color: Color) {
self.default_color = color;
}
pub fn set_default_font_attrs(&mut self, attrs: FontAttributes) {
self.default_font_attrs = attrs;
}
pub fn set_align(&mut self, align: TextAlign) {
self.align = align;
}
pub fn set_vertical_align(&mut self, align: VerticalAlign) {
self.vertical_align = align;
}
pub fn set_wrap(&mut self, wrap: TextWrap) {
self.wrap = wrap;
}
pub fn set_line_break(&mut self, config: LineBreakConfig) {
self.wrap = config.wrap;
self.break_at_hyphens = config.break_at_hyphens;
}
pub fn set_max_width(&mut self, width: Option<f32>) {
self.max_width = width;
}
pub fn set_max_height(&mut self, height: Option<f32>) {
self.max_height = height;
}
pub fn set_line_height(&mut self, height: f32) {
self.line_height = height;
}
pub fn full_text(&self) -> String {
self.spans.iter().map(|s| s.text.as_str()).collect()
}
pub fn to_text_segments(&self) -> Vec<(Text, TextSpanStyle)> {
let mut segments = Vec::new();
for span in &self.spans {
let mut text = Text::new(&span.text)
.size(
span.style
.font_size
.or(span.style.scale.map(|s| self.default_font_size * s))
.unwrap_or(self.default_font_size),
)
.color(span.style.color.unwrap_or(self.default_color))
.align(self.align)
.vertical_align(self.vertical_align)
.wrap(self.wrap)
.line_height(self.line_height);
if let Some(weight) = span.style.weight {
text = text.weight(weight);
} else {
text = text.weight(self.default_font_attrs.weight);
}
if let Some(style) = span.style.style {
text = text.style(style);
} else {
text = text.style(self.default_font_attrs.style);
}
if let Some(ref family) = span.style.font_family {
text = text.font(family.clone());
} else if !self.default_font_attrs.family.is_empty() {
text = text.font(self.default_font_attrs.family.clone());
}
if let Some(width) = self.max_width {
text = text.max_width(width);
}
if let Some(height) = self.max_height {
text = text.max_height(height);
}
segments.push((text, span.style.clone()));
}
segments
}
pub fn from_markup(markup: &str) -> Self {
let mut rich = RichText::new();
let mut current = String::new();
let mut chars = markup.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'*' => {
if chars.peek() == Some(&'*') {
chars.next();
if !current.is_empty() {
rich.push_str(current.clone());
current.clear();
}
let mut bold_text = String::new();
let mut found_end = false;
while let Some(ch) = chars.next() {
if ch == '*' && chars.peek() == Some(&'*') {
chars.next(); found_end = true;
break;
}
bold_text.push(ch);
}
if found_end {
rich.push_bold(bold_text);
} else {
current.push_str("**");
current.push_str(&bold_text);
}
} else {
if !current.is_empty() {
rich.push_str(current.clone());
current.clear();
}
let mut italic_text = String::new();
let mut found_end = false;
for ch in chars.by_ref() {
if ch == '*' {
found_end = true;
break;
}
italic_text.push(ch);
}
if found_end {
rich.push_italic(italic_text);
} else {
current.push('*');
current.push_str(&italic_text);
}
}
}
'_' => {
if chars.peek() == Some(&'_') {
chars.next();
if !current.is_empty() {
rich.push_str(current.clone());
current.clear();
}
let mut underline_text = String::new();
let mut found_end = false;
while let Some(ch) = chars.next() {
if ch == '_' && chars.peek() == Some(&'_') {
chars.next(); found_end = true;
break;
}
underline_text.push(ch);
}
if found_end {
rich.push(
underline_text,
TextSpanStyle::default().with_underline(true),
);
} else {
current.push_str("__");
current.push_str(&underline_text);
}
} else {
current.push(ch);
}
}
'~' => {
if chars.peek() == Some(&'~') {
chars.next();
if !current.is_empty() {
rich.push_str(current.clone());
current.clear();
}
let mut strike_text = String::new();
let mut found_end = false;
while let Some(ch) = chars.next() {
if ch == '~' && chars.peek() == Some(&'~') {
chars.next(); found_end = true;
break;
}
strike_text.push(ch);
}
if found_end {
rich.push(
strike_text,
TextSpanStyle::default().with_strikethrough(true),
);
} else {
current.push_str("~~");
current.push_str(&strike_text);
}
} else {
current.push(ch);
}
}
_ => {
current.push(ch);
}
}
}
if !current.is_empty() {
rich.push_str(current);
}
rich
}
}
impl Default for RichText {
fn default() -> Self {
Self::new()
}
}
pub struct RichTextBuilder {
rich_text: RichText,
}
impl RichTextBuilder {
pub fn new() -> Self {
Self {
rich_text: RichText::new(),
}
}
pub fn text(mut self, text: impl Into<String>) -> Self {
self.rich_text.push_str(text);
self
}
pub fn bold(mut self, text: impl Into<String>) -> Self {
self.rich_text.push_bold(text);
self
}
pub fn italic(mut self, text: impl Into<String>) -> Self {
self.rich_text.push_italic(text);
self
}
pub fn colored(mut self, text: impl Into<String>, color: Color) -> Self {
self.rich_text.push_colored(text, color);
self
}
pub fn span(mut self, text: impl Into<String>, style: TextSpanStyle) -> Self {
self.rich_text.push(text, style);
self
}
pub fn default_size(mut self, size: f32) -> Self {
self.rich_text.set_default_font_size(size);
self
}
pub fn default_color(mut self, color: Color) -> Self {
self.rich_text.set_default_color(color);
self
}
pub fn align(mut self, align: TextAlign) -> Self {
self.rich_text.set_align(align);
self
}
pub fn vertical_align(mut self, align: VerticalAlign) -> Self {
self.rich_text.set_vertical_align(align);
self
}
pub fn wrap(mut self, wrap: TextWrap) -> Self {
self.rich_text.set_wrap(wrap);
self
}
pub fn line_break(mut self, config: LineBreakConfig) -> Self {
self.rich_text.set_line_break(config);
self
}
pub fn max_width(mut self, width: f32) -> Self {
self.rich_text.set_max_width(Some(width));
self
}
pub fn max_height(mut self, height: f32) -> Self {
self.rich_text.set_max_height(Some(height));
self
}
pub fn line_height(mut self, height: f32) -> Self {
self.rich_text.set_line_height(height);
self
}
pub fn build(self) -> RichText {
self.rich_text
}
}
impl Default for RichTextBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rich_text_builder() {
let rich = RichTextBuilder::new()
.text("This is ")
.bold("bold")
.text(" and ")
.italic("italic")
.text(" text.")
.build();
assert_eq!(rich.spans().len(), 5);
assert_eq!(rich.full_text(), "This is bold and italic text.");
}
#[test]
fn test_markup_parsing_bold() {
let rich = RichText::from_markup("This is **bold** text.");
assert_eq!(rich.spans().len(), 3);
assert_eq!(rich.full_text(), "This is bold text.");
assert!(rich.spans()[1].style.weight == Some(FontWeight::Bold));
}
#[test]
fn test_markup_parsing_italic() {
let rich = RichText::from_markup("This is *italic* text.");
assert_eq!(rich.spans().len(), 3);
assert_eq!(rich.full_text(), "This is italic text.");
assert!(rich.spans()[1].style.style == Some(FontStyle::Italic));
}
#[test]
fn test_markup_parsing_underline() {
let rich = RichText::from_markup("This is __underlined__ text.");
assert_eq!(rich.spans().len(), 3);
assert_eq!(rich.full_text(), "This is underlined text.");
assert!(rich.spans()[1].style.underline);
}
#[test]
fn test_markup_parsing_strikethrough() {
let rich = RichText::from_markup("This is ~~strikethrough~~ text.");
assert_eq!(rich.spans().len(), 3);
assert_eq!(rich.full_text(), "This is strikethrough text.");
assert!(rich.spans()[1].style.strikethrough);
}
#[test]
fn test_markup_parsing_mixed() {
let rich = RichText::from_markup("This is **bold** and *italic* and __underlined__ text.");
assert_eq!(rich.spans().len(), 7);
}
}