use std::fmt::{self, Display, Formatter};
use std::hash::{Hash, Hasher};
use ecow::{EcoString, eco_format};
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use typst_utils::Rdedup;
use crate::diag::{Hint, HintedStrResult};
use crate::foundations::{Dict, Fold, IntoValue, Repr, cast};
use crate::layout::Abs;
use crate::text::{FontStyle, FontVariant, Tag};
#[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize)]
pub struct FontAxis {
pub tag: Tag,
pub min: AxisValue,
pub max: AxisValue,
pub default: AxisValue,
}
impl FontAxis {
pub(super) fn distance<T, N>(
&self,
target: T,
parse: impl Fn(AxisValue) -> T,
distance: impl Fn(T, T) -> N,
) -> N
where
T: Ord,
N: Default,
{
let min = parse(self.min);
let max = parse(self.max);
if target < min {
distance(min, target)
} else if target < max {
N::default()
} else {
distance(target, max)
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(transparent)]
pub struct AxisValue(pub f32);
impl AxisValue {
pub fn clamp(self, axis: &FontAxis) -> Self {
AxisValue(self.0.clamp(axis.min.0, axis.max.0))
}
}
impl Display for AxisValue {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", typst_utils::round_with_precision(self.0.into(), 2))
}
}
impl Hash for AxisValue {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.to_bits().hash(state);
}
}
cast! {
AxisValue,
self => (self.0 as f64).into_value(),
v: f64 => Self(v as f32),
}
#[derive(Default, Copy, Clone)]
pub struct StandardAxes<'a> {
pub ital: Option<&'a FontAxis>,
pub slnt: Option<&'a FontAxis>,
pub wght: Option<&'a FontAxis>,
pub wdth: Option<&'a FontAxis>,
pub opsz: Option<&'a FontAxis>,
}
impl<'a> StandardAxes<'a> {
pub const ITAL: Tag = Tag::from_bytes(b"ital");
pub const SLNT: Tag = Tag::from_bytes(b"slnt");
pub const WGHT: Tag = Tag::from_bytes(b"wght");
pub const WDTH: Tag = Tag::from_bytes(b"wdth");
pub const OPSZ: Tag = Tag::from_bytes(b"opsz");
pub const LIST: [Tag; 5] =
[Self::ITAL, Self::SLNT, Self::WGHT, Self::WDTH, Self::OPSZ];
pub fn parse(axes: &'a [FontAxis]) -> Self {
let mut this = StandardAxes::default();
for axis in axes {
match axis.tag {
Self::ITAL => this.ital = Some(axis),
Self::SLNT => this.slnt = Some(axis),
Self::WGHT => this.wght = Some(axis),
Self::WDTH => this.wdth = Some(axis),
Self::OPSZ => this.opsz = Some(axis),
_ => {}
}
}
this
}
pub fn knows(tag: Tag) -> bool {
Self::LIST.contains(&tag)
}
pub fn order(tag: Tag) -> impl Ord {
Self::LIST.iter().position(|&t| t == tag).unwrap_or(Self::LIST.len())
}
}
#[derive(Debug, Default, Clone, PartialEq, Hash)]
pub struct FontVariations(pub SmallVec<[(Tag, AxisValue); 2]>);
impl FontVariations {
pub fn resolve(axes: &[FontAxis], variant: FontVariant, size: Abs) -> Self {
let mut variations = FontVariations::default();
let mut set = |axis: &FontAxis, value: AxisValue| {
variations.0.push((axis.tag, value.clamp(axis)));
};
let axes = StandardAxes::parse(axes);
match (variant.style, axes.ital, axes.slnt) {
(FontStyle::Normal, ..) | (_, None, None) => {}
(FontStyle::Italic, Some(axis), _)
| (FontStyle::Oblique, Some(axis), None) => {
set(axis, AxisValue(axis.max.0.min(1.0)));
}
(FontStyle::Oblique, _, Some(axis))
| (FontStyle::Italic, None, Some(axis)) => {
if axis.min.0 < 0.0 {
set(axis, axis.min);
} else if axis.max.0 > 0.0 {
set(axis, axis.max);
}
}
}
if let Some(axis) = axes.wdth {
set(axis, variant.stretch.to_wdth());
}
if let Some(axis) = axes.wght {
set(axis, variant.weight.to_wght());
}
if let Some(axis) = axes.opsz {
set(axis, AxisValue(size.to_pt() as f32));
}
variations
}
pub fn chain(mut self, other: &FontVariations) -> Self {
self.0.extend_from_slice(&other.0);
self
}
pub fn normalized(mut self) -> Self {
self.0.sort_by_key(|&(tag, _)| tag);
self.0.rdedup_by_key(|&mut (tag, _)| tag);
self
}
}
impl Fold for FontVariations {
fn fold(self, outer: Self) -> Self {
Self(self.0.fold(outer.0))
}
}
cast! {
FontVariations,
self => self.0
.into_iter()
.map(|(tag, num)|(tag.to_str_lossy().into(), num.into_value()))
.collect::<Dict>()
.into_value(),
values: Dict => Self(values
.into_iter()
.enumerate()
.map(|(i, (k, v))| Ok((
k.clone().into_value().cast::<Tag>().hint(tag_hint_helper(i, &k))?,
v.cast::<AxisValue>().hint(tag_hint_helper(i, &k))?
)))
.collect::<HintedStrResult<_>>()?),
}
fn tag_hint_helper(index: usize, key: &impl Repr) -> EcoString {
eco_format!("occurred in tag at index {index} (`{}`)", key.repr())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_axis_value_fmt() {
assert_eq!(format!("{}", AxisValue(100.)), "100");
assert_eq!(format!("{}", AxisValue(-2.5)), "-2.5");
assert_eq!(format!("{}", AxisValue(4.2)), "4.2");
assert_eq!(format!("{}", AxisValue(i16::MAX as f32 + 0.75)), "32767.75");
assert_eq!(format!("{}", AxisValue(f32::NAN)), "NaN");
assert_eq!(format!("{}", AxisValue(f32::INFINITY)), "inf");
}
}