1use gpui::{Font, FontStyle, FontWeight, font};
2
3#[derive(Clone, Debug, PartialEq)]
8pub struct ShellTypography {
9 pub font: Font,
10 pub size: f32,
12}
13
14impl Default for ShellTypography {
15 fn default() -> Self {
16 Self {
17 font: font(".SystemUIFont"),
18 size: 14.0,
19 }
20 }
21}
22
23impl ShellTypography {
24 pub(crate) fn normalized(mut self) -> Self {
25 if self.font.family.trim().is_empty() {
26 self.font.family = ".SystemUIFont".into();
27 }
28 self.size = if self.size.is_finite() {
29 self.size.clamp(8.0, 48.0)
30 } else {
31 14.0
32 };
33 self.font.weight = if self.font.weight.0.is_finite() {
34 FontWeight(self.font.weight.0.clamp(100.0, 900.0))
35 } else {
36 FontWeight::NORMAL
37 };
38 self
39 }
40
41 pub fn from_desktop_description(description: &str) -> Option<Self> {
48 if description.chars().any(char::is_control) {
49 return None;
50 }
51 let description = description.trim();
52 if description.is_empty() {
53 return None;
54 }
55 let fields: Vec<_> = description.split(',').map(str::trim).collect();
56 let parsed = if fields.len() >= 10 && fields[1].parse::<f32>().is_ok() {
57 qfont(&fields)
58 } else {
59 pango(description)
60 }?;
61 Some(parsed.normalized())
62 }
63}
64
65fn positive(value: &str) -> Option<f32> {
66 value
67 .parse::<f32>()
68 .ok()
69 .filter(|v| v.is_finite() && *v > 0.0)
70}
71
72fn qfont(fields: &[&str]) -> Option<ShellTypography> {
73 let family = fields.first()?.trim();
74 if family.is_empty() {
75 return None;
76 }
77 let size = positive(fields.get(1)?)
78 .map(|pt| pt * 96.0 / 72.0)
79 .or_else(|| positive(fields.get(2)?))?;
80 let weight = fields.get(4)?.parse::<u16>().ok()?;
81 let weight = if weight < 100 {
82 match weight {
85 0..=11 => 100.0,
86 12..=24 => 200.0,
87 25..=49 => 300.0,
88 50..=56 => 400.0,
89 57..=62 => 500.0,
90 63..=74 => 600.0,
91 75..=80 => 700.0,
92 81..=86 => 800.0,
93 _ => 900.0,
94 }
95 } else if weight <= 900 {
96 weight as f32
97 } else {
98 return None;
99 };
100 let style = match *fields.get(5)? {
101 "0" => FontStyle::Normal,
102 "1" => FontStyle::Italic,
103 "2" => FontStyle::Oblique,
104 _ => return None,
105 };
106 let mut font = font(family.to_owned());
107 font.weight = FontWeight(weight);
108 font.style = style;
109 Some(ShellTypography { font, size })
110}
111
112fn pango(description: &str) -> Option<ShellTypography> {
113 if description.contains(['@', '#', '=']) {
114 return None;
115 }
116 let mut words: Vec<_> = description.split_whitespace().collect();
117 let last = words.last().copied()?;
118 let (size, has_size) = if let Some(value) = last.strip_suffix("px") {
119 (positive(value)?, true)
120 } else if let Some(points) = positive(last) {
121 (points * 96.0 / 72.0, true)
122 } else {
123 if last.parse::<f32>().is_ok() {
125 return None;
126 }
127 (14.0, false)
128 };
129 if has_size {
130 words.pop();
131 }
132 let mut weight = FontWeight::NORMAL;
133 let mut style = FontStyle::Normal;
134 while let Some(word) = words.last() {
135 match word.to_ascii_lowercase().as_str() {
136 "normal" | "regular" => (),
137 "thin" => weight = FontWeight::THIN,
138 "ultralight" | "extra-light" => weight = FontWeight::EXTRA_LIGHT,
139 "light" => weight = FontWeight::LIGHT,
140 "medium" => weight = FontWeight::MEDIUM,
141 "semibold" | "demibold" | "semi-bold" => weight = FontWeight::SEMIBOLD,
142 "bold" => weight = FontWeight::BOLD,
143 "ultrabold" | "extra-bold" => weight = FontWeight::EXTRA_BOLD,
144 "heavy" | "black" => weight = FontWeight::BLACK,
145 "italic" => style = FontStyle::Italic,
146 "oblique" => style = FontStyle::Oblique,
147 "condensed" | "expanded" | "small-caps" | "ultracondensed" | "semicondensed"
148 | "ultraexpanded" | "semiexpanded" => return None,
149 _ => break,
150 }
151 words.pop();
152 }
153 let families = words.join(" ");
154 let family = families.trim_end_matches(',').trim();
155 if family.is_empty() || family.contains(',') {
158 return None;
159 }
160 let mut font = font(family.to_owned());
161 font.weight = weight;
162 font.style = style;
163 Some(ShellTypography { font, size })
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 #[test]
170 fn gnome_fonts_preserve_families_units_and_common_styles() {
171 let f = ShellTypography::from_desktop_description("Adwaita Sans 11").unwrap();
172 assert_eq!(f.font.family.as_ref(), "Adwaita Sans");
173 assert!((f.size - 14.666667).abs() < 0.001);
174 let f = ShellTypography::from_desktop_description("Noto Sans Bold Italic 16px").unwrap();
175 assert_eq!(f.font.family.as_ref(), "Noto Sans");
176 assert_eq!(f.size, 16.0);
177 assert_eq!(f.font.weight, FontWeight::BOLD);
178 assert_eq!(f.font.style, FontStyle::Italic);
179 let f = ShellTypography::from_desktop_description("Bookman Light, 12").unwrap();
180 assert_eq!(f.font.family.as_ref(), "Bookman Light");
181 }
182 #[test]
183 fn kde_fonts_support_legacy_and_css_weights_and_pixel_sizes() {
184 let f =
185 ShellTypography::from_desktop_description("Noto Sans,10,-1,5,50,0,0,0,0,0").unwrap();
186 assert_eq!(f.font.weight, FontWeight::NORMAL);
187 assert!((f.size - 13.333333).abs() < 0.001);
188 let f =
189 ShellTypography::from_desktop_description("Noto Sans,-1,18,5,700,1,0,0,0,0").unwrap();
190 assert_eq!(f.size, 18.0);
191 assert_eq!(f.font.weight, FontWeight::BOLD);
192 assert_eq!(f.font.style, FontStyle::Italic);
193 }
194 #[test]
195 fn malformed_and_unsupported_descriptions_are_not_used_as_families() {
196 for value in [
197 "",
198 "Sans NaN",
199 "Sans -1",
200 "Sans 0",
201 "Sans Condensed 11",
202 "Sans 11 @wght=500",
203 "Sans,Serif 11",
204 "\nSans 11",
205 "Noto,NaN,-1,5,50,0,0,0,0,0",
206 ] {
207 assert!(
208 ShellTypography::from_desktop_description(value).is_none(),
209 "{value}"
210 );
211 }
212 let normalized = ShellTypography {
213 font: font(""),
214 size: f32::NAN,
215 }
216 .normalized();
217 assert_eq!(normalized.size, 14.0);
218 assert_eq!(normalized.font.family.as_ref(), ".SystemUIFont");
219 }
220}