use crate::style::TEXT;
use crate::{
font_metrics::{FONT_METRICS, FontMetrics},
style::Style,
};
use alloc::borrow::Cow;
use bon::bon;
use core::cmp;
use core::ptr;
use strum::EnumString;
#[derive(Debug, Clone, PartialEq, Eq, EnumString)]
#[strum(serialize_all = "lowercase")]
pub enum FontWeight {
TextBf, TextMd, #[strum(serialize = "")]
Empty, }
impl FontWeight {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::TextBf => "textbf",
Self::TextMd => "textmd",
Self::Empty => "",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FontShape {
TextIt,
TextUp,
Empty, }
impl FontShape {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::TextIt => "textit",
Self::TextUp => "textup",
Self::Empty => "",
}
}
}
impl From<&str> for FontShape {
fn from(s: &str) -> Self {
match s {
"textit" => Self::TextIt,
"textup" => Self::TextUp,
_ => Self::Empty,
}
}
}
const SIZE_STYLE_MAP: [[usize; 3]; 11] = [
[1, 1, 1], [2, 1, 1], [3, 1, 1], [4, 2, 1], [5, 2, 1], [6, 3, 1], [7, 4, 2], [8, 6, 3], [9, 7, 6], [10, 8, 7], [11, 10, 9], ];
const SIZE_MULTIPLIERS: [f64; 11] = [0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.2, 1.44, 1.728, 2.074, 2.488];
const fn size_at_style(size: usize, style: &Style) -> usize {
if style.size < 2 {
size
} else {
SIZE_STYLE_MAP[size - 1][style.size - 1]
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Options {
pub style: &'static Style,
pub color: Option<String>,
pub size: usize,
pub text_size: usize,
pub phantom: bool,
pub font: String,
pub font_family: String,
pub font_weight: FontWeight,
pub font_shape: FontShape,
pub size_multiplier: f64,
pub max_size: f64,
pub min_rule_thickness: f64,
}
#[bon]
impl Options {
#[builder]
pub fn new(
style: &'static Style,
color: Option<String>,
size: Option<usize>,
text_size: Option<usize>,
phantom: Option<bool>,
font: Option<String>,
font_family: Option<String>,
font_weight: Option<FontWeight>,
font_shape: Option<FontShape>,
max_size: f64,
min_rule_thickness: f64,
) -> Self {
let size = size.unwrap_or(Self::BASESIZE);
let multiplier_idx = cmp::min(size, SIZE_MULTIPLIERS.len());
let size_multiplier = SIZE_MULTIPLIERS[multiplier_idx - 1];
Self {
style,
color,
size,
text_size: text_size.unwrap_or(size),
phantom: phantom.unwrap_or(false),
font: font.unwrap_or_default(),
font_family: font_family.unwrap_or_default(),
font_weight: font_weight.unwrap_or(FontWeight::Empty),
font_shape: font_shape.unwrap_or(FontShape::Empty),
size_multiplier,
max_size,
min_rule_thickness,
}
}
}
impl Default for Options {
fn default() -> Self {
Self {
style: TEXT,
color: None,
size: Self::BASESIZE,
text_size: Self::BASESIZE,
phantom: false,
font: String::new(),
font_family: String::new(),
font_weight: FontWeight::Empty,
font_shape: FontShape::Empty,
size_multiplier: SIZE_MULTIPLIERS[Self::BASESIZE - 1],
max_size: 1000.0,
min_rule_thickness: 0.04,
}
}
}
impl Options {
pub const BASESIZE: usize = 6;
#[must_use]
pub fn having_style(&self, style: &'static Style) -> Self {
if ptr::eq(self.style, style) {
self.clone()
} else {
let size = size_at_style(self.text_size, style);
let mut new_options = self.clone();
new_options.style = style;
new_options.size = size;
new_options.size_multiplier = SIZE_MULTIPLIERS[size - 1];
new_options
}
}
#[must_use]
pub fn having_cramped_style(&self) -> Self {
self.having_style(self.style.cramp())
}
#[must_use]
pub fn having_size(&self, size: usize) -> Self {
if self.size == size && self.text_size == size {
self.clone()
} else {
let mut new_options = self.clone();
new_options.style = TEXT;
new_options.size = size;
new_options.text_size = size;
new_options.size_multiplier = SIZE_MULTIPLIERS[size - 1];
new_options
}
}
#[must_use]
pub fn having_base_style(&self, style: Option<&'static Style>) -> Self {
let style = style.unwrap_or_else(|| self.style.text());
let want_size = size_at_style(Self::BASESIZE, style);
if self.size == want_size && self.text_size == Self::BASESIZE && self.style == style {
self.clone()
} else {
let mut new_options = self.clone();
new_options.style = style;
new_options.size = want_size;
new_options.size_multiplier = SIZE_MULTIPLIERS[want_size - 1];
new_options
}
}
#[must_use]
pub fn having_base_sizing(&self) -> Self {
let size = match self.style.id {
4 | 5 => 3, 6 | 7 => 1, _ => 6, };
let mut new_options = self.clone();
new_options.style = self.style.text();
new_options.size = size;
new_options.size_multiplier = SIZE_MULTIPLIERS[size - 1];
new_options
}
#[must_use]
pub fn with_color(&self, color: &str) -> Self {
let mut new_options = self.clone();
new_options.color = Some(color.to_owned());
new_options
}
#[must_use]
pub fn with_phantom(&self) -> Self {
let mut new_options = self.clone();
new_options.phantom = true;
new_options
}
#[must_use]
pub fn with_font(&self, font: String) -> Self {
let mut new_options = self.clone();
new_options.font = font;
new_options
}
#[must_use]
pub fn with_text_font_family(&self, font_family: String) -> Self {
let mut new_options = self.clone();
new_options.font_family = font_family;
new_options.font = String::new();
new_options
}
#[must_use]
pub fn with_text_font_weight(&self, font_weight: FontWeight) -> Self {
let mut new_options = self.clone();
new_options.font_weight = font_weight;
new_options.font.clear();
new_options
}
#[must_use]
pub fn with_text_font_shape(&self, font_shape: FontShape) -> Self {
let mut new_options = self.clone();
new_options.font_shape = font_shape;
new_options.font.clear();
new_options
}
#[must_use]
pub fn sizing_classes(&self, old_options: &Self) -> Vec<Cow<'static, str>> {
if old_options.size == self.size {
vec![]
} else {
vec![
Cow::Borrowed("sizing"),
Cow::Owned(format!("reset-size{}", old_options.size)),
Cow::Owned(format!("size{}", self.size)),
]
}
}
#[must_use]
pub fn base_sizing_classes(&self) -> Vec<Cow<'static, str>> {
if self.size == Self::BASESIZE {
vec![]
} else {
vec![
Cow::Borrowed("sizing"),
Cow::Owned(format!("reset-size{}", self.size)),
Cow::Owned(format!("size{}", Self::BASESIZE)),
]
}
}
#[must_use]
pub fn get_color(&self) -> Option<&str> {
if self.phantom {
Some("transparent")
} else {
self.color.as_deref()
}
}
#[must_use]
pub const fn font_metrics(&self) -> &FontMetrics {
use crate::font_metrics::FontSizeIndex;
let size_index: FontSizeIndex = if self.size >= 5 {
0
} else if self.size >= 3 {
1
} else {
2
};
&FONT_METRICS[size_index as usize]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::{DISPLAY, SCRIPT, SCRIPTSCRIPT, TEXT};
#[test]
fn test_font_shape_conversion() {
assert_eq!(FontShape::from("textit"), FontShape::TextIt);
assert_eq!(FontShape::from("textup"), FontShape::TextUp);
assert_eq!(FontShape::from("unknown"), FontShape::Empty);
assert_eq!(FontShape::TextIt.as_str(), "textit");
assert_eq!(FontShape::TextUp.as_str(), "textup");
assert_eq!(FontShape::Empty.as_str(), "");
}
#[test]
fn test_size_at_style() {
assert_eq!(size_at_style(6, DISPLAY), 6);
assert_eq!(size_at_style(6, SCRIPT), 3);
assert_eq!(size_at_style(6, SCRIPTSCRIPT), 1);
}
#[test]
fn test_options_creation() {
let options = Options::builder()
.style(TEXT)
.color("red".to_owned())
.size(5)
.text_size(5)
.font("Main-Regular".to_owned())
.font_family("Main".to_owned())
.font_weight(FontWeight::TextBf)
.font_shape(FontShape::TextIt)
.max_size(1000.0)
.min_rule_thickness(0.04)
.build();
assert_eq!(options.style, TEXT);
assert_eq!(options.color, Some("red".to_owned()));
assert_eq!(options.size, 5);
assert_eq!(options.text_size, 5);
assert_eq!(options.font, "Main-Regular");
assert_eq!(options.font_family, "Main");
assert_eq!(options.font_weight, FontWeight::TextBf);
assert_eq!(options.font_shape, FontShape::TextIt);
assert_eq!(options.size_multiplier, SIZE_MULTIPLIERS[4]); assert_eq!(options.max_size, 1000.0);
assert_eq!(options.min_rule_thickness, 0.04);
}
}