use std::sync::{LazyLock, RwLock};
use cosmic_text::{Family, Weight};
use rustc_hash::FxHashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FontFamily {
#[default]
SansSerif,
Serif,
Monospace,
Cursive,
Fantasy,
Name(FamilyId),
}
static FAMILIES: LazyLock<RwLock<FamilyTable>> =
LazyLock::new(|| RwLock::new(FamilyTable::default()));
#[derive(Default)]
struct FamilyTable {
names: Vec<&'static str>,
ids: FxHashMap<&'static str, FamilyId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FamilyId(u32);
impl FontFamily {
pub fn name(name: &str) -> Self {
if let Some(&id) = FAMILIES.read().expect("family table").ids.get(name) {
return FontFamily::Name(id);
}
let mut table = FAMILIES.write().expect("family table");
if let Some(&id) = table.ids.get(name) {
return FontFamily::Name(id);
}
let leaked: &'static str = String::leak(name.to_owned());
let id = FamilyId(table.names.len() as u32);
table.names.push(leaked);
table.ids.insert(leaked, id);
FontFamily::Name(id)
}
pub fn family_name(self) -> Option<&'static str> {
match self {
FontFamily::Name(FamilyId(index)) => FAMILIES
.read()
.expect("family table")
.names
.get(index as usize)
.copied(),
_ => None,
}
}
pub fn to_cosmic(self) -> Family<'static> {
match self {
FontFamily::SansSerif => Family::SansSerif,
FontFamily::Serif => Family::Serif,
FontFamily::Monospace => Family::Monospace,
FontFamily::Cursive => Family::Cursive,
FontFamily::Fantasy => Family::Fantasy,
FontFamily::Name(_) => self.family_name().map_or(Family::SansSerif, Family::Name),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct FontWeight(pub u16);
impl FontWeight {
pub const THIN: Self = Self(100);
pub const EXTRA_LIGHT: Self = Self(200);
pub const LIGHT: Self = Self(300);
pub const NORMAL: Self = Self(400);
pub const MEDIUM: Self = Self(500);
pub const SEMI_BOLD: Self = Self(600);
pub const BOLD: Self = Self(700);
pub const EXTRA_BOLD: Self = Self(800);
pub const BLACK: Self = Self(900);
pub fn to_cosmic(self) -> Weight {
Weight(self.0)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub enum LineHeight {
#[default]
Normal,
Relative(f32),
Absolute(f32),
}
impl From<f32> for LineHeight {
fn from(factor: f32) -> Self {
LineHeight::Relative(factor)
}
}
impl From<f64> for LineHeight {
fn from(factor: f64) -> Self {
LineHeight::Relative(factor as f32)
}
}
crate::reactive::converts!(
f32 => LineHeight,
f64 => LineHeight,
);
impl LineHeight {
pub(crate) fn key(self) -> (u8, u32) {
match self {
LineHeight::Normal => (0, 0),
LineHeight::Relative(factor) => (1, factor.to_bits()),
LineHeight::Absolute(height) => (2, height.to_bits()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_bare_number_is_a_factor() {
assert_eq!(LineHeight::from(1.5_f32), LineHeight::Relative(1.5));
assert_eq!(LineHeight::from(1.5_f64), LineHeight::Relative(1.5));
}
#[test]
fn font_family_default() {
assert_eq!(FontFamily::default(), FontFamily::SansSerif);
}
#[test]
fn font_weight_default() {
assert_eq!(FontWeight::default(), FontWeight(0));
}
#[test]
fn font_weight_constants() {
assert_eq!(FontWeight::NORMAL.0, 400);
assert_eq!(FontWeight::BOLD.0, 700);
}
#[test]
fn a_family_is_eight_bytes_and_a_property_holding_one_is_twelve() {
use std::mem::size_of;
assert_eq!(size_of::<FontFamily>(), 8, "a discriminant and an id");
assert_eq!(
size_of::<crate::reactive::Prop<FontFamily>>(),
size_of::<crate::reactive::Signal<FontFamily>>(),
"so a reactive family costs the signal and not a byte more"
);
fn only_for_copy<T: Copy>(_: T) {}
let family = FontFamily::name("Inter");
only_for_copy(family);
only_for_copy(family);
}
#[test]
fn one_name_is_one_id_however_often_it_is_named() {
assert_eq!(FontFamily::name("Inter"), FontFamily::name("Inter"));
assert_ne!(FontFamily::name("Inter"), FontFamily::name("Iosevka"));
assert_ne!(FontFamily::name("Inter"), FontFamily::Monospace);
assert_eq!(FontFamily::name("Inter").family_name(), Some("Inter"));
assert_eq!(FontFamily::Monospace.family_name(), None);
}
#[test]
fn an_id_survives_the_application_and_the_thread_that_minted_it() {
let _decoy = FontFamily::name("a name nothing else in the suite uses");
let before = FontFamily::name("Iosevka");
crate::app_state::reset();
assert_eq!(
before,
FontFamily::name("Iosevka"),
"the same name still means the same family"
);
assert_eq!(before.family_name(), Some("Iosevka"));
let elsewhere = std::thread::spawn(|| FontFamily::name("Iosevka"))
.join()
.expect("the worker interned a name");
assert_eq!(
elsewhere, before,
"and a family named on another thread is the same family here"
);
assert_eq!(elsewhere.family_name(), Some("Iosevka"));
}
}