Krolyxon
Commit ab1dc3b

Branches

Tags

Refs →

remagesrcrendertext.rs

313 lines · 9.7 KB · Rust

1//! Text through glyphon, with the embedded Nothing fonts.
2
3use std::collections::HashMap;
4
5use glyphon::{
6 Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache,
7 TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Weight, Wrap,
8 cosmic_text::{Ellipsize, EllipsizeHeightLimit},
9 fontdb,
10};
11
12use crate::config::theme::Rgba;
13
14const FONTS: [&[u8]; 6] = [
15 include_bytes!("../../assets/fonts/SpaceGrotesk-Light.otf"),
16 include_bytes!("../../assets/fonts/SpaceGrotesk-Regular.otf"),
17 include_bytes!("../../assets/fonts/SpaceGrotesk-Medium.otf"),
18 include_bytes!("../../assets/fonts/SpaceMono-Regular.ttf"),
19 include_bytes!("../../assets/fonts/SpaceMono-Bold.ttf"),
20 include_bytes!("../../assets/fonts/Doto-Variable.ttf"),
21];
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24pub enum FontRole {
25 /// Space Grotesk.
26 Ui,
27 /// Space Mono, for labels and data.
28 Mono,
29 /// Doto, dot-matrix, 36 px and up.
30 Display,
31}
32
33impl FontRole {
34 fn index(self) -> usize {
35 match self {
36 FontRole::Ui => 0,
37 FontRole::Mono => 1,
38 FontRole::Display => 2,
39 }
40 }
41}
42
43/// The embedded families: UI, mono, display.
44pub const DEFAULT_FAMILIES: [&str; 3] = ["Space Grotesk", "Space Mono", "Doto"];
45
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum Align {
48 Left,
49 Center,
50 Right,
51}
52
53/// A single line of text. `x` is the left edge, centre or right edge depending on `align`;
54/// `y` is the vertical centre of the line. All units are physical pixels.
55#[derive(Clone, Debug)]
56pub struct TextItem {
57 pub spans: Vec<(String, Rgba)>,
58 pub role: FontRole,
59 pub size: f32,
60 pub weight: u16,
61 /// Tracking in em.
62 pub tracking: f32,
63 pub x: f32,
64 pub y: f32,
65 pub align: Align,
66 pub max_width: Option<f32>,
67 /// Only the part inside this rect (x, y, w, h) shows.
68 pub clip: Option<[f32; 4]>,
69}
70
71#[derive(Clone, PartialEq, Eq, Hash)]
72struct Key {
73 spans: Vec<(String, [u8; 4])>,
74 role: FontRole,
75 size: u32,
76 weight: u16,
77 tracking: u32,
78 max_width: Option<u32>,
79}
80
81impl Key {
82 fn of(t: &TextItem) -> Self {
83 Self {
84 spans: t.spans.iter().map(|(s, c)| (s.clone(), c.0)).collect(),
85 role: t.role,
86 size: t.size.to_bits(),
87 weight: t.weight,
88 tracking: t.tracking.to_bits(),
89 max_width: t.max_width.map(|m| m.max(0.0).round() as u32),
90 }
91 }
92}
93
94struct Shaped {
95 buffer: Buffer,
96 width: f32,
97 line_height: f32,
98 used: bool,
99}
100
101pub struct TextSystem {
102 fonts: FontSystem,
103 swash: SwashCache,
104 cache: Cache,
105 atlas: TextAtlas,
106 renderer: TextRenderer,
107 viewport: Viewport,
108 format: wgpu::TextureFormat,
109 shaped: HashMap<Key, Shaped>,
110 /// Family names per [`FontRole`], from `ui.font_*`.
111 families: [String; 3],
112}
113
114/// Locale for shaping, from the usual environment variables.
115fn locale() -> String {
116 ["LC_ALL", "LC_MESSAGES", "LANG"]
117 .iter()
118 .filter_map(|v| std::env::var(v).ok())
119 .find(|v| !v.is_empty() && v != "C" && v != "POSIX")
120 .and_then(|v| v.split('.').next().map(|s| s.replace('_', "-")))
121 .unwrap_or_else(|| "en-US".into())
122}
123
124/// The embedded fonts only: fast to build, enough for the UI itself.
125pub fn embedded_fonts() -> fontdb::Database {
126 let mut db = fontdb::Database::new();
127 for data in FONTS {
128 db.load_font_data(data.to_vec());
129 }
130 db.set_sans_serif_family("Space Grotesk");
131 db.set_monospace_family("Space Mono");
132 db
133}
134
135/// Embedded plus system fonts, for filenames in any script. Slow; build it off-thread.
136pub fn all_fonts() -> fontdb::Database {
137 let mut db = embedded_fonts();
138 db.load_system_fonts();
139 db
140}
141
142impl TextSystem {
143 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
144 let fonts = FontSystem::new_with_locale_and_db(locale(), embedded_fonts());
145 let cache = Cache::new(device);
146 let mut atlas = TextAtlas::new(device, queue, &cache, format);
147 let renderer =
148 TextRenderer::new(&mut atlas, device, wgpu::MultisampleState::default(), None);
149 let viewport = Viewport::new(device, &cache);
150 Self {
151 fonts,
152 swash: SwashCache::new(),
153 cache,
154 atlas,
155 renderer,
156 viewport,
157 format,
158 shaped: HashMap::new(),
159 families: DEFAULT_FAMILIES.map(String::from),
160 }
161 }
162
163 pub fn set_families(&mut self, families: [String; 3]) {
164 if families != self.families {
165 self.families = families;
166 self.shaped.clear();
167 }
168 }
169
170 /// Swap in a larger font database (glyph caches are keyed by font id, so start fresh).
171 pub fn set_fonts(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, db: fontdb::Database) {
172 self.fonts = FontSystem::new_with_locale_and_db(locale(), db);
173 self.swash = SwashCache::new();
174 self.atlas = TextAtlas::new(device, queue, &self.cache, self.format);
175 self.renderer = TextRenderer::new(
176 &mut self.atlas,
177 device,
178 wgpu::MultisampleState::default(),
179 None,
180 );
181 self.shaped.clear();
182 }
183
184 fn shape(&mut self, item: &TextItem) -> Key {
185 let key = Key::of(item);
186 if let Some(s) = self.shaped.get_mut(&key) {
187 s.used = true;
188 return key;
189 }
190 let line_height = (item.size * 1.25).ceil();
191 let mut buffer = Buffer::new(&mut self.fonts, Metrics::new(item.size, line_height));
192 buffer.set_size(None, None);
193 let family = self.families[item.role.index()].clone();
194 let base = Attrs::new()
195 .family(Family::Name(&family))
196 .weight(Weight(item.weight))
197 .letter_spacing(item.tracking);
198 let spans = item.spans.iter().map(|(s, c)| {
199 let [r, g, b, a] = c.0;
200 (s.as_str(), base.clone().color(Color::rgba(r, g, b, a)))
201 });
202 buffer.set_rich_text(spans, &base, Shaping::Advanced, None);
203 buffer.shape_until_scroll(&mut self.fonts, false);
204 let mut width = buffer.layout_runs().map(|r| r.line_w).fold(0.0, f32::max);
205 if let Some(max) = key.max_width.map(|m| m as f32)
206 && width > max
207 {
208 // Middle ellipsis keeps both the start and the extension of a filename.
209 buffer.set_size(Some(max), None);
210 buffer.set_wrap(Wrap::None);
211 buffer.set_ellipsize(Ellipsize::Middle(EllipsizeHeightLimit::Lines(1)));
212 buffer.shape_until_scroll(&mut self.fonts, false);
213 width = buffer
214 .layout_runs()
215 .map(|r| r.line_w)
216 .fold(0.0, f32::max)
217 .min(max);
218 }
219 self.shaped.insert(
220 key.clone(),
221 Shaped {
222 buffer,
223 width,
224 line_height,
225 used: true,
226 },
227 );
228 key
229 }
230
231 /// Width of `item` once laid out, in physical pixels.
232 pub fn measure(&mut self, item: &TextItem) -> f32 {
233 let key = self.shape(item);
234 self.shaped.get(&key).map_or(0.0, |s| s.width)
235 }
236
237 /// Shape and upload this frame's text.
238 pub fn prepare(
239 &mut self,
240 device: &wgpu::Device,
241 queue: &wgpu::Queue,
242 items: &[TextItem],
243 size: [u32; 2],
244 ) -> anyhow::Result<()> {
245 let keys: Vec<Key> = items.iter().map(|i| self.shape(i)).collect();
246 // Keep what this frame (including any `measure` calls) used, and reset for the next.
247 self.shaped.retain(|_, s| std::mem::take(&mut s.used));
248 self.viewport.update(
249 queue,
250 Resolution {
251 width: size[0],
252 height: size[1],
253 },
254 );
255
256 let areas = items.iter().zip(&keys).filter_map(|(item, key)| {
257 let s = self.shaped.get(key)?;
258 let width = s.width;
259 let left = match item.align {
260 Align::Left => item.x,
261 Align::Center => item.x - width * 0.5,
262 Align::Right => item.x - width,
263 };
264 let top = item.y - s.line_height * 0.5;
265 let color = item.spans.first().map_or(Rgba([255; 4]), |(_, c)| *c).0;
266 let mut bounds = [left, top, left + width, top + s.line_height];
267 if let Some([x, y, w, h]) = item.clip {
268 bounds = [
269 bounds[0].max(x),
270 bounds[1].max(y),
271 bounds[2].min(x + w),
272 bounds[3].min(y + h),
273 ];
274 if bounds[2] <= bounds[0] || bounds[3] <= bounds[1] {
275 return None;
276 }
277 }
278 Some(TextArea {
279 buffer: &s.buffer,
280 left,
281 top,
282 scale: 1.0,
283 bounds: TextBounds {
284 left: bounds[0].floor() as i32,
285 top: bounds[1].floor() as i32,
286 right: bounds[2].ceil() as i32,
287 bottom: bounds[3].ceil() as i32,
288 },
289 default_color: Color::rgba(color[0], color[1], color[2], color[3]),
290 custom_glyphs: &[],
291 })
292 });
293 self.renderer.prepare(
294 device,
295 queue,
296 &mut self.fonts,
297 &mut self.atlas,
298 &self.viewport,
299 areas,
300 &mut self.swash,
301 )?;
302 Ok(())
303 }
304
305 pub fn render(&self, pass: &mut wgpu::RenderPass<'_>) -> anyhow::Result<()> {
306 self.renderer.render(&self.atlas, &self.viewport, pass)?;
307 Ok(())
308 }
309
310 pub fn trim(&mut self) {
311 self.atlas.trim();
312 }
313}