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}