use std::fmt::{self, Debug, Display};
use crate::{clamp_double, lerp_double, lerp_int};
pub const K_TEXT_HEIGHT_NONE: f64 = 0.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum FontStyle {
Normal,
Italic,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct FontWeight {
pub value: i32,
}
impl FontWeight {
pub fn new(value: i32) -> FontWeight {
debug_assert!(
(1..=1000).contains(&value),
"Font weight must be between 1 and 1000"
);
FontWeight { value }
}
pub fn index(self) -> i32 {
((self.value / 100) - 1).clamp(0, 8)
}
pub const W100: FontWeight = FontWeight { value: 100 };
pub const W200: FontWeight = FontWeight { value: 200 };
pub const W300: FontWeight = FontWeight { value: 300 };
pub const W400: FontWeight = FontWeight { value: 400 };
pub const W500: FontWeight = FontWeight { value: 500 };
pub const W600: FontWeight = FontWeight { value: 600 };
pub const W700: FontWeight = FontWeight { value: 700 };
pub const W800: FontWeight = FontWeight { value: 800 };
pub const W900: FontWeight = FontWeight { value: 900 };
pub const NORMAL: FontWeight = FontWeight::W400;
pub const BOLD: FontWeight = FontWeight::W700;
pub const VALUES: [FontWeight; 9] = [
FontWeight::W100,
FontWeight::W200,
FontWeight::W300,
FontWeight::W400,
FontWeight::W500,
FontWeight::W600,
FontWeight::W700,
FontWeight::W800,
FontWeight::W900,
];
pub fn lerp(a: Option<FontWeight>, b: Option<FontWeight>, t: f64) -> Option<FontWeight> {
if a.is_none() && b.is_none() {
return None;
}
let a = a.unwrap_or(FontWeight::NORMAL).value;
let b = b.unwrap_or(FontWeight::NORMAL).value;
Some(FontWeight::new(
lerp_int(a, b, t).round().clamp(100.0, 900.0) as i32,
))
}
}
impl Debug for FontWeight {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
impl Display for FontWeight {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.value % 100 != 0 {
return write!(f, "FontWeight({})", self.value);
}
let name = match self.index() {
0 => "w100",
1 => "w200",
2 => "w300",
3 => "w400",
4 => "w500",
5 => "w600",
6 => "w700",
7 => "w800",
8 => "w900",
_ => unreachable!(),
};
write!(f, "FontWeight.{name}")
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TextAlign {
Left,
Right,
Center,
Justify,
Start,
End,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TextBaseline {
Alphabetic,
Ideographic,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct TextDecoration {
mask: i32,
}
impl TextDecoration {
pub const NONE: TextDecoration = TextDecoration { mask: 0x0 };
pub const UNDERLINE: TextDecoration = TextDecoration { mask: 0x1 };
pub const OVERLINE: TextDecoration = TextDecoration { mask: 0x2 };
pub const LINE_THROUGH: TextDecoration = TextDecoration { mask: 0x4 };
pub fn combine(decorations: &[TextDecoration]) -> TextDecoration {
let mut mask = 0;
for decoration in decorations {
mask |= decoration.mask;
}
TextDecoration { mask }
}
pub fn mask_value(self) -> i32 {
self.mask
}
pub fn contains(self, other: TextDecoration) -> bool {
(self.mask | other.mask) == self.mask
}
}
impl Debug for TextDecoration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(self, f)
}
}
impl Display for TextDecoration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.mask == 0 {
return write!(f, "TextDecoration.none");
}
let mut values = Vec::new();
if self.mask & TextDecoration::UNDERLINE.mask != 0 {
values.push("underline");
}
if self.mask & TextDecoration::OVERLINE.mask != 0 {
values.push("overline");
}
if self.mask & TextDecoration::LINE_THROUGH.mask != 0 {
values.push("lineThrough");
}
if values.len() == 1 {
write!(f, "TextDecoration.{}", values[0])
} else {
write!(f, "TextDecoration.combine([{}])", values.join(", "))
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TextDecorationStyle {
Solid,
Double,
Dotted,
Dashed,
Wavy,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TextLeadingDistribution {
Proportional,
Even,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TextHeightBehavior {
pub apply_height_to_first_ascent: bool,
pub apply_height_to_last_descent: bool,
pub leading_distribution: TextLeadingDistribution,
}
impl Default for TextHeightBehavior {
fn default() -> TextHeightBehavior {
TextHeightBehavior {
apply_height_to_first_ascent: true,
apply_height_to_last_descent: true,
leading_distribution: TextLeadingDistribution::Proportional,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TextDirection {
Rtl,
Ltr,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct FontFeature {
pub feature: String,
pub value: i32,
}
impl FontFeature {
pub fn new(feature: impl Into<String>, value: i32) -> FontFeature {
let feature = feature.into();
debug_assert!(
feature.len() == 4,
"Feature tag must be exactly four characters long."
);
debug_assert!(
value >= 0,
"Feature value must be zero or a positive integer."
);
FontFeature { feature, value }
}
pub fn enable(feature: impl Into<String>) -> FontFeature {
FontFeature::new(feature, 1)
}
pub fn disable(feature: impl Into<String>) -> FontFeature {
FontFeature::new(feature, 0)
}
}
impl Display for FontFeature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "FontFeature('{}', {})", self.feature, self.value)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct FontVariation {
pub axis: String,
pub value: f64,
}
impl FontVariation {
pub fn new(axis: impl Into<String>, value: f64) -> FontVariation {
let axis = axis.into();
debug_assert!(
axis.len() == 4,
"Axis tag must be exactly four characters long."
);
debug_assert!(
(-32768.0..32768.0).contains(&value),
"Value must be representable as a signed 16.16 fixed-point number, i.e. it must be in this range: -32768.0 ≤ value < 32768.0"
);
FontVariation { axis, value }
}
pub fn italic(value: f64) -> FontVariation {
debug_assert!((0.0..=1.0).contains(&value));
FontVariation::new("ital", value)
}
pub fn optical_size(value: f64) -> FontVariation {
debug_assert!(value > 0.0);
FontVariation::new("opsz", value)
}
pub fn slant(value: f64) -> FontVariation {
debug_assert!(value > -90.0 && value < 90.0);
FontVariation::new("slnt", value)
}
pub fn width(value: f64) -> FontVariation {
debug_assert!(value >= 0.0);
FontVariation::new("wdth", value)
}
pub fn weight(value: f64) -> FontVariation {
debug_assert!((1.0..=1000.0).contains(&value));
FontVariation::new("wght", value)
}
pub fn lerp(
a: Option<&FontVariation>,
b: Option<&FontVariation>,
t: f64,
) -> Option<FontVariation> {
if a.map(|v| v.axis.as_str()) != b.map(|v| v.axis.as_str()) || (a.is_none() && b.is_none())
{
return if t < 0.5 { a.cloned() } else { b.cloned() };
}
Some(FontVariation::new(
a.unwrap().axis.clone(),
clamp_double(
lerp_double(Some(a.unwrap().value), Some(b.unwrap().value), t).unwrap(),
-32768.0,
32768.0 - 1.0 / 65536.0,
),
))
}
}
impl Display for FontVariation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "FontVariation('{}', {})", self.axis, self.value)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn font_weight_lerp_treats_null_as_normal() {
assert!(FontWeight::lerp(None, None, 0.5).is_none());
assert_eq!(
FontWeight::lerp(None, Some(FontWeight::W700), 0.0),
Some(FontWeight::NORMAL)
);
assert_eq!(
FontWeight::lerp(Some(FontWeight::W100), Some(FontWeight::W900), 0.5),
Some(FontWeight::W500)
);
}
#[test]
fn text_decoration_combine_unions_masks() {
let combined =
TextDecoration::combine(&[TextDecoration::UNDERLINE, TextDecoration::LINE_THROUGH]);
assert!(combined.contains(TextDecoration::UNDERLINE));
assert!(combined.contains(TextDecoration::LINE_THROUGH));
assert!(!combined.contains(TextDecoration::OVERLINE));
assert_eq!(
format!("{combined}"),
"TextDecoration.combine([underline, lineThrough])"
);
}
#[test]
fn text_direction_lists_rtl_first() {
assert_eq!(TextDirection::Rtl as u8, 0);
assert_eq!(TextDirection::Ltr as u8, 1);
}
#[test]
fn font_variation_lerp_switches_axis_at_half() {
let a = FontVariation::weight(400.0);
let b = FontVariation::width(100.0);
assert_eq!(
FontVariation::lerp(Some(&a), Some(&b), 0.4).unwrap().axis,
"wght"
);
assert_eq!(
FontVariation::lerp(Some(&a), Some(&b), 0.6).unwrap().axis,
"wdth"
);
}
#[test]
fn font_variation_lerp_interpolates_same_axis() {
let a = FontVariation::weight(100.0);
let b = FontVariation::weight(900.0);
let mid = FontVariation::lerp(Some(&a), Some(&b), 0.5).unwrap();
assert_eq!(mid.axis, "wght");
assert!((mid.value - 500.0).abs() < 1e-9);
}
}