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. |
7 | use crate::title::{config, font_preference::FontPreference}; |
8 | use ab_glyph::{point, Font, FontRef, Glyph, PxScale, PxScaleFont, ScaleFont, VariableFont}; |
9 | use std::{fs::File, process::Command}; |
10 | use tiny_skia::{Color, Pixmap, PremultipliedColorU8}; |
11 | |
12 | const CANTARELL: &[u8] = include_bytes!("Cantarell-Regular.ttf" ); |
13 | |
14 | #[derive (Debug)] |
15 | pub 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 | |
24 | impl 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. |
146 | fn 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 |
174 | fn 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 | |
190 | fn 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 | |