use std::{collections::HashMap, fmt, iter, num::ParseFloatError, sync::Arc};
use itertools::Itertools;
use log::warn;
use rmpv::Value;
use serde::Deserialize;
use skia_safe::{
FontStyle,
font_style::{Slant, Weight, Width},
};
use crate::{editor, error_msg, settings::ParseFromValue};
pub const DEFAULT_FONT_SIZE: f32 = 14.0;
const FONT_OPTS_SEPARATOR: char = ':';
const FONT_LIST_SEPARATOR: char = ',';
const FONT_HINTING_PREFIX: &str = "#h-";
const FONT_EDGING_PREFIX: &str = "#e-";
const FONT_HEIGHT_PREFIX: char = 'h';
const FONT_WIDTH_PREFIX: char = 'w';
const FONT_BOLD_OPT: &str = "b";
const FONT_ITALIC_OPT: &str = "i";
const INVALID_SIZE_ERR: &str = "Invalid size";
const INVALID_WIDTH_ERR: &str = "Invalid width";
#[derive(Clone, Debug, Deserialize, PartialEq, Hash, Eq, Default)]
pub struct FontDescription {
pub family: String,
pub style: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct SecondaryFontDescription {
pub family: Option<String>,
pub style: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct FontFeature(pub String, pub u16);
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct CoarseStyle {
bold: bool,
italic: bool,
}
impl CoarseStyle {
pub fn name(&self) -> Option<&'static str> {
let name = match (self.bold, self.italic) {
(true, true) => "Bold Italic",
(true, false) => "Bold",
(false, true) => "Italic",
(false, false) => return None,
};
Some(name)
}
pub fn permutations() -> impl Iterator<Item = CoarseStyle> {
iter::repeat_n([true, false], 2)
.multi_cartesian_product()
.map(|values| CoarseStyle { bold: values[0], italic: values[1] })
}
}
impl From<CoarseStyle> for FontStyle {
fn from(CoarseStyle { bold, italic }: CoarseStyle) -> Self {
match (bold, italic) {
(true, true) => FontStyle::bold_italic(),
(true, false) => FontStyle::bold(),
(false, true) => FontStyle::italic(),
(false, false) => FontStyle::normal(),
}
}
}
impl From<&editor::Style> for CoarseStyle {
fn from(fine_style: &editor::Style) -> Self {
Self { bold: fine_style.bold, italic: fine_style.italic }
}
}
impl From<&Arc<editor::Style>> for CoarseStyle {
fn from(fine_style: &Arc<editor::Style>) -> Self {
Self::from(&**fine_style)
}
}
#[derive(Clone, Debug)]
pub struct FontOptions {
pub normal: Vec<FontDescription>,
pub italic: Option<Vec<SecondaryFontDescription>>,
pub bold: Option<Vec<SecondaryFontDescription>>,
pub bold_italic: Option<Vec<SecondaryFontDescription>>,
pub features: HashMap<String , Vec<FontFeature> >,
pub size: f32,
pub width: f32,
pub hinting: FontHinting,
pub edging: FontEdging,
pub underline_offset: Option<f32>,
}
impl FontFeature {
pub fn parse(feature: &str) -> Result<Self, &str> {
if let Some(name) = feature.strip_prefix('+') {
Ok(FontFeature(name.trim().to_string(), 1u16))
} else if let Some(name) = feature.strip_prefix('-') {
Ok(FontFeature(name.trim().to_string(), 0u16))
} else if let Some((name, value)) = feature.split_once('=') {
let value = value.parse();
if let Ok(value) = value {
Ok(FontFeature(name.to_string(), value))
} else {
warn!("Wrong feature format: {feature}");
Err(feature)
}
} else {
warn!("Wrong feature format: {feature}");
Err(feature)
}
}
}
impl FontOptions {
pub fn parse(guifont_setting: &str) -> Result<FontOptions, &str> {
let mut font_options = FontOptions::default();
let mut parts = guifont_setting.split(FONT_OPTS_SEPARATOR).filter(|part| !part.is_empty());
if let Some(parts) = parts.next() {
let parsed_font_list = parts
.split(FONT_LIST_SEPARATOR)
.map(str::trim_ascii)
.filter(|fallback| !fallback.is_empty())
.map(parse_font_name)
.collect_vec();
if !parsed_font_list.is_empty() {
font_options.normal = parsed_font_list
.into_iter()
.map(|family| FontDescription { family, style: None })
.collect();
}
}
let mut style: Vec<String> = vec![];
for part in parts {
if let Some(hinting_string) = part.strip_prefix(FONT_HINTING_PREFIX) {
font_options.hinting = FontHinting::parse(hinting_string)?;
} else if let Some(edging_string) = part.strip_prefix(FONT_EDGING_PREFIX) {
font_options.edging = FontEdging::parse(edging_string)?;
} else if part.starts_with(FONT_HEIGHT_PREFIX) && part.len() > 1 {
font_options.size = parse_pixels(part).map_err(|_| INVALID_SIZE_ERR)?;
} else if part.starts_with(FONT_WIDTH_PREFIX) && part.len() > 1 {
font_options.width = parse_pixels(part).map_err(|_| INVALID_WIDTH_ERR)?;
} else if part == FONT_BOLD_OPT {
style.push("Bold".to_string());
} else if part == FONT_ITALIC_OPT {
style.push("Italic".to_string());
}
}
let style = if style.is_empty() {
None
} else {
Some(style.into_iter().unique().sorted().join(" "))
};
for font in font_options.normal.iter_mut() {
font.style.clone_from(&style);
}
Ok(font_options)
}
pub fn primary_font(&self) -> Option<FontDescription> {
self.normal.first().cloned()
}
pub fn font_list(&self, style: CoarseStyle) -> Vec<FontDescription> {
if style == CoarseStyle::default() {
return self.normal.clone();
}
let fonts = match (style.bold, style.italic) {
(true, true) => &self.bold_italic,
(true, false) => &self.bold,
(false, true) => &self.italic,
(false, false) => &None,
};
let normal_fallback = self.normal.iter().map(|font| FontDescription {
style: style.name().map(str::to_string),
family: font.family.clone(),
});
fonts
.as_ref()
.map(|fonts| {
fonts
.iter()
.filter(|font| font.family.is_some() || font.style.is_some())
.map(|font| {
if font.family.is_none() && self.primary_font().is_some() {
error_msg!("Font style {:?} is missing font family", font.style);
self.primary_font()
.map(|primary_font| FontDescription {
family: primary_font.family.clone(),
style: font.style.clone(),
})
.unwrap()
} else {
FontDescription {
family: font.family.clone().unwrap(),
style: font
.style
.clone()
.or_else(|| style.name().map(str::to_string)),
}
}
})
.chain(normal_fallback.clone())
.collect()
})
.unwrap_or_else(|| normal_fallback.collect())
}
pub fn possible_fonts(&self) -> Vec<FontDescription> {
CoarseStyle::permutations()
.flat_map(|style| self.font_list(style))
.collect()
}
}
impl Default for FontOptions {
fn default() -> Self {
FontOptions {
normal: Vec::new(),
italic: None,
bold: None,
bold_italic: None,
features: HashMap::new(),
size: points_to_pixels(DEFAULT_FONT_SIZE),
width: 0.0,
hinting: FontHinting::default(),
edging: FontEdging::default(),
underline_offset: None,
}
}
}
impl PartialEq for FontOptions {
fn eq(&self, other: &Self) -> bool {
self.normal == other.normal
&& self.bold == other.bold
&& self.italic == other.italic
&& self.bold_italic == other.bold_italic
&& self.features == other.features
&& self.edging == other.edging
&& (self.size - other.size).abs() < f32::EPSILON
&& self.hinting == other.hinting
}
}
fn parse_pixels(part: &str) -> Result<f32, ParseFloatError> {
Ok(points_to_pixels(part[1..].parse::<f32>()?))
}
fn parse_font_name(font_name: impl AsRef<str>) -> String {
let parsed_font_name = font_name
.as_ref()
.chars()
.batching(|iter| {
let ch = iter.next();
match ch? {
'\\' => iter.next(),
'_' => Some(' '),
_ => ch,
}
})
.collect();
parsed_font_name
}
#[derive(Clone, Debug, Hash, PartialEq, Eq, Default)]
pub enum FontEdging {
#[default]
AntiAlias,
SubpixelAntiAlias,
Alias,
}
impl FontEdging {
const INVALID_ERR: &'static str = "Invalid edging";
pub fn parse(value: &str) -> Result<Self, &str> {
match value {
"antialias" => Ok(Self::AntiAlias),
"subpixelantialias" => Ok(Self::SubpixelAntiAlias),
"alias" => Ok(Self::Alias),
_ => Err(Self::INVALID_ERR),
}
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq, Default)]
pub enum FontHinting {
#[default]
Full,
Normal,
Slight,
None,
}
impl FontHinting {
const INVALID_ERR: &'static str = "Invalid hinting";
pub fn parse(value: &str) -> Result<Self, &str> {
match value {
"full" => Ok(Self::Full),
"normal" => Ok(Self::Normal),
"slight" => Ok(Self::Slight),
"none" => Ok(Self::None),
_ => Err(Self::INVALID_ERR),
}
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq, Default)]
#[allow(clippy::upper_case_acronyms)]
pub enum PixelGeometry {
#[default]
Unknown,
RGBH,
BGRH,
RGBV,
BGRV,
}
impl ParseFromValue for PixelGeometry {
fn parse_from_value(&mut self, value: Value) {
match value.as_str() {
Some("Unknown") => *self = Self::Unknown,
Some("RGBH") => *self = Self::RGBH,
Some("BGRH") => *self = Self::BGRH,
Some("RGBV") => *self = Self::RGBV,
Some("BGRV") => *self = Self::BGRV,
_ => {
error_msg!(
"Setting expected \"RGBH\", \"BGRH\", \"RGBV\", \"BGRV\", or \"Unknown\", but received {value:?}"
);
}
}
}
}
impl std::convert::From<PixelGeometry> for Value {
fn from(value: PixelGeometry) -> Self {
Value::from(match value {
PixelGeometry::Unknown => "Unknown",
PixelGeometry::RGBH => "RGBH",
PixelGeometry::BGRH => "BGRH",
PixelGeometry::RGBV => "RGBV",
PixelGeometry::BGRV => "BGRV",
})
}
}
impl From<PixelGeometry> for skia_safe::PixelGeometry {
fn from(value: PixelGeometry) -> Self {
match value {
PixelGeometry::Unknown => Self::Unknown,
PixelGeometry::RGBH => Self::RGBH,
PixelGeometry::BGRH => Self::BGRH,
PixelGeometry::RGBV => Self::RGBV,
PixelGeometry::BGRV => Self::BGRV,
}
}
}
pub fn points_to_pixels(value: f32) -> f32 {
if cfg!(target_os = "macos") {
value
} else {
let pixels_per_inch = 96.0;
let points_per_inch = 72.0;
value * (pixels_per_inch / points_per_inch)
}
}
impl FontDescription {
pub fn as_family_and_font_style(&self) -> (&str, FontStyle) {
let style = if let Some(style) = &self.style {
let mut weight = Weight::NORMAL;
let mut slant = Slant::Upright;
for part in style.split_whitespace() {
match part {
"Thin" => weight = Weight::THIN,
"ExtraLight" => weight = Weight::EXTRA_LIGHT,
"Light" => weight = Weight::LIGHT,
"Normal" => weight = Weight::NORMAL,
"Medium" => weight = Weight::MEDIUM,
"SemiBold" => weight = Weight::SEMI_BOLD,
"Bold" => weight = Weight::BOLD,
"ExtraBold" => weight = Weight::EXTRA_BOLD,
"Black" => weight = Weight::BLACK,
"ExtraBlack" => weight = Weight::EXTRA_BLACK,
"Italic" => slant = Slant::Italic,
"Oblique" => slant = Slant::Oblique,
_ => {
if let Some(rest) = part.strip_prefix('W') {
if let Ok(weight_value) = rest.parse::<i32>() {
weight = Weight::from(weight_value);
}
}
}
}
}
FontStyle::new(weight, Width::NORMAL, slant)
} else {
FontStyle::default()
};
(self.family.as_str(), style)
}
}
impl fmt::Display for FontDescription {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.family)?;
if let Some(style) = &self.style {
write!(f, " {style}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_one_font_from_guifont_setting() {
let guifont_setting = "Fira Code Mono";
let font_options = FontOptions::parse(guifont_setting).unwrap();
assert_eq!(
font_options.normal.len(),
1,
"font list length should equal {}, but {}",
1,
font_options.normal.len(),
);
}
#[test]
fn test_parse_many_fonts_from_guifont_setting() {
let guifont_setting = "Fira Code Mono,Console";
let font_options = FontOptions::parse(guifont_setting).unwrap();
assert_eq!(
font_options.normal.len(),
2,
"font list length should equal {}, but {}",
2,
font_options.normal.len(),
);
}
#[test]
fn test_parse_edging_from_guifont_setting() {
let guifont_setting = "Fira Code Mono:#e-subpixelantialias";
let font_options = FontOptions::parse(guifont_setting).unwrap();
assert_eq!(
font_options.edging,
FontEdging::SubpixelAntiAlias,
"font edging should equal {:?}, but {:?}",
FontEdging::SubpixelAntiAlias,
font_options.edging,
);
}
#[test]
fn test_parse_invalid_edging_from_guifont_setting() {
let guifont_setting = "Fira Code Mono:#e-aliens";
let err = FontOptions::parse(guifont_setting).unwrap_err();
assert_eq!(
err,
FontEdging::INVALID_ERR,
"parse error should equal {:?}, but {:?}",
FontEdging::INVALID_ERR,
err,
);
}
#[test]
fn test_parse_hinting_from_guifont_setting() {
let guifont_setting = "Fira Code Mono:#h-slight";
let font_options = FontOptions::parse(guifont_setting).unwrap();
assert_eq!(
font_options.hinting,
FontHinting::Slight,
"font hinting should equal {:?}, but {:?}",
FontHinting::Slight,
font_options.hinting,
);
}
#[test]
fn test_parse_invalid_hinting_from_guifont_setting() {
let guifont_setting = "Fira Code Mono:#h-fool";
let err = FontOptions::parse(guifont_setting).unwrap_err();
assert_eq!(
err,
FontHinting::INVALID_ERR,
"parse error should equal {:?}, but {:?}",
FontHinting::INVALID_ERR,
err,
);
}
#[test]
fn test_parse_font_size_float_from_guifont_setting() {
let guifont_setting = "Fira Code Mono:h15.5";
let font_options = FontOptions::parse(guifont_setting).unwrap();
let font_size_pixels = points_to_pixels(15.5);
assert_eq!(
font_options.size, font_size_pixels,
"font size should equal {}, but {}",
font_size_pixels, font_options.size,
);
}
#[test]
fn test_parse_invalid_font_size_float_from_guifont_setting() {
let guifont_setting = "Fira Code Mono:h15.a";
let err = FontOptions::parse(guifont_setting).unwrap_err();
assert_eq!(err, INVALID_SIZE_ERR, "parse err should equal {INVALID_SIZE_ERR}, but {err}",);
}
#[test]
fn test_parse_invalid_font_width_float_from_guifont_setting() {
let guifont_setting = "Fira Code Mono:w1.b";
let err = FontOptions::parse(guifont_setting).unwrap_err();
assert_eq!(err, INVALID_WIDTH_ERR, "parse err should equal {INVALID_WIDTH_ERR}, but {err}",);
}
#[test]
#[allow(clippy::bool_assert_comparison)]
fn test_parse_all_params_together_from_guifont_setting() {
let guifont_setting = "Fira Code Mono:h15.5:b:i:#h-slight:#e-alias";
let font_options = FontOptions::parse(guifont_setting).unwrap();
let font_size_pixels = points_to_pixels(15.5);
assert_eq!(
font_options.size, font_size_pixels,
"font size should equal {}, but {}",
font_size_pixels, font_options.size,
);
for font in font_options.normal.iter() {
assert_eq!(
font.family, "Fira Code Mono",
"font family should equal {}, but {}",
"Fira Code Mono", font.family,
);
assert_eq!(
font.style,
Some("Bold Italic".to_string()),
"font style should equal {:?}, but {:?}",
Some("Bold Italic".to_string()),
font.style,
);
}
assert_eq!(
font_options.edging,
FontEdging::Alias,
"font hinting should equal {:?}, but {:?}",
FontEdging::Alias,
font_options.edging,
);
assert_eq!(
font_options.hinting,
FontHinting::Slight,
"font hinting should equal {:?}, but {:?}",
FontHinting::Slight,
font_options.hinting,
);
}
#[test]
fn test_parse_font_name_with_escapes() {
let without_escapes_or_specials_chars = parse_font_name("Fira Code Mono");
let without_escapes = parse_font_name("Fira_Code_Mono");
let with_escapes = parse_font_name(r"Fira\_Code\_Mono");
let with_too_many_escapes = parse_font_name(r"Fira\\_Code\\_Mono");
let ignored_escape_at_the_end = parse_font_name(r"Fira_Code_Mono\");
assert_eq!(
without_escapes_or_specials_chars, "Fira Code Mono",
"font name should equal {}, but {}",
without_escapes_or_specials_chars, "Fira Code Mono"
);
assert_eq!(
without_escapes, "Fira Code Mono",
"font name should equal {}, but {}",
without_escapes, "Fira Code Mono"
);
assert_eq!(
with_escapes, "Fira_Code_Mono",
"font name should equal {}, but {}",
with_escapes, "Fira_Code_Mono"
);
assert_eq!(
with_too_many_escapes, "Fira\\ Code\\ Mono",
"font name should equal {}, but {}",
with_too_many_escapes, "Fira\\ Code\\ Mono"
);
assert_eq!(
ignored_escape_at_the_end, "Fira Code Mono",
"font name should equal {}, but {}",
ignored_escape_at_the_end, "Fira Code Mono"
)
}
#[test]
fn test_parse_font_style() {
let font_style = FontDescription {
family: "Fira Code Mono".to_string(),
style: Some("Bold Italic".to_string()),
};
let (family, style) = font_style.as_family_and_font_style();
assert_eq!(
family, "Fira Code Mono",
"font family should equal {}, but {}",
family, "Fira Code Mono"
);
assert_eq!(style.weight(), Weight::BOLD);
assert_eq!(style.slant(), Slant::Italic);
}
#[test]
fn test_parse_font_style_semibold() {
let font_style = FontDescription {
family: "Fira Code Mono".to_string(),
style: Some("SemiBold".to_string()),
};
let (family, style) = font_style.as_family_and_font_style();
assert_eq!(
family, "Fira Code Mono",
"font family should equal {}, but {}",
family, "Fira Code Mono"
);
assert_eq!(style.weight(), Weight::SEMI_BOLD);
assert_eq!(style.slant(), Slant::Upright);
}
#[test]
fn test_parse_font_style_variable_weight() {
let font_style = FontDescription {
family: "Fira Code Mono".to_string(),
style: Some("W100".to_string()),
};
let (family, style) = font_style.as_family_and_font_style();
assert_eq!(
family, "Fira Code Mono",
"font family should equal {}, but {}",
family, "Fira Code Mono"
);
assert_eq!(style.weight(), Weight::from(100));
assert_eq!(style.slant(), Slant::Upright);
}
}