use core::fmt::Debug;
use nami::{Computed, Signal, impl_constant};
use waterui_core::{
Environment, Str,
resolve::{self, AnyResolvable, Resolvable},
};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Font(AnyResolvable<ResolvedFont>);
impl Default for Font {
fn default() -> Self {
Self::new(Body)
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ResolvedFont {
pub size: f32,
pub weight: FontWeight,
pub line_height: Option<f32>,
pub letter_spacing: f32,
pub family: Option<Str>,
pub design: FontDesign,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FontDesign {
#[default]
Default,
Monospaced,
}
impl ResolvedFont {
#[must_use]
pub const fn new(size: f32, weight: FontWeight) -> Self {
Self {
size,
weight,
line_height: None,
letter_spacing: 0.0,
family: None,
design: FontDesign::Default,
}
}
#[must_use]
pub fn with_family(size: f32, weight: FontWeight, family: impl Into<Str>) -> Self {
Self {
size,
weight,
line_height: None,
letter_spacing: 0.0,
family: Some(family.into()),
design: FontDesign::Default,
}
}
#[must_use]
pub const fn with_static_family(size: f32, weight: FontWeight, family: &'static str) -> Self {
Self {
size,
weight,
line_height: None,
letter_spacing: 0.0,
family: Some(Str::from_static(family)),
design: FontDesign::Default,
}
}
#[must_use]
pub const fn with_design(mut self, design: FontDesign) -> Self {
self.design = design;
self
}
#[must_use]
pub const fn with_typography_metrics(mut self, line_height: f32, letter_spacing: f32) -> Self {
self.line_height = Some(line_height);
self.letter_spacing = letter_spacing;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FontWeight {
Thin,
UltraLight,
Light,
#[default]
Normal,
Medium,
SemiBold,
Bold,
UltraBold,
Black,
}
impl_constant!(Font, ResolvedFont, FontWeight, FontDesign);
impl Font {
pub fn new(font: impl Resolvable<Resolved = ResolvedFont> + 'static) -> Self {
Self(AnyResolvable::new(font))
}
#[must_use]
pub fn weight(self, weight: FontWeight) -> Self {
Self::new(resolve::Map::new(self.0, move |mut font| {
font.weight = weight;
font
}))
}
#[must_use]
pub fn size(self, size: f32) -> Self {
Self::new(resolve::Map::new(self.0, move |mut font| {
font.size = size;
font.line_height = None;
font.letter_spacing = 0.0;
font
}))
}
#[must_use]
pub fn family(self, family: impl Into<Str> + Clone + 'static) -> Self {
Self::new(resolve::Map::new(self.0, move |mut font| {
font.family = Some(family.clone().into());
font
}))
}
#[must_use]
pub fn design(self, design: FontDesign) -> Self {
Self::new(resolve::Map::new(self.0, move |mut font| {
font.design = design;
font
}))
}
#[must_use]
pub fn monospaced(self) -> Self {
self.design(FontDesign::Monospaced)
}
#[must_use]
pub fn line_height(self, line_height: f32) -> Self {
Self::new(resolve::Map::new(self.0, move |mut font| {
font.line_height = Some(line_height);
font
}))
}
#[must_use]
pub fn letter_spacing(self, letter_spacing: f32) -> Self {
Self::new(resolve::Map::new(self.0, move |mut font| {
font.letter_spacing = letter_spacing;
font
}))
}
#[must_use]
pub fn bold(self) -> Self {
self.weight(FontWeight::Bold)
}
#[must_use]
pub fn resolve(&self, env: &Environment) -> Computed<ResolvedFont> {
self.0.resolve(env)
}
}
pub trait FontSlot {
const DEFAULT: ResolvedFont;
}
macro_rules! impl_font {
($name:ident, $doc:expr, $size:expr, $weight:expr) => {
#[doc = $doc]
#[derive(Debug, Clone, Copy)]
pub struct $name;
impl FontSlot for $name {
const DEFAULT: ResolvedFont = ResolvedFont::new($size, $weight);
}
impl Resolvable for $name {
type Resolved = ResolvedFont;
fn resolve(&self, env: &Environment) -> impl Signal<Output = Self::Resolved> {
env.query::<Self, Computed<Self::Resolved>>()
.cloned()
.expect(concat!(
stringify!($name),
" font token is not installed in the environment"
))
}
}
impl From<$name> for Font {
fn from(value: $name) -> Self {
Self::new(value)
}
}
impl_constant!($name);
};
}
impl_font!(Body, "Body font style.", 16.0, FontWeight::Normal);
impl_font!(Title, "Title font style.", 22.0, FontWeight::Normal);
impl_font!(Headline, "Headline font style.", 24.0, FontWeight::Normal);
impl_font!(
Subheadline,
"Subheadline font style.",
16.0,
FontWeight::Medium
);
impl_font!(Caption, "Caption font style.", 12.0, FontWeight::Normal);
impl_font!(Footnote, "Footnote font style.", 11.0, FontWeight::Medium);
#[cfg(test)]
mod tests {
use super::*;
fn env_with_body(font: ResolvedFont) -> Environment {
Environment::new().store::<Body, Computed<ResolvedFont>>(Computed::constant(font))
}
#[test]
fn size_drops_the_previous_face_typography_metrics() {
let slot = ResolvedFont::new(17.0, FontWeight::Normal).with_typography_metrics(22.0, 0.4);
let env = env_with_body(slot);
let resized = Font::new(Body).size(14.0).resolve(&env).get();
assert!((resized.size - 14.0).abs() < f32::EPSILON);
assert_eq!(resized.line_height, None);
assert!(resized.letter_spacing.abs() < f32::EPSILON);
let declared = Font::new(Body)
.size(14.0)
.line_height(18.0)
.resolve(&env)
.get();
assert_eq!(declared.line_height, Some(18.0));
}
}