use std::ops::Range;
use skia_safe::{
FontArguments, FontMgr, FontStyle, Paint as SkPaint, Point as SkPoint,
font_arguments::{VariationPosition, variation_position::Coordinate},
font_style::{Slant, Weight, Width},
textlayout::{
FontCollection, Paragraph as SkParagraph,
ParagraphBuilder as SkParagraphBuilder,
ParagraphStyle as SkParagraphStyle, RectHeightStyle, RectWidthStyle,
StrutStyle as SkStrutStyle, TextAlign as SkTextAlign,
TextDecoration as SkTextDecoration,
TextDecorationStyle as SkTextDecorationStyle,
TextHeightBehavior as SkTextHeightBehavior, TextShadow as SkTextShadow,
TextStyle as SkTextStyle, TypefaceFontProvider,
},
};
use crate::{
color::{
RgbaLinear, linear_srgb_color_space, rgba_linear_to_skia_color,
rgba_linear_to_unpremul_color4f,
},
font::{FontManager, FontVariation},
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, Eq, Hash)]
pub struct FontFeature {
pub name: String,
pub value: i32,
}
impl FontFeature {
pub fn new(name: impl Into<String>, value: i32) -> Self {
Self {
name: name.into(),
value,
}
}
pub fn on(name: impl Into<String>) -> Self {
Self::new(name, 1)
}
pub fn off(name: impl Into<String>) -> Self {
Self::new(name, 0)
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct StrutStyle {
pub font_families: Vec<String>,
pub font_size: Option<f32>,
pub height: Option<f32>,
pub leading: Option<f32>,
pub force_height: bool,
pub half_leading: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum TextHeightBehavior {
#[default]
All,
DisableFirstAscent,
DisableLastDescent,
DisableAll,
}
impl TextHeightBehavior {
fn to_skia(self) -> SkTextHeightBehavior {
match self {
Self::All => SkTextHeightBehavior::All,
Self::DisableFirstAscent => {
SkTextHeightBehavior::DisableFirstAscent
}
Self::DisableLastDescent => {
SkTextHeightBehavior::DisableLastDescent
}
Self::DisableAll => SkTextHeightBehavior::DisableAll,
}
}
}
#[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>,
pub font_features: Vec<FontFeature>,
pub half_leading: bool,
pub strut: Option<StrutStyle>,
pub text_height_behavior: TextHeightBehavior,
pub max_lines: Option<usize>,
}
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(),
font_features: Vec::new(),
half_leading: false,
strut: None,
text_height_behavior: TextHeightBehavior::All,
max_lines: None,
}
}
}
#[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 LineMetrics {
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 TextEngine {
pub(crate) collection: FontCollection,
asset_provider: Option<TypefaceFontProvider>,
registered_families: Vec<String>,
}
impl TextEngine {
pub fn new(font_manager: &FontManager) -> 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()));
collection.enable_font_fallback();
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);
collection.enable_font_fallback();
Self {
collection,
asset_provider: None,
registered_families: Vec::new(),
}
}
pub fn layout_text(
&self,
text: &str,
style: &TextStyle,
max_width: f32,
) -> TextLayout {
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);
TextLayout {
paragraph,
max_width,
}
}
pub fn layout_rich_text(
&self,
spans: &[RichTextSpan],
base_style: &TextStyle,
max_width: f32,
) -> TextLayout {
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);
TextLayout {
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.enable_font_fallback();
collection
}
}
pub struct TextLayout {
pub(crate) paragraph: SkParagraph,
max_width: f32,
}
impl TextLayout {
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<LineMetrics> {
self.paragraph
.get_line_metrics()
.iter()
.enumerate()
.map(|(i, m)| LineMetrics {
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()
}
pub fn did_exceed_max_lines(&self) -> bool {
self.paragraph.did_exceed_max_lines()
}
pub fn rects_for_placeholders(&self) -> Vec<Rect> {
self.paragraph
.get_rects_for_placeholders()
.into_iter()
.map(|tb| {
let r = tb.rect;
Rect {
left: r.left,
top: r.top,
right: r.right,
bottom: r.bottom,
}
})
.collect()
}
pub fn unresolved_codepoints(&mut self) -> Vec<u32> {
self.paragraph
.unresolved_codepoints()
.into_iter()
.map(|cp| cp as u32)
.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);
}
for feature in &style.font_features {
sk_style.add_font_feature(&feature.name, feature.value);
}
if style.half_leading {
sk_style.set_half_leading(true);
}
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);
if style.text_height_behavior != TextHeightBehavior::All {
paragraph_style
.set_text_height_behavior(style.text_height_behavior.to_skia());
}
if let Some(max_lines) = style.max_lines {
paragraph_style.set_max_lines(max_lines);
}
if let Some(strut) = &style.strut {
let mut sk_strut = SkStrutStyle::new();
sk_strut.set_strut_enabled(true);
if !strut.font_families.is_empty() {
let families: Vec<&str> =
strut.font_families.iter().map(String::as_str).collect();
sk_strut.set_font_families(&families);
}
if let Some(size) = strut.font_size {
sk_strut.set_font_size(size);
}
if let Some(height) = strut.height {
sk_strut.set_height(height);
sk_strut.set_height_override(true);
}
if let Some(leading) = strut.leading {
sk_strut.set_leading(leading);
}
sk_strut.set_force_strut_height(strut.force_height);
sk_strut.set_half_leading(strut.half_leading);
paragraph_style.set_strut_style(sk_strut);
}
paragraph_style
}