use peniko::Color;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FamilyName {
Named(String),
Generic(GenericSlot),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GenericSlot {
Monospace,
SansSerif,
Serif,
SystemUi,
Emoji,
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub enum FontFamily {
#[default]
SystemUi,
Named(Vec<String>),
NamedWithGeneric(Vec<FamilyName>),
}
impl FontFamily {
pub fn named(name: impl Into<String>) -> Self {
Self::Named(vec![name.into()])
}
pub fn stack(names: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self::Named(names.into_iter().map(Into::into).collect())
}
pub fn stack_with_generic(
names: impl IntoIterator<Item = impl Into<String>>,
generic: GenericSlot,
) -> Self {
let mut families: Vec<FamilyName> = names
.into_iter()
.map(|name| FamilyName::Named(name.into()))
.collect();
families.push(FamilyName::Generic(generic));
Self::NamedWithGeneric(families)
}
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct FontWeight(f32);
impl FontWeight {
pub const THIN: Self = Self(100.0);
pub const EXTRA_LIGHT: Self = Self(200.0);
pub const LIGHT: Self = Self(300.0);
pub const REGULAR: Self = Self(400.0);
pub const MEDIUM: Self = Self(500.0);
pub const SEMI_BOLD: Self = Self(600.0);
pub const BOLD: Self = Self(700.0);
pub const EXTRA_BOLD: Self = Self(800.0);
pub const BLACK: Self = Self(900.0);
pub const fn new(value: f32) -> Self {
Self(value)
}
pub const fn value(self) -> f32 {
self.0
}
}
impl Default for FontWeight {
fn default() -> Self {
Self::REGULAR
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum FontStyle {
#[default]
Normal,
Italic,
Oblique(Option<f32>),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum TextAlign {
#[default]
Start,
End,
Left,
Center,
Right,
Justify,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum TextOverflow {
#[default]
Clip,
Ellipsis,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum LineHeight {
MetricsRelative(f32),
FontSizeRelative(f32),
Absolute(f32),
}
impl Default for LineHeight {
fn default() -> Self {
Self::MetricsRelative(1.0)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TextStyle {
pub family: FontFamily,
pub weight: FontWeight,
pub style: FontStyle,
pub size: f32,
pub color: Color,
pub letter_spacing: f32,
pub line_height: LineHeight,
pub align: TextAlign,
}
impl TextStyle {
pub fn new(size: f32, color: Color) -> Self {
Self {
size,
color,
..Self::default()
}
}
}
impl Default for TextStyle {
fn default() -> Self {
Self {
family: FontFamily::default(),
weight: FontWeight::default(),
style: FontStyle::default(),
size: 16.0,
color: Color::BLACK,
letter_spacing: 0.0,
line_height: LineHeight::default(),
align: TextAlign::default(),
}
}
}
pub(crate) fn to_parley_family(family: &FontFamily) -> parley::FontFamily<'static> {
match family {
FontFamily::SystemUi => parley::GenericFamily::SystemUi.into(),
FontFamily::Named(names) => {
let list: Vec<parley::FontFamilyName<'static>> = names
.iter()
.map(|name| parley::FontFamilyName::Named(name.clone().into()))
.collect();
parley::FontFamily::List(list.into())
}
FontFamily::NamedWithGeneric(families) => {
let list: Vec<parley::FontFamilyName<'static>> = families
.iter()
.map(|f| match f {
FamilyName::Named(name) => parley::FontFamilyName::Named(name.clone().into()),
FamilyName::Generic(slot) => {
parley::FontFamilyName::Generic(generic_slot_to_parley(*slot))
}
})
.collect();
parley::FontFamily::List(list.into())
}
}
}
fn generic_slot_to_parley(slot: GenericSlot) -> parley::GenericFamily {
match slot {
GenericSlot::Monospace => parley::GenericFamily::Monospace,
GenericSlot::SansSerif => parley::GenericFamily::SansSerif,
GenericSlot::Serif => parley::GenericFamily::Serif,
GenericSlot::SystemUi => parley::GenericFamily::SystemUi,
GenericSlot::Emoji => parley::GenericFamily::Emoji,
}
}
pub(crate) fn to_parley_weight(weight: FontWeight) -> parley::FontWeight {
parley::FontWeight::new(weight.0)
}
pub(crate) fn to_parley_style(style: FontStyle) -> parley::FontStyle {
match style {
FontStyle::Normal => parley::FontStyle::Normal,
FontStyle::Italic => parley::FontStyle::Italic,
FontStyle::Oblique(angle) => parley::FontStyle::Oblique(angle),
}
}
pub(crate) fn to_parley_line_height(line_height: LineHeight) -> parley::LineHeight {
match line_height {
LineHeight::MetricsRelative(v) => parley::LineHeight::MetricsRelative(v),
LineHeight::FontSizeRelative(v) => parley::LineHeight::FontSizeRelative(v),
LineHeight::Absolute(v) => parley::LineHeight::Absolute(v),
}
}
pub(crate) fn to_parley_align(align: TextAlign) -> parley::layout::Alignment {
match align {
TextAlign::Start => parley::layout::Alignment::Start,
TextAlign::End => parley::layout::Alignment::End,
TextAlign::Left => parley::layout::Alignment::Left,
TextAlign::Center => parley::layout::Alignment::Center,
TextAlign::Right => parley::layout::Alignment::Right,
TextAlign::Justify => parley::layout::Alignment::Justify,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_preserves_old_v1_semantics() {
let style = TextStyle::default();
assert_eq!(style.family, FontFamily::SystemUi);
assert_eq!(style.weight, FontWeight::REGULAR);
assert_eq!(style.style, FontStyle::Normal);
assert_eq!(style.size, 16.0);
assert_eq!(style.color, Color::BLACK);
assert_eq!(style.letter_spacing, 0.0);
assert_eq!(style.line_height, LineHeight::MetricsRelative(1.0));
assert_eq!(style.align, TextAlign::Start);
}
#[test]
fn new_only_overrides_size_and_color() {
let style = TextStyle::new(24.0, Color::from_rgb8(0x11, 0x22, 0x33));
assert_eq!(style.size, 24.0);
assert_eq!(style.color, Color::from_rgb8(0x11, 0x22, 0x33));
assert_eq!(style.family, FontFamily::SystemUi);
assert_eq!(style.weight, FontWeight::REGULAR);
assert_eq!(style.style, FontStyle::Normal);
assert_eq!(style.align, TextAlign::Start);
}
#[test]
fn to_parley_align_maps_every_variant() {
assert_eq!(
to_parley_align(TextAlign::Start),
parley::layout::Alignment::Start
);
assert_eq!(
to_parley_align(TextAlign::End),
parley::layout::Alignment::End
);
assert_eq!(
to_parley_align(TextAlign::Left),
parley::layout::Alignment::Left
);
assert_eq!(
to_parley_align(TextAlign::Center),
parley::layout::Alignment::Center
);
assert_eq!(
to_parley_align(TextAlign::Right),
parley::layout::Alignment::Right
);
assert_eq!(
to_parley_align(TextAlign::Justify),
parley::layout::Alignment::Justify
);
}
#[test]
fn font_weight_consts_match_css_scale() {
assert_eq!(FontWeight::THIN.value(), 100.0);
assert_eq!(FontWeight::REGULAR.value(), 400.0);
assert_eq!(FontWeight::MEDIUM.value(), 500.0);
assert_eq!(FontWeight::BOLD.value(), 700.0);
assert_eq!(FontWeight::BLACK.value(), 900.0);
}
#[test]
fn family_named_and_stack_constructors() {
assert_eq!(
FontFamily::named("Inter"),
FontFamily::Named(vec!["Inter".to_string()])
);
assert_eq!(
FontFamily::stack(["Inter", "Roboto"]),
FontFamily::Named(vec!["Inter".to_string(), "Roboto".to_string()])
);
}
#[test]
fn stack_with_generic_creates_correct_variant() {
let family = FontFamily::stack_with_generic(["IBM Plex Mono"], GenericSlot::Monospace);
match family {
FontFamily::NamedWithGeneric(families) => {
assert_eq!(families.len(), 2);
assert_eq!(families[0], FamilyName::Named("IBM Plex Mono".to_string()));
assert_eq!(families[1], FamilyName::Generic(GenericSlot::Monospace));
}
_ => panic!("Expected NamedWithGeneric variant"),
}
}
#[test]
fn to_parley_family_named_then_generic() {
let family = FontFamily::stack_with_generic(["NoSuchFont"], GenericSlot::Monospace);
let parley_family = to_parley_family(&family);
match parley_family {
parley::FontFamily::List(_) => {
}
_ => panic!("Expected FontFamily::List variant"),
}
}
#[test]
fn generic_slot_maps_correctly_to_parley() {
let mono = generic_slot_to_parley(GenericSlot::Monospace);
assert_eq!(mono, parley::GenericFamily::Monospace);
let sans = generic_slot_to_parley(GenericSlot::SansSerif);
assert_eq!(sans, parley::GenericFamily::SansSerif);
let serif = generic_slot_to_parley(GenericSlot::Serif);
assert_eq!(serif, parley::GenericFamily::Serif);
let system = generic_slot_to_parley(GenericSlot::SystemUi);
assert_eq!(system, parley::GenericFamily::SystemUi);
let emoji = generic_slot_to_parley(GenericSlot::Emoji);
assert_eq!(emoji, parley::GenericFamily::Emoji);
}
#[test]
fn stack_with_generic_monospace_fallback_shapes_correctly() {
let family = FontFamily::stack_with_generic(
["NonexistentFontFamilyName12345"],
GenericSlot::Monospace,
);
let parley_family = to_parley_family(&family);
match parley_family {
parley::FontFamily::List(list) => {
assert!(
list.len() >= 2,
"expected at least 2 family entries (named + generic), got {}",
list.len()
);
if let Some(last) = list.last() {
assert!(
matches!(last, parley::FontFamilyName::Generic(_)),
"expected last entry to be a generic family, got {:?}",
last
);
}
}
_ => panic!("expected FontFamily::List variant from stack_with_generic"),
}
}
}