1use crate::scene::*;
2use glyphon::{
3 Attrs, Buffer, Cache, Color as GlyphonColor, Family, FontSystem, Metrics, Resolution, Shaping,
4 SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Weight,
5};
6
7pub struct TextEngine {
8 pub font_system: FontSystem,
9 pub cache: SwashCache,
10 pub atlas: TextAtlas,
11 pub text_renderer: TextRenderer,
12 pub buffers: Vec<(NodeId, Buffer)>,
13 pub viewport: Viewport,
14}
15
16impl TextEngine {
17 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
18 let font_system = FontSystem::new();
19 let cache = SwashCache::new();
20 let glyph_cache = Cache::new(device);
21 let mut atlas = TextAtlas::new(device, queue, &glyph_cache, format);
22 let text_renderer =
23 TextRenderer::new(&mut atlas, device, wgpu::MultisampleState::default(), None);
24 let viewport = Viewport::new(device, &glyph_cache);
25
26 Self {
27 font_system,
28 cache,
29 atlas,
30 text_renderer,
31 buffers: Vec::new(),
32 viewport,
33 }
34 }
35
36 pub fn prepare(
37 &mut self,
38 scene: &SceneGraph,
39 device: &wgpu::Device,
40 queue: &wgpu::Queue,
41 viewport_width: u32,
42 viewport_height: u32,
43 ) {
44 self.buffers.clear();
45
46 if let Some(root) = scene.root {
47 self.collect_text_nodes(scene, root);
48 }
49
50 self.viewport.update(
51 queue,
52 Resolution {
53 width: viewport_width,
54 height: viewport_height,
55 },
56 );
57
58 let text_areas: Vec<TextArea> = self
59 .buffers
60 .iter()
61 .map(|(node_id, buffer)| {
62 let node = scene.get(*node_id);
63 let color = &node.style.color;
64 let r = (color.r * 255.0) as u8;
65 let g = (color.g * 255.0) as u8;
66 let b = (color.b * 255.0) as u8;
67 let a = (color.a * 255.0) as u8;
68
69 TextArea {
70 buffer,
71 left: node.layout.x,
72 top: node.layout.y,
73 scale: 1.0,
74 bounds: TextBounds {
75 left: 0,
76 top: 0,
77 right: viewport_width as i32,
78 bottom: viewport_height as i32,
79 },
80 default_color: GlyphonColor::rgba(r, g, b, a),
81 custom_glyphs: &[],
82 }
83 })
84 .collect();
85
86 self.text_renderer
87 .prepare(
88 device,
89 queue,
90 &mut self.font_system,
91 &mut self.atlas,
92 &self.viewport,
93 text_areas,
94 &mut self.cache,
95 )
96 .ok();
97 }
98
99 pub fn render<'a>(&'a self, pass: &mut wgpu::RenderPass<'a>) {
100 self.text_renderer
101 .render(&self.atlas, &self.viewport, pass)
102 .ok();
103 }
104
105 fn collect_text_nodes(&mut self, scene: &SceneGraph, node_id: NodeId) {
106 let node = scene.get(node_id);
107
108 if node.style.display == Display::None {
109 return;
110 }
111
112 if node.kind == ElementKind::Text {
113 if let Some(text) = &node.text_content {
114 let parent_style = node
115 .parent
116 .map(|p| &scene.get(p).style)
117 .unwrap_or(&node.style);
118
119 let font_size = parent_style.font_size;
120 let line_height = font_size * parent_style.line_height;
121
122 let mut buffer =
123 Buffer::new(&mut self.font_system, Metrics::new(font_size, line_height));
124
125 let weight = if parent_style.font_weight >= 700 {
126 Weight::BOLD
127 } else if parent_style.font_weight >= 500 {
128 Weight::MEDIUM
129 } else {
130 Weight::NORMAL
131 };
132
133 let family = if parent_style.font_family.is_empty() {
134 Family::SansSerif
135 } else {
136 Family::Name(&parent_style.font_family)
137 };
138
139 let attrs = Attrs::new().family(family).weight(weight);
140
141 buffer.set_size(&mut self.font_system, None, None);
144 buffer.set_text(&mut self.font_system, text, &attrs, Shaping::Advanced, None);
145 buffer.shape_until_scroll(&mut self.font_system, false);
146
147 self.buffers.push((node_id, buffer));
148 }
149 }
150
151 let children: Vec<NodeId> = node.children.clone();
152 for child_id in children {
153 self.collect_text_nodes(scene, child_id);
154 }
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::test_gpu::with_headless_device;
162
163 const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
164
165 fn scene_with_text(text: &str, font_size: f32) -> (SceneGraph, NodeId) {
167 let mut scene = SceneGraph::new();
168 let root = scene.add_node(ElementKind::from_tag("div"), "div".to_string());
169 let p = scene.add_node(ElementKind::from_tag("p"), "p".to_string());
170 scene.add_child(root, p);
171 scene.get_mut(p).style.font_size = font_size;
172 let t = scene.add_node(ElementKind::Text, "#text".to_string());
173 scene.get_mut(t).text_content = Some(text.to_string());
174 scene.add_child(p, t);
175 (scene, t)
176 }
177
178 fn shaped_width(engine: &TextEngine) -> f32 {
180 engine.buffers[0]
181 .1
182 .layout_runs()
183 .map(|run| run.line_w)
184 .fold(0.0, f32::max)
185 }
186
187 #[test]
188 fn text_nodes_are_collected_and_shaped_into_glyphs() {
189 with_headless_device(
190 "text_nodes_are_collected_and_shaped_into_glyphs",
191 |device, queue| {
192 let mut engine = TextEngine::new(device, queue, FORMAT);
193 let (scene, t) = scene_with_text("hello", 16.0);
194
195 engine.prepare(&scene, device, queue, 800, 600);
196
197 assert_eq!(engine.buffers.len(), 1);
198 assert_eq!(engine.buffers[0].0, t);
199 let glyphs: usize = engine.buffers[0]
200 .1
201 .layout_runs()
202 .map(|r| r.glyphs.len())
203 .sum();
204 assert_eq!(glyphs, 5, "one glyph per ASCII character");
205 assert!(shaped_width(&engine) > 0.0);
206 },
207 );
208 }
209
210 #[test]
211 fn shaped_width_scales_with_the_font_size() {
212 with_headless_device("shaped_width_scales_with_the_font_size", |device, queue| {
213 let mut engine = TextEngine::new(device, queue, FORMAT);
214
215 let (small, _) = scene_with_text("hello world", 16.0);
216 engine.prepare(&small, device, queue, 800, 600);
217 let w16 = shaped_width(&engine);
218
219 let (large, _) = scene_with_text("hello world", 32.0);
220 engine.prepare(&large, device, queue, 800, 600);
221 let w32 = shaped_width(&engine);
222
223 assert!(w16 > 0.0 && w32 > 0.0, "{w16} {w32}");
224 let ratio = w32 / w16;
226 assert!((1.8..=2.2).contains(&ratio), "ratio {ratio}");
227 });
228 }
229
230 #[test]
231 fn shaped_width_grows_with_the_string() {
232 with_headless_device("shaped_width_grows_with_the_string", |device, queue| {
233 let mut engine = TextEngine::new(device, queue, FORMAT);
234
235 let (short, _) = scene_with_text("hi", 16.0);
236 engine.prepare(&short, device, queue, 800, 600);
237 let short_w = shaped_width(&engine);
238
239 let (long, _) = scene_with_text("hi there, this is longer", 16.0);
240 engine.prepare(&long, device, queue, 800, 600);
241 let long_w = shaped_width(&engine);
242
243 assert!(long_w > short_w, "{long_w} !> {short_w}");
244 });
245 }
246
247 #[test]
248 fn line_metrics_come_from_the_parent_font_size_and_line_height() {
249 with_headless_device(
250 "line_metrics_come_from_the_parent_font_size_and_line_height",
251 |device, queue| {
252 let mut engine = TextEngine::new(device, queue, FORMAT);
253 let (mut scene, _) = scene_with_text("hello", 20.0);
254 let p = scene.get(scene.root.unwrap()).children[0];
255 scene.get_mut(p).style.line_height = 1.5;
256
257 engine.prepare(&scene, device, queue, 800, 600);
258
259 let run_height = engine.buffers[0]
260 .1
261 .layout_runs()
262 .map(|r| r.line_height)
263 .next()
264 .expect("no layout runs");
265 assert!((run_height - 30.0).abs() < 0.01, "line height {run_height}");
266 },
267 );
268 }
269
270 #[test]
271 fn long_text_is_not_wrapped_into_extra_lines() {
272 with_headless_device(
273 "long_text_is_not_wrapped_into_extra_lines",
274 |device, queue| {
275 let mut engine = TextEngine::new(device, queue, FORMAT);
276 let (scene, _) = scene_with_text(
277 "a very long single line of text that would wrap in a narrow box",
278 16.0,
279 );
280
281 engine.prepare(&scene, device, queue, 100, 100);
283
284 assert_eq!(engine.buffers[0].1.layout_runs().count(), 1);
285 assert!(shaped_width(&engine) > 100.0);
286 },
287 );
288 }
289
290 #[test]
291 fn display_none_text_is_not_prepared() {
292 with_headless_device("display_none_text_is_not_prepared", |device, queue| {
293 let mut engine = TextEngine::new(device, queue, FORMAT);
294 let (mut scene, _) = scene_with_text("hello", 16.0);
295 let p = scene.get(scene.root.unwrap()).children[0];
296 scene.get_mut(p).style.display = Display::None;
297
298 engine.prepare(&scene, device, queue, 800, 600);
299
300 assert!(engine.buffers.is_empty());
301 });
302 }
303
304 #[test]
305 fn every_text_node_in_the_tree_gets_its_own_buffer() {
306 with_headless_device(
307 "every_text_node_in_the_tree_gets_its_own_buffer",
308 |device, queue| {
309 let mut engine = TextEngine::new(device, queue, FORMAT);
310 let mut scene = SceneGraph::new();
311 let root = scene.add_node(ElementKind::from_tag("div"), "div".to_string());
312 for word in ["one", "two", "three"] {
313 let p = scene.add_node(ElementKind::from_tag("p"), "p".to_string());
314 scene.add_child(root, p);
315 let t = scene.add_node(ElementKind::Text, "#text".to_string());
316 scene.get_mut(t).text_content = Some(word.to_string());
317 scene.add_child(p, t);
318 }
319
320 engine.prepare(&scene, device, queue, 800, 600);
321
322 assert_eq!(engine.buffers.len(), 3);
323 engine.prepare(&scene, device, queue, 800, 600);
325 assert_eq!(engine.buffers.len(), 3);
326 },
327 );
328 }
329
330 #[test]
331 fn an_empty_scene_prepares_no_text() {
332 with_headless_device("an_empty_scene_prepares_no_text", |device, queue| {
333 let mut engine = TextEngine::new(device, queue, FORMAT);
334 engine.prepare(&SceneGraph::new(), device, queue, 800, 600);
335 assert!(engine.buffers.is_empty());
336 });
337 }
338}