1//! Title renderer using ab_glyph.
2//!
3//! Requires no dynamically linked dependencies.
4//!
5//! Can fallback to a embedded Cantarell-Regular.ttf font (SIL Open Font Licence v1.1)
6//! if the system font doesn't work.
7use crate::title::{config, font_preference::FontPreference};
8use ab_glyph::{point, Font, FontRef, Glyph, PxScale, PxScaleFont, ScaleFont, VariableFont};
9use std::{fs::File, process::Command};
10use tiny_skia::{Color, Pixmap, PremultipliedColorU8};
11
12const CANTARELL: &[u8] = include_bytes!("Cantarell-Regular.ttf");
13
14#[derive(Debug)]
15pub struct AbGlyphTitleText {
16 title: String,
17 font: Option<(memmap2::Mmap, FontPreference)>,
18 original_px_size: f32,
19 size: PxScale,
20 color: Color,
21 pixmap: Option<Pixmap>,
22}
23
24impl AbGlyphTitleText {
25 pub fn new(color: Color) -> Self {
26 let font_pref = config::titlebar_font().unwrap_or_default();
27 let font_pref_pt_size = font_pref.pt_size;
28 let font = font_file_matching(&font_pref)
29 .and_then(|f| mmap(&f))
30 .map(|mmap| (mmap, font_pref));
31
32 let size = parse_font(&font)
33 .pt_to_px_scale(font_pref_pt_size)
34 .unwrap_or_else(|| {
35 log::error!("invalid font units_per_em");
36 PxScale { x: 17.6, y: 17.6 }
37 });
38
39 Self {
40 title: <_>::default(),
41 font,
42 original_px_size: size.x,
43 size,
44 color,
45 pixmap: None,
46 }
47 }
48
49 pub fn update_scale(&mut self, scale: u32) {
50 let new_scale = PxScale::from(self.original_px_size * scale as f32);
51 if (self.size.x - new_scale.x).abs() > f32::EPSILON {
52 self.size = new_scale;
53 self.pixmap = self.render();
54 }
55 }
56
57 pub fn update_title(&mut self, title: impl Into<String>) {
58 let new_title = title.into();
59 if new_title != self.title {
60 self.title = new_title;
61 self.pixmap = self.render();
62 }
63 }
64
65 pub fn update_color(&mut self, color: Color) {
66 if color != self.color {
67 self.color = color;
68 self.pixmap = self.render();
69 }
70 }
71
72 pub fn pixmap(&self) -> Option<&Pixmap> {
73 self.pixmap.as_ref()
74 }
75
76 /// Render returning the new `Pixmap`.
77 fn render(&self) -> Option<Pixmap> {
78 let font = parse_font(&self.font);
79 let font = font.as_scaled(self.size);
80
81 let glyphs = self.layout(&font);
82 let last_glyph = glyphs.last()?;
83 // + 2 because ab_glyph likes to draw outside of its area,
84 // so we add 1px border around the pixmap
85 let width = (last_glyph.position.x + font.h_advance(last_glyph.id)).ceil() as u32 + 2;
86 let height = font.height().ceil() as u32 + 2;
87
88 let mut pixmap = Pixmap::new(width, height)?;
89
90 let pixels = pixmap.pixels_mut();
91
92 for glyph in glyphs {
93 if let Some(outline) = font.outline_glyph(glyph) {
94 let bounds = outline.px_bounds();
95 let left = bounds.min.x as u32;
96 let top = bounds.min.y as u32;
97 outline.draw(|x, y, c| {
98 // `ab_glyph` may return values greater than 1.0, but they are defined to be
99 // same as 1.0. For our purposes, we need to contrain this value.
100 let c = c.min(1.0);
101
102 // offset the index by 1, so it is in the center of the pixmap
103 let p_idx = (top + y + 1) * width + (left + x + 1);
104 let old_alpha_u8 = pixels[p_idx as usize].alpha();
105 let new_alpha = c + (old_alpha_u8 as f32 / 255.0);
106 if let Some(px) = PremultipliedColorU8::from_rgba(
107 (self.color.red() * new_alpha * 255.0) as _,
108 (self.color.green() * new_alpha * 255.0) as _,
109 (self.color.blue() * new_alpha * 255.0) as _,
110 (new_alpha * 255.0) as _,
111 ) {
112 pixels[p_idx as usize] = px;
113 }
114 })
115 }
116 }
117
118 Some(pixmap)
119 }
120
121 /// Simple single-line glyph layout.
122 fn layout(&self, font: &PxScaleFont<impl Font>) -> Vec<Glyph> {
123 let mut caret = point(0.0, font.ascent());
124 let mut last_glyph: Option<Glyph> = None;
125 let mut target = Vec::new();
126 for c in self.title.chars() {
127 if c.is_control() {
128 continue;
129 }
130 let mut glyph = font.scaled_glyph(c);
131 if let Some(previous) = last_glyph.take() {
132 caret.x += font.kern(previous.id, glyph.id);
133 }
134 glyph.position = caret;
135
136 last_glyph = Some(glyph.clone());
137 caret.x += font.h_advance(glyph.id);
138
139 target.push(glyph);
140 }
141 target
142 }
143}
144
145/// Parse the memmapped system font or fallback to built-in cantarell.
146fn parse_font(sys_font: &Option<(memmap2::Mmap, FontPreference)>) -> FontRef<'_> {
147 match sys_font {
148 Some((mmap, font_pref)) => {
149 FontRef::try_from_slice(mmap)
150 .map(|mut f| {
151 // basic "bold" handling for variable fonts
152 if font_pref
153 .style
154 .as_deref()
155 .map_or(false, |s| s.eq_ignore_ascii_case("bold"))
156 {
157 f.set_variation(b"wght", 700.0);
158 }
159 f
160 })
161 .unwrap_or_else(|_| {
162 // We control the default font, so I guess it's fine to unwrap it
163 #[allow(clippy::unwrap_used)]
164 FontRef::try_from_slice(CANTARELL).unwrap()
165 })
166 }
167 // We control the default font, so I guess it's fine to unwrap it
168 #[allow(clippy::unwrap_used)]
169 _ => FontRef::try_from_slice(CANTARELL).unwrap(),
170 }
171}
172
173/// Font-config without dynamically linked dependencies
174fn font_file_matching(pref: &FontPreference) -> Option<File> {
175 let mut pattern: String = pref.name.clone();
176 if let Some(style: &String) = &pref.style {
177 pattern.push(ch:':');
178 pattern.push_str(string:style);
179 }
180 CommandOption::new(program:"fc-match")
181 .arg("-f")
182 .arg("%{file}")
183 .arg(&pattern)
184 .output()
185 .ok()
186 .and_then(|out: Output| String::from_utf8(vec:out.stdout).ok())
187 .and_then(|path: String| File::open(path:path.trim()).ok())
188}
189
190fn mmap(file: &File) -> Option<memmap2::Mmap> {
191 // Safety: System font files are not expected to be mutated during use
192 unsafe { memmap2::Mmap::map(file).ok() }
193}
194