Skip to main content

foundry_runtime/
text.rs

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                // Never constrain single-line text width. Let glyphon render
142                // the full text without wrapping. Overflow is better than clipping.
143                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    /// `<div><p style="font-size:{size}px">{text}</p></div>`
166    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    /// Total shaped advance width of the first prepared buffer.
179    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            // doubling the em size roughly doubles the advance
225            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                // the surface is deliberately narrower than the shaped text
282                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                // prepare is idempotent -- buffers are rebuilt, not appended
324                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}