Krolyxon
Commit e515e5a

Branches

Tags

Refs →

remagesrcrendertext.rs

347 lines · 10.8 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 /// Text above overlay panels, sharing the atlas.
108 overlay: TextRenderer,
109 viewport: Viewport,
110 format: wgpu::TextureFormat,
111 shaped: HashMap<Key, Shaped>,
112 /// Family names per [`FontRole`], from `ui.font_*`.
113 families: [String; 3],
114}
115
116/// Locale for shaping, from the usual environment variables.
117fn locale() -> String {
118 ["LC_ALL", "LC_MESSAGES", "LANG"]
119 .iter()
120 .filter_map(|v| std::env::var(v).ok())
121 .find(|v| !v.is_empty() && v != "C" && v != "POSIX")
122 .and_then(|v| v.split('.').next().map(|s| s.replace('_', "-")))
123 .unwrap_or_else(|| "en-US".into())
124}
125
126/// The embedded fonts only: fast to build, enough for the UI itself.
127pub fn embedded_fonts() -> fontdb::Database {
128 let mut db = fontdb::Database::new();
129 for data in FONTS {
130 db.load_font_data(data.to_vec());
131 }
132 db.set_sans_serif_family("Space Grotesk");
133 db.set_monospace_family("Space Mono");
134 db
135}
136
137/// Embedded plus system fonts, for filenames in any script. Slow; build it off-thread.
138pub fn all_fonts() -> fontdb::Database {
139 let mut db = embedded_fonts();
140 db.load_system_fonts();
141 db
142}
143
144impl TextSystem {
145 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
146 let fonts = FontSystem::new_with_locale_and_db(locale(), embedded_fonts());
147 let cache = Cache::new(device);
148 let mut atlas = TextAtlas::new(device, queue, &cache, format);
149 let renderer =
150 TextRenderer::new(&mut atlas, device, wgpu::MultisampleState::default(), None);
151 let overlay =
152 TextRenderer::new(&mut atlas, device, wgpu::MultisampleState::default(), None);
153 let viewport = Viewport::new(device, &cache);
154 Self {
155 fonts,
156 swash: SwashCache::new(),
157 cache,
158 atlas,
159 renderer,
160 overlay,
161 viewport,
162 format,
163 shaped: HashMap::new(),
164 families: DEFAULT_FAMILIES.map(String::from),
165 }
166 }
167
168 pub fn set_families(&mut self, families: [String; 3]) {
169 if families != self.families {
170 self.families = families;
171 self.shaped.clear();
172 }
173 }
174
175 /// Swap in a larger font database (glyph caches are keyed by font id, so start fresh).
176 pub fn set_fonts(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, db: fontdb::Database) {
177 self.fonts = FontSystem::new_with_locale_and_db(locale(), db);
178 self.swash = SwashCache::new();
179 self.atlas = TextAtlas::new(device, queue, &self.cache, self.format);
180 self.renderer = TextRenderer::new(
181 &mut self.atlas,
182 device,
183 wgpu::MultisampleState::default(),
184 None,
185 );
186 self.overlay = TextRenderer::new(
187 &mut self.atlas,
188 device,
189 wgpu::MultisampleState::default(),
190 None,
191 );
192 self.shaped.clear();
193 }
194
195 fn shape(&mut self, item: &TextItem) -> Key {
196 let key = Key::of(item);
197 if let Some(s) = self.shaped.get_mut(&key) {
198 s.used = true;
199 return key;
200 }
201 let line_height = (item.size * 1.25).ceil();
202 let mut buffer = Buffer::new(&mut self.fonts, Metrics::new(item.size, line_height));
203 buffer.set_size(None, None);
204 let family = self.families[item.role.index()].clone();
205 let base = Attrs::new()
206 .family(Family::Name(&family))
207 .weight(Weight(item.weight))
208 .letter_spacing(item.tracking);
209 let spans = item.spans.iter().map(|(s, c)| {
210 let [r, g, b, a] = c.0;
211 (s.as_str(), base.clone().color(Color::rgba(r, g, b, a)))
212 });
213 buffer.set_rich_text(spans, &base, Shaping::Advanced, None);
214 buffer.shape_until_scroll(&mut self.fonts, false);
215 let mut width = buffer.layout_runs().map(|r| r.line_w).fold(0.0, f32::max);
216 if let Some(max) = key.max_width.map(|m| m as f32)
217 && width > max
218 {
219 // Middle ellipsis keeps both the start and the extension of a filename.
220 buffer.set_size(Some(max), None);
221 buffer.set_wrap(Wrap::None);
222 buffer.set_ellipsize(Ellipsize::Middle(EllipsizeHeightLimit::Lines(1)));
223 buffer.shape_until_scroll(&mut self.fonts, false);
224 width = buffer
225 .layout_runs()
226 .map(|r| r.line_w)
227 .fold(0.0, f32::max)
228 .min(max);
229 }
230 self.shaped.insert(
231 key.clone(),
232 Shaped {
233 buffer,
234 width,
235 line_height,
236 used: true,
237 },
238 );
239 key
240 }
241
242 /// Width of `item` once laid out, in physical pixels.
243 pub fn measure(&mut self, item: &TextItem) -> f32 {
244 let key = self.shape(item);
245 self.shaped.get(&key).map_or(0.0, |s| s.width)
246 }
247
248 /// Shape and upload this frame's text: the base layer and the overlay layer.
249 pub fn prepare(
250 &mut self,
251 device: &wgpu::Device,
252 queue: &wgpu::Queue,
253 layers: [&[TextItem]; 2],
254 size: [u32; 2],
255 ) -> anyhow::Result<()> {
256 let keys: [Vec<Key>; 2] = layers.map(|l| l.iter().map(|i| self.shape(i)).collect());
257 // Keep what this frame (including any `measure` calls) used, and reset for the next.
258 self.shaped.retain(|_, s| std::mem::take(&mut s.used));
259 self.viewport.update(
260 queue,
261 Resolution {
262 width: size[0],
263 height: size[1],
264 },
265 );
266 for (layer, (items, keys)) in layers.iter().zip(&keys).enumerate() {
267 self.prepare_layer(device, queue, layer, items, keys)?;
268 }
269 Ok(())
270 }
271
272 fn prepare_layer(
273 &mut self,
274 device: &wgpu::Device,
275 queue: &wgpu::Queue,
276 layer: usize,
277 items: &[TextItem],
278 keys: &[Key],
279 ) -> anyhow::Result<()> {
280 let areas = items.iter().zip(keys).filter_map(|(item, key)| {
281 let s = self.shaped.get(key)?;
282 let width = s.width;
283 let left = match item.align {
284 Align::Left => item.x,
285 Align::Center => item.x - width * 0.5,
286 Align::Right => item.x - width,
287 };
288 let top = item.y - s.line_height * 0.5;
289 let color = item.spans.first().map_or(Rgba([255; 4]), |(_, c)| *c).0;
290 let mut bounds = [left, top, left + width, top + s.line_height];
291 if let Some([x, y, w, h]) = item.clip {
292 bounds = [
293 bounds[0].max(x),
294 bounds[1].max(y),
295 bounds[2].min(x + w),
296 bounds[3].min(y + h),
297 ];
298 if bounds[2] <= bounds[0] || bounds[3] <= bounds[1] {
299 return None;
300 }
301 }
302 Some(TextArea {
303 buffer: &s.buffer,
304 left,
305 top,
306 scale: 1.0,
307 bounds: TextBounds {
308 left: bounds[0].floor() as i32,
309 top: bounds[1].floor() as i32,
310 right: bounds[2].ceil() as i32,
311 bottom: bounds[3].ceil() as i32,
312 },
313 default_color: Color::rgba(color[0], color[1], color[2], color[3]),
314 custom_glyphs: &[],
315 })
316 });
317 let renderer = if layer == 0 {
318 &mut self.renderer
319 } else {
320 &mut self.overlay
321 };
322 renderer.prepare(
323 device,
324 queue,
325 &mut self.fonts,
326 &mut self.atlas,
327 &self.viewport,
328 areas,
329 &mut self.swash,
330 )?;
331 Ok(())
332 }
333
334 pub fn render(&self, pass: &mut wgpu::RenderPass<'_>) -> anyhow::Result<()> {
335 self.renderer.render(&self.atlas, &self.viewport, pass)?;
336 Ok(())
337 }
338
339 pub fn render_overlay(&self, pass: &mut wgpu::RenderPass<'_>) -> anyhow::Result<()> {
340 self.overlay.render(&self.atlas, &self.viewport, pass)?;
341 Ok(())
342 }
343
344 pub fn trim(&mut self) {
345 self.atlas.trim();
346 }
347}