Krolyxon
Commit f2bd31a

Branches

Tags

Refs →

remagesrcrendertext.rs

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