use std::ops::Range;
use skia_safe::{
FontArguments, FontMgr, FontStyle, Paint as SkPaint, Point as SkPoint,
font_arguments::VariationPosition,
font_arguments::variation_position::Coordinate,
font_style::{Slant, Weight, Width},
textlayout::{
FontCollection, Paragraph as SkParagraph, ParagraphBuilder as SkParagraphBuilder,
ParagraphStyle as SkParagraphStyle, RectHeightStyle, RectWidthStyle,
TextAlign as SkTextAlign, TextDecoration as SkTextDecoration,
TextDecorationStyle as SkTextDecorationStyle, TextShadow as SkTextShadow,
TextStyle as SkTextStyle, TypefaceFontProvider,
},
};
use crate::native::color::{
RgbaLinear, linear_srgb_color_space, rgba_linear_to_skia_color, rgba_linear_to_unpremul_color4f,
};
use crate::native::font::{FontVariation, NativeFontManager};
use crate::native::geometry::Rect;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum TextAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VerticalAlign {
Top,
Center,
Bottom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum TextSlant {
#[default]
Upright,
Italic,
Oblique,
}
impl TextSlant {
fn to_skia(self) -> Slant {
match self {
Self::Upright => Slant::Upright,
Self::Italic => Slant::Italic,
Self::Oblique => Slant::Oblique,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TextStyle {
pub font_families: Vec<String>,
pub font_size: f32,
pub font_weight: i32,
pub slant: TextSlant,
pub color: RgbaLinear,
pub align: TextAlign,
pub line_height_multiplier: f32,
pub letter_spacing: f32,
pub word_spacing: f32,
pub decoration: TextDecoration,
pub decoration_style: TextDecorationStyle,
pub decoration_color: Option<RgbaLinear>,
pub decoration_thickness: f32,
pub shadows: Vec<TextShadow>,
pub baseline_shift: f32,
pub font_variations: Vec<FontVariation>,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
font_families: Vec::new(),
font_size: 16.0,
font_weight: 400,
slant: TextSlant::Upright,
color: RgbaLinear::opaque(0.0, 0.0, 0.0),
align: TextAlign::Left,
line_height_multiplier: 1.0,
letter_spacing: 0.0,
word_spacing: 0.0,
decoration: TextDecoration::default(),
decoration_style: TextDecorationStyle::Solid,
decoration_color: None,
decoration_thickness: 1.0,
shadows: Vec::new(),
baseline_shift: 0.0,
font_variations: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct TextDecoration {
pub underline: bool,
pub overline: bool,
pub line_through: bool,
}
impl TextDecoration {
pub const fn underline() -> Self {
Self {
underline: true,
overline: false,
line_through: false,
}
}
pub const fn line_through() -> Self {
Self {
underline: false,
overline: false,
line_through: true,
}
}
fn to_skia(self) -> SkTextDecoration {
let mut bits = SkTextDecoration::NO_DECORATION;
if self.underline {
bits |= SkTextDecoration::UNDERLINE;
}
if self.overline {
bits |= SkTextDecoration::OVERLINE;
}
if self.line_through {
bits |= SkTextDecoration::LINE_THROUGH;
}
bits
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum TextDecorationStyle {
#[default]
Solid,
Double,
Dotted,
Dashed,
Wavy,
}
impl TextDecorationStyle {
fn to_skia(self) -> SkTextDecorationStyle {
match self {
Self::Solid => SkTextDecorationStyle::Solid,
Self::Double => SkTextDecorationStyle::Double,
Self::Dotted => SkTextDecorationStyle::Dotted,
Self::Dashed => SkTextDecorationStyle::Dashed,
Self::Wavy => SkTextDecorationStyle::Wavy,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TextShadow {
pub color: RgbaLinear,
pub offset_x: f32,
pub offset_y: f32,
pub blur_sigma: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RichTextSpan {
pub text: String,
pub style: TextStyle,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NativeLineMetrics {
pub line_number: usize,
pub start_index: usize,
pub end_index: usize,
pub ascent: f32,
pub descent: f32,
pub height: f32,
pub width: f32,
pub baseline: f32,
pub left: f32,
pub hard_break: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TextBoxOptions {
pub color: RgbaLinear,
pub font_family: Option<String>,
pub font_size: f32,
pub font_weight: i32,
pub horizontal_align: TextAlign,
pub vertical_align: VerticalAlign,
pub opacity: f32,
}
impl Default for TextBoxOptions {
fn default() -> Self {
Self {
color: RgbaLinear::opaque(0.0, 0.0, 0.0),
font_family: None,
font_size: 16.0,
font_weight: 400,
horizontal_align: TextAlign::Left,
vertical_align: VerticalAlign::Top,
opacity: 1.0,
}
}
}
pub struct NativeTextEngine {
pub(crate) collection: FontCollection,
asset_provider: Option<TypefaceFontProvider>,
registered_families: Vec<String>,
}
impl NativeTextEngine {
pub fn new(font_manager: &NativeFontManager) -> Self {
let asset_provider = font_manager.snapshot_provider();
let registered_families = font_manager.registered_family_names();
let mut collection = FontCollection::new();
collection.set_default_font_manager(FontMgr::new(), None);
collection.set_asset_font_manager(Some(asset_provider.clone().into()));
Self {
collection,
asset_provider: Some(asset_provider),
registered_families,
}
}
pub fn with_system_fonts() -> Self {
let mut collection = FontCollection::new();
collection.set_default_font_manager(FontMgr::new(), None);
Self {
collection,
asset_provider: None,
registered_families: Vec::new(),
}
}
pub fn layout_text(&self, text: &str, style: &TextStyle, max_width: f32) -> NativeTextLayout {
let collection = self.collection_for(style);
let sk_text_style = build_text_style(style);
let paragraph_style = build_paragraph_style(style, &sk_text_style);
let mut builder = SkParagraphBuilder::new(¶graph_style, collection);
builder.add_text(text);
let mut paragraph = builder.build();
paragraph.layout(max_width);
NativeTextLayout {
paragraph,
max_width,
}
}
pub fn layout_rich_text(
&self,
spans: &[RichTextSpan],
base_style: &TextStyle,
max_width: f32,
) -> NativeTextLayout {
let collection = self.collection_for(base_style);
let base_sk_style = build_text_style(base_style);
let paragraph_style = build_paragraph_style(base_style, &base_sk_style);
let mut builder = SkParagraphBuilder::new(¶graph_style, collection);
for span in spans {
let span_sk_style = build_text_style(&span.style);
builder.push_style(&span_sk_style);
builder.add_text(&span.text);
builder.pop();
}
let mut paragraph = builder.build();
paragraph.layout(max_width);
NativeTextLayout {
paragraph,
max_width,
}
}
fn collection_for(&self, style: &TextStyle) -> FontCollection {
if style.font_variations.is_empty() || style.font_families.is_empty() {
return self.collection.clone();
}
let families: Vec<&str> = style.font_families.iter().map(String::as_str).collect();
let sk_font_style = FontStyle::new(
Weight::from(style.font_weight),
Width::NORMAL,
style.slant.to_skia(),
);
let mut find_collection = self.collection.clone();
let matches = find_collection.find_typefaces(&families, sk_font_style);
if !matches
.iter()
.any(|tf| tf.variation_design_parameters().is_some())
{
return self.collection.clone();
}
let mut dynamic = TypefaceFontProvider::new();
let explicit_tags: Vec<u32> = style
.font_variations
.iter()
.map(|v| u32::from_be_bytes(*v.axis.as_bytes()))
.collect();
for face in matches {
let Some(params) = face.variation_design_parameters() else {
continue;
};
let mut coords: Vec<Coordinate> = Vec::new();
for v in &style.font_variations {
let axis_u32 = u32::from_be_bytes(*v.axis.as_bytes());
if let Some(param) = params.iter().find(|p| *p.tag == axis_u32) {
coords.push(Coordinate {
axis: param.tag,
value: v.value.clamp(param.min, param.max),
});
}
}
let wght_u32 = u32::from_be_bytes(*b"wght");
if !explicit_tags.contains(&wght_u32)
&& let Some(param) = params.iter().find(|p| *p.tag == wght_u32)
{
let weight_f = (*sk_font_style.weight() - *Weight::INVISIBLE).max(0) as f32;
coords.push(Coordinate {
axis: param.tag,
value: weight_f.clamp(param.min, param.max),
});
}
if coords.is_empty() {
continue;
}
let v_pos = VariationPosition {
coordinates: &coords,
};
let args = FontArguments::new().set_variation_design_position(v_pos);
let Some(instance) = face.clone_with_arguments(&args) else {
continue;
};
let intrinsic = face.family_name();
let alias = self
.registered_families
.iter()
.find(|f| f.as_str() == intrinsic.as_str())
.map(String::as_str);
dynamic.register_typeface(instance, alias);
}
let mut collection = FontCollection::new();
collection.set_default_font_manager(FontMgr::new(), None);
if let Some(provider) = &self.asset_provider {
collection.set_asset_font_manager(Some(provider.clone().into()));
}
collection.set_dynamic_font_manager(Some(dynamic.into()));
collection
}
}
pub struct NativeTextLayout {
pub(crate) paragraph: SkParagraph,
max_width: f32,
}
impl NativeTextLayout {
pub fn width(&self) -> f32 {
self.paragraph.longest_line()
}
pub fn max_width(&self) -> f32 {
self.max_width
}
pub fn height(&self) -> f32 {
self.paragraph.height()
}
pub fn line_count(&self) -> usize {
self.paragraph.line_number()
}
pub fn first_line_ascent(&self) -> f32 {
let metrics = self.paragraph.get_line_metrics();
metrics.first().map(|m| m.ascent as f32).unwrap_or_default()
}
pub fn line_metrics(&self) -> Vec<NativeLineMetrics> {
self.paragraph
.get_line_metrics()
.iter()
.enumerate()
.map(|(i, m)| NativeLineMetrics {
line_number: i,
start_index: m.start_index,
end_index: m.end_index,
ascent: m.ascent as f32,
descent: m.descent as f32,
height: m.height as f32,
width: m.width as f32,
baseline: m.baseline as f32,
left: m.left as f32,
hard_break: m.hard_break,
})
.collect()
}
pub fn rects_for_range(&self, range: Range<usize>) -> Vec<Rect> {
self.paragraph
.get_rects_for_range(range, RectHeightStyle::Tight, RectWidthStyle::Tight)
.into_iter()
.map(|tb| {
let r = tb.rect;
Rect {
left: r.left,
top: r.top,
right: r.right,
bottom: r.bottom,
}
})
.collect()
}
}
fn build_text_style(style: &TextStyle) -> SkTextStyle {
let mut sk_style = SkTextStyle::new();
let mut paint = SkPaint::default();
let cs = linear_srgb_color_space();
paint.set_color4f(rgba_linear_to_unpremul_color4f(style.color), Some(&cs));
paint.set_anti_alias(true);
sk_style.set_foreground_paint(&paint);
sk_style.set_font_size(style.font_size);
if !style.font_families.is_empty() {
let families: Vec<&str> = style.font_families.iter().map(String::as_str).collect();
sk_style.set_font_families(&families);
}
sk_style.set_font_style(FontStyle::new(
Weight::from(style.font_weight),
Width::NORMAL,
style.slant.to_skia(),
));
if (style.line_height_multiplier - 1.0).abs() > f32::EPSILON {
sk_style.set_height(style.line_height_multiplier);
sk_style.set_height_override(true);
}
if style.letter_spacing != 0.0 {
sk_style.set_letter_spacing(style.letter_spacing);
}
if style.word_spacing != 0.0 {
sk_style.set_word_spacing(style.word_spacing);
}
if style.baseline_shift != 0.0 {
sk_style.set_baseline_shift(style.baseline_shift);
}
let sk_decoration = style.decoration.to_skia();
if sk_decoration != SkTextDecoration::NO_DECORATION {
sk_style.set_decoration_type(sk_decoration);
sk_style.set_decoration_style(style.decoration_style.to_skia());
if let Some(color) = style.decoration_color {
sk_style.set_decoration_color(rgba_linear_to_skia_color(color));
}
if (style.decoration_thickness - 1.0).abs() > f32::EPSILON {
sk_style.set_decoration_thickness_multiplier(style.decoration_thickness);
}
}
for shadow in &style.shadows {
sk_style.add_shadow(SkTextShadow::new(
rgba_linear_to_skia_color(shadow.color),
SkPoint::new(shadow.offset_x, shadow.offset_y),
shadow.blur_sigma as f64,
));
}
sk_style
}
fn build_paragraph_style(style: &TextStyle, base_sk_style: &SkTextStyle) -> SkParagraphStyle {
let mut paragraph_style = SkParagraphStyle::new();
paragraph_style.set_text_align(match style.align {
TextAlign::Left => SkTextAlign::Left,
TextAlign::Center => SkTextAlign::Center,
TextAlign::Right => SkTextAlign::Right,
});
paragraph_style.set_text_style(base_sk_style);
paragraph_style
}