Krolyxon
Commit 92928dd

Branches

Tags

Refs →

remagesrcrendertext.rs

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