use skia_safe::font_style::{Slant, Weight, Width};
use skia_safe::FontStyle;
use super::face::IntrinsicStyle;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum Toggle {
#[default]
Absent,
Off,
On,
}
impl Toggle {
pub fn from_option(value: Option<bool>) -> Self {
match value {
None => Self::Absent,
Some(false) => Self::Off,
Some(true) => Self::On,
}
}
pub fn is_on(self) -> bool {
matches!(self, Self::On)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct FaceRequest<'a> {
pub name: &'a str,
pub bold: Toggle,
pub italic: Toggle,
}
impl<'a> FaceRequest<'a> {
pub fn new(name: &'a str, bold: Toggle, italic: Toggle) -> Self {
Self { name, bold, italic }
}
pub fn plain(name: &'a str) -> Self {
Self::new(name, Toggle::Absent, Toggle::Absent)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EffectiveStyle {
pub weight: i32,
pub width: Width,
pub slant: Slant,
weight_requested: bool,
slant_requested: bool,
}
impl EffectiveStyle {
pub fn resolve(request: &FaceRequest<'_>, base: IntrinsicStyle) -> Self {
let weight = if request.bold.is_on() {
base.weight.max(*Weight::BOLD)
} else {
base.weight
};
let slant = if request.italic.is_on() {
Slant::Italic
} else {
base.slant
};
Self {
weight,
width: base.width,
slant,
weight_requested: request.bold.is_on(),
slant_requested: request.italic.is_on(),
}
}
pub fn weight_is_requested(&self) -> bool {
self.weight_requested
}
pub fn slant_is_requested(&self) -> bool {
self.slant_requested
}
pub fn is_italic(&self) -> bool {
matches!(self.slant, Slant::Italic | Slant::Oblique)
}
pub fn font_style(&self) -> FontStyle {
FontStyle::new(Weight::from(self.weight), self.width, self.slant)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct Synthesis {
pub embolden: bool,
pub oblique: bool,
}
impl Synthesis {
pub fn compute(request: &FaceRequest<'_>, selected: &IntrinsicStyle) -> Self {
Self {
embolden: request.bold == Toggle::On && !selected.is_already_bold(),
oblique: request.italic == Toggle::On && !selected.is_italic(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::fonts::FaceRequest;
use crate::render::fonts::Toggle;
fn at(weight: i32, slant: Slant) -> IntrinsicStyle {
IntrinsicStyle {
weight,
slant,
..IntrinsicStyle::NEUTRAL
}
}
#[test]
fn the_cascade_s_option_bool_maps_onto_all_three_states() {
assert_eq!(Toggle::from_option(None), Toggle::Absent);
assert_eq!(Toggle::from_option(Some(false)), Toggle::Off);
assert_eq!(Toggle::from_option(Some(true)), Toggle::On);
}
#[test]
fn only_on_is_asking_for_something() {
assert!(Toggle::On.is_on());
assert!(!Toggle::Off.is_on());
assert!(!Toggle::Absent.is_on());
}
#[test]
fn absent_is_the_default() {
assert_eq!(Toggle::default(), Toggle::Absent);
}
#[test]
fn an_unrequested_weight_leaves_a_light_face_light() {
let base = at(342, Slant::Upright);
for toggle in [Toggle::Absent, Toggle::Off] {
let e = EffectiveStyle::resolve(
&FaceRequest::new("Calibri Light", toggle, Toggle::Absent),
base,
);
assert_eq!(e.weight, 342, "{toggle:?} must not thicken the face");
assert!(!e.weight_is_requested());
}
}
#[test]
fn a_family_name_with_no_toggles_still_resolves_to_regular() {
let e = EffectiveStyle::resolve(&FaceRequest::plain("Calibri"), IntrinsicStyle::NEUTRAL);
assert_eq!(e.weight, 400);
assert_eq!(e.width, Width::NORMAL);
assert_eq!(e.slant, Slant::Upright);
assert_eq!(e.font_style(), FontStyle::normal());
}
#[test]
fn an_explicit_bold_raises_a_lighter_face() {
let e = EffectiveStyle::resolve(
&FaceRequest::new("Calibri Light", Toggle::On, Toggle::Absent),
at(342, Slant::Upright),
);
assert_eq!(e.weight, *Weight::BOLD);
assert!(e.weight_is_requested());
}
#[test]
fn an_explicit_bold_never_lowers_a_heavier_face() {
let e = EffectiveStyle::resolve(
&FaceRequest::new("Inter ExtraBold", Toggle::On, Toggle::Absent),
at(*Weight::EXTRA_BOLD, Slant::Upright),
);
assert_eq!(e.weight, *Weight::EXTRA_BOLD);
}
#[test]
fn an_explicit_italic_overrides_an_upright_face() {
let e = EffectiveStyle::resolve(
&FaceRequest::new("Inter", Toggle::Absent, Toggle::On),
at(400, Slant::Upright),
);
assert_eq!(e.slant, Slant::Italic);
assert!(e.is_italic());
assert!(e.slant_is_requested());
}
#[test]
fn an_unrequested_slant_preserves_the_face_s_own() {
for toggle in [Toggle::Absent, Toggle::Off] {
let e = EffectiveStyle::resolve(
&FaceRequest::new("Inter Oblique", Toggle::Absent, toggle),
at(400, Slant::Oblique),
);
assert_eq!(e.slant, Slant::Oblique, "{toggle:?}");
assert!(e.is_italic());
assert!(!e.slant_is_requested());
}
}
#[test]
fn width_is_carried_through_untouched() {
let base = IntrinsicStyle {
width: Width::CONDENSED,
..at(600, Slant::Upright)
};
let e = EffectiveStyle::resolve(
&FaceRequest::new("X Condensed SemiBold", Toggle::On, Toggle::On),
base,
);
assert_eq!(
e.width,
Width::CONDENSED,
"no OOXML toggle addresses width, so the face's own must survive"
);
assert_eq!(*e.font_style().weight(), *Weight::BOLD);
assert_eq!(e.font_style().slant(), Slant::Italic);
}
#[test]
fn both_toggles_compose() {
let e = EffectiveStyle::resolve(
&FaceRequest::new("Inter", Toggle::On, Toggle::On),
IntrinsicStyle::NEUTRAL,
);
assert_eq!(e.font_style(), FontStyle::bold_italic());
}
#[test]
fn bold_requested_against_a_light_face_embolds() {
let s = Synthesis::compute(
&FaceRequest::new("Dx", Toggle::On, Toggle::Absent),
&at(400, Slant::Upright),
);
assert!(s.embolden);
assert!(!s.oblique);
}
#[test]
fn bold_requested_against_a_bold_slot_face_does_not_embold() {
let bold_slot_face = IntrinsicStyle {
weight: 600,
bold_slot: true,
..IntrinsicStyle::NEUTRAL
};
let s = Synthesis::compute(
&FaceRequest::new("Dx SemiBold", Toggle::On, Toggle::Absent),
&bold_slot_face,
);
assert!(!s.embolden);
}
#[test]
fn bold_requested_against_an_unmarked_black_face_does_not_embold() {
let black_no_bit = IntrinsicStyle {
weight: *Weight::BLACK,
bold_slot: false,
..IntrinsicStyle::NEUTRAL
};
let s = Synthesis::compute(
&FaceRequest::new("Dx Black", Toggle::On, Toggle::Absent),
&black_no_bit,
);
assert!(!s.embolden);
}
#[test]
fn off_and_absent_never_embold_even_against_an_already_bold_face() {
let already_bold = IntrinsicStyle {
weight: 600,
bold_slot: true,
..IntrinsicStyle::NEUTRAL
};
for toggle in [Toggle::Off, Toggle::Absent] {
let s = Synthesis::compute(
&FaceRequest::new("Dx SemiBold", toggle, Toggle::Absent),
&already_bold,
);
assert!(!s.embolden, "{toggle:?} must not synthesise");
}
}
#[test]
fn italic_requested_against_an_upright_face_obliques() {
let s = Synthesis::compute(
&FaceRequest::new("Dx", Toggle::Absent, Toggle::On),
&at(400, Slant::Upright),
);
assert!(s.oblique);
assert!(!s.embolden);
}
#[test]
fn italic_requested_against_an_oblique_face_does_not_double_slant() {
let s = Synthesis::compute(
&FaceRequest::new("Dx Oblique", Toggle::Absent, Toggle::On),
&at(400, Slant::Oblique),
);
assert!(!s.oblique);
}
#[test]
fn off_and_absent_never_oblique_even_against_an_already_italic_face() {
let already_italic = at(400, Slant::Italic);
for toggle in [Toggle::Off, Toggle::Absent] {
let s = Synthesis::compute(
&FaceRequest::new("Dx Italic", Toggle::Absent, toggle),
&already_italic,
);
assert!(!s.oblique, "{toggle:?} must not synthesise");
}
}
#[test]
fn both_toggles_synthesise_together_against_a_fully_plain_face() {
let s = Synthesis::compute(
&FaceRequest::new("Dx", Toggle::On, Toggle::On),
&IntrinsicStyle::NEUTRAL,
);
assert!(s.embolden);
assert!(s.oblique);
}
}