use anyhow::Result;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use skia_safe::{Canvas, Font, FontStyle};
use crate::engine::renderer::{font_mgr, paint_from_hex, draw_text_with_fallback, measure_text_with_fallback, emoji_typeface};
use crate::layout::{Constraints, LayoutNode};
use crate::schema::{FontStyleType, FontWeight, LayerStyle, TextAlign};
use crate::traits::{RenderContext, TimingConfig, Widget};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct RichTextSpan {
pub text: String,
#[serde(default)]
pub color: Option<String>,
#[serde(default, rename = "font-size")]
pub font_size: Option<f32>,
#[serde(default, rename = "font-weight")]
pub font_weight: Option<FontWeight>,
#[serde(default, rename = "font-family")]
pub font_family: Option<String>,
#[serde(default, rename = "font-style")]
pub font_style: Option<FontStyleType>,
#[serde(default, rename = "letter-spacing")]
pub letter_spacing: Option<f32>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct RichText {
pub spans: Vec<RichTextSpan>,
#[serde(default)]
pub max_width: Option<f32>,
#[serde(flatten)]
pub timing: TimingConfig,
#[serde(default)]
pub style: LayerStyle,
}
crate::impl_traits!(RichText {
Animatable => style,
Timed => timing,
Styled => style,
});
fn make_font(
family: &str,
weight: &FontWeight,
font_style_type: &FontStyleType,
size: f32,
) -> Font {
let fm = font_mgr();
let slant = match font_style_type {
FontStyleType::Normal => skia_safe::font_style::Slant::Upright,
FontStyleType::Italic => skia_safe::font_style::Slant::Italic,
FontStyleType::Oblique => skia_safe::font_style::Slant::Oblique,
};
let weight_val = match weight {
FontWeight::Bold => skia_safe::font_style::Weight::BOLD,
FontWeight::Normal => skia_safe::font_style::Weight::NORMAL,
FontWeight::Weight(w) => skia_safe::font_style::Weight::from(*w as i32),
};
let skia_style = FontStyle::new(weight_val, skia_safe::font_style::Width::NORMAL, slant);
let typeface = fm
.match_family_style(family, skia_style)
.or_else(|| fm.match_family_style("Helvetica", skia_style))
.or_else(|| fm.match_family_style("Arial", skia_style))
.or_else(|| fm.match_family_style("sans-serif", skia_style))
.unwrap_or_else(|| {
fm.match_family_style(&fm.family_name(0), skia_style).unwrap()
});
Font::from_typeface(typeface, size)
}
fn resolve_line_height(line_height: Option<f32>, font_size: f32) -> f32 {
match line_height {
Some(v) if v <= 10.0 => font_size * v,
Some(v) => v,
None => font_size * 1.3,
}
}
struct PreparedSpan {
text: String,
font: Font,
color: String,
letter_spacing: f32,
width: f32,
}
impl Widget for RichText {
fn render(
&self,
canvas: &Canvas,
layout: &LayoutNode,
_ctx: &RenderContext,
_props: &crate::engine::animator::AnimatedProperties,
) -> Result<()> {
let default_size = self.style.font_size_or(48.0);
let default_color = self.style.color_or("#FFFFFF");
let default_family = self.style.font_family_or("Inter");
let default_weight = self.style.font_weight_or(FontWeight::Normal);
let default_font_style = self.style.font_style_or(FontStyleType::Normal);
let align = self.style.text_align_or(TextAlign::Left);
let line_height_val = resolve_line_height(self.style.line_height, default_size);
let emoji_tf = emoji_typeface();
let prepared: Vec<PreparedSpan> = self.spans.iter().map(|span| {
let size = span.font_size.unwrap_or(default_size);
let family = span.font_family.as_deref().unwrap_or(default_family);
let weight = span.font_weight.as_ref().unwrap_or(&default_weight);
let fstyle = span.font_style.as_ref().unwrap_or(&default_font_style);
let color = span.color.as_deref().unwrap_or(default_color).to_string();
let letter_spacing = span.letter_spacing.unwrap_or(0.0);
let font = make_font(family, weight, fstyle, size);
let emoji_font = emoji_tf.as_ref().map(|tf| Font::from_typeface(tf.clone(), size));
let width = measure_text_with_fallback(&span.text, &font, &emoji_font, letter_spacing);
PreparedSpan { text: span.text.clone(), font, color, letter_spacing, width }
}).collect();
let wrap_width = if layout.width.is_finite() && layout.width > 0.0 {
match self.max_width {
Some(mw) => mw.min(layout.width),
None => layout.width,
}
} else {
self.max_width.unwrap_or(f32::INFINITY)
};
struct LineSpan {
span_idx: usize,
x: f32,
}
struct Line {
spans: Vec<LineSpan>,
width: f32,
}
let mut lines: Vec<Line> = vec![Line { spans: vec![], width: 0.0 }];
for (i, ps) in prepared.iter().enumerate() {
let current = lines.last_mut().unwrap();
if current.width + ps.width > wrap_width && !current.spans.is_empty() {
lines.push(Line {
spans: vec![LineSpan { span_idx: i, x: 0.0 }],
width: ps.width,
});
} else {
let x = current.width;
current.spans.push(LineSpan { span_idx: i, x });
current.width += ps.width;
}
}
let align_width = if layout.width.is_finite() && layout.width > 0.0 {
layout.width
} else {
lines.iter().map(|l| l.width).fold(0.0f32, f32::max)
};
let max_ascent = prepared.iter().map(|ps| {
let (_, m) = ps.font.metrics();
-m.ascent
}).fold(0.0f32, f32::max);
let baseline_offset = (line_height_val + max_ascent) / 2.0;
for (line_idx, line) in lines.iter().enumerate() {
let line_x_offset = match align {
TextAlign::Left => 0.0,
TextAlign::Center => (align_width - line.width) / 2.0,
TextAlign::Right => align_width - line.width,
};
let y = line_idx as f32 * line_height_val + baseline_offset;
for ls in &line.spans {
let ps = &prepared[ls.span_idx];
let paint = paint_from_hex(&ps.color);
let emoji_font = emoji_tf.as_ref().map(|tf| Font::from_typeface(tf.clone(), ps.font.size()));
draw_text_with_fallback(
canvas,
&ps.text,
&ps.font,
&emoji_font,
ps.letter_spacing,
line_x_offset + ls.x,
y,
&paint,
);
}
}
Ok(())
}
fn measure(&self, _constraints: &Constraints) -> (f32, f32) {
let default_size = self.style.font_size_or(48.0);
let default_family = self.style.font_family_or("Inter");
let default_weight = self.style.font_weight_or(FontWeight::Normal);
let default_font_style = self.style.font_style_or(FontStyleType::Normal);
let line_height_val = resolve_line_height(self.style.line_height, default_size);
let emoji_tf = emoji_typeface();
let mut total_width = 0.0f32;
for span in &self.spans {
let size = span.font_size.unwrap_or(default_size);
let family = span.font_family.as_deref().unwrap_or(default_family);
let weight = span.font_weight.as_ref().unwrap_or(&default_weight);
let fstyle = span.font_style.as_ref().unwrap_or(&default_font_style);
let letter_spacing = span.letter_spacing.unwrap_or(0.0);
let font = make_font(family, weight, fstyle, size);
let emoji_font = emoji_tf.as_ref().map(|tf| Font::from_typeface(tf.clone(), size));
total_width += measure_text_with_fallback(&span.text, &font, &emoji_font, letter_spacing);
}
let wrap_width = self.max_width.unwrap_or(total_width);
let num_lines = (total_width / wrap_width).ceil().max(1.0) as usize;
let h = num_lines as f32 * line_height_val;
(total_width.min(wrap_width), h)
}
}