sctk_adwaita/title/
ab_glyph_renderer.rs
1use 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 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 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 let c = c.min(1.0);
101
102 let p_idx = (top + y + 1) * width + (left + x + 1);
104 let Some(pixel) = pixels.get_mut(p_idx as usize) else {
105 log::warn!("Ignoring out of range pixel (pixel id: {p_idx}");
106 return;
107 };
108
109 let old_alpha_u8 = pixel.alpha();
110
111 let new_alpha = c + (old_alpha_u8 as f32 / 255.0);
112 if let Some(px) = PremultipliedColorU8::from_rgba(
113 (self.color.red() * new_alpha * 255.0) as _,
114 (self.color.green() * new_alpha * 255.0) as _,
115 (self.color.blue() * new_alpha * 255.0) as _,
116 (new_alpha * 255.0) as _,
117 ) {
118 *pixel = px;
119 }
120 })
121 }
122 }
123
124 Some(pixmap)
125 }
126
127 fn layout(&self, font: &PxScaleFont<impl Font>) -> Vec<Glyph> {
129 let mut caret = point(0.0, font.ascent());
130 let mut last_glyph: Option<Glyph> = None;
131 let mut target = Vec::new();
132 for c in self.title.chars() {
133 if c.is_control() {
134 continue;
135 }
136 let mut glyph = font.scaled_glyph(c);
137 if let Some(previous) = last_glyph.take() {
138 caret.x += font.kern(previous.id, glyph.id);
139 }
140 glyph.position = caret;
141
142 last_glyph = Some(glyph.clone());
143 caret.x += font.h_advance(glyph.id);
144
145 target.push(glyph);
146 }
147 target
148 }
149}
150
151fn parse_font(sys_font: &Option<(memmap2::Mmap, FontPreference)>) -> FontRef<'_> {
153 match sys_font {
154 Some((mmap, font_pref)) => {
155 FontRef::try_from_slice(mmap)
156 .map(|mut f| {
157 if font_pref
159 .style
160 .as_deref()
161 .map_or(false, |s| s.eq_ignore_ascii_case("bold"))
162 {
163 f.set_variation(b"wght", 700.0);
164 }
165 f
166 })
167 .unwrap_or_else(|_| {
168 #[allow(clippy::unwrap_used)]
170 FontRef::try_from_slice(CANTARELL).unwrap()
171 })
172 }
173 #[allow(clippy::unwrap_used)]
175 _ => FontRef::try_from_slice(CANTARELL).unwrap(),
176 }
177}
178
179fn font_file_matching(pref: &FontPreference) -> Option<File> {
181 let mut pattern = pref.name.clone();
182 if let Some(style) = &pref.style {
183 pattern.push(':');
184 pattern.push_str(style);
185 }
186 Command::new("fc-match")
187 .arg("-f")
188 .arg("%{file}")
189 .arg(&pattern)
190 .output()
191 .ok()
192 .and_then(|out| String::from_utf8(out.stdout).ok())
193 .and_then(|path| File::open(path.trim()).ok())
194}
195
196fn mmap(file: &File) -> Option<memmap2::Mmap> {
197 unsafe { memmap2::Mmap::map(file).ok() }
199}