1use std::collections::{BTreeSet, HashMap};
2
3use crate::target::Targets;
4use glyphon::{
5 Attrs, Buffer, Cache, Color, ColorMode, FontSystem, Metrics, Resolution, Shaping, SwashCache,
6 TextArea, TextAtlas, TextBounds, TextRenderer, Viewport,
7};
8
9#[derive(Clone)]
10struct Queued {
11 text: String,
12 left: f32,
13 top: f32,
14 size_px: f32,
15 color: [u8; 4],
16 layer: Option<i16>,
18}
19
20#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
23pub enum Align {
24 #[default]
26 Left,
27 Center,
29 Right,
31}
32
33impl Align {
34 pub(crate) fn factor(self) -> f32 {
36 match self {
37 Self::Left => 0.0,
38 Self::Center => 0.5,
39 Self::Right => 1.0,
40 }
41 }
42}
43
44struct CachedBuffer {
47 buffer: Buffer,
48 last_used: u64,
49}
50
51const EVICT_AFTER_FRAMES: u64 = 240;
54
55pub(crate) struct TextSystem {
56 font_system: FontSystem,
57 swash_cache: SwashCache,
58 #[allow(dead_code)]
59 cache: Cache,
60 atlas: TextAtlas,
61 viewport: Viewport,
62 renderers: HashMap<Option<i16>, TextRenderer>,
65 prepared: BTreeSet<Option<i16>>,
67 sample_count: u32,
68 depth_format: Option<wgpu::TextureFormat>,
69 queued: Vec<Queued>,
70 buffers: HashMap<u32, HashMap<String, CachedBuffer>>,
73 frame: u64,
74}
75
76fn size_key(size_px: f32) -> u32 {
78 (size_px.max(0.0) * 100.0).round() as u32
79}
80
81#[cfg(target_arch = "wasm32")]
83const DEFAULT_FONT: &[u8] = include_bytes!("../fonts/DejaVuSans.ttf");
84
85fn new_font_system() -> FontSystem {
88 #[cfg(not(target_arch = "wasm32"))]
89 {
90 FontSystem::new()
91 }
92 #[cfg(target_arch = "wasm32")]
93 {
94 let mut fs = FontSystem::new_with_fonts([glyphon::fontdb::Source::Binary(
95 std::sync::Arc::new(DEFAULT_FONT),
96 )]);
97 let db = fs.db_mut();
98 db.set_sans_serif_family("DejaVu Sans");
99 db.set_serif_family("DejaVu Sans");
100 db.set_monospace_family("DejaVu Sans");
101 fs
102 }
103}
104
105impl TextSystem {
106 #[allow(clippy::too_many_arguments)]
107 pub fn new(
108 device: &wgpu::Device,
109 queue: &wgpu::Queue,
110 surface_format: wgpu::TextureFormat,
111 width: u32,
112 height: u32,
113 sample_count: u32,
114 depth_format: Option<wgpu::TextureFormat>,
115 ) -> Self {
116 let font_system = new_font_system();
117 let swash_cache = SwashCache::new();
118 let cache = Cache::new(device);
119 let atlas =
120 TextAtlas::with_color_mode(device, queue, &cache, surface_format, ColorMode::Accurate);
121 let mut viewport = Viewport::new(device, &cache);
122 viewport.update(queue, Resolution { width, height });
123 Self {
124 font_system,
125 swash_cache,
126 cache,
127 atlas,
128 viewport,
129 renderers: HashMap::new(),
130 prepared: BTreeSet::new(),
131 sample_count,
132 depth_format,
133 queued: Vec::new(),
134 buffers: HashMap::new(),
135 frame: 0,
136 }
137 }
138
139 pub fn load_font_bytes(&mut self, bytes: Vec<u8>) {
142 self.font_system.db_mut().load_font_data(bytes);
143 }
144
145 pub fn resize(&mut self, queue: &wgpu::Queue, width: u32, height: u32) {
146 self.viewport.update(queue, Resolution { width, height });
147 }
148
149 pub fn queue(
150 &mut self,
151 text: &str,
152 pos: [f32; 2],
153 size_px: f32,
154 color: [f32; 4],
155 layer: Option<i16>,
156 ) {
157 let c = |v: f32| (v.clamp(0.0, 1.0) * 255.0) as u8;
158 self.queued.push(Queued {
159 text: text.to_string(),
160 left: pos[0],
161 top: pos[1],
162 size_px,
163 color: [c(color[0]), c(color[1]), c(color[2]), c(color[3])],
164 layer,
165 });
166 }
167
168 pub fn measure(&mut self, text: &str, size_px: f32) -> [f32; 2] {
172 let buffer = self.shaped(text, size_px);
173 let (mut width, mut height) = (0.0f32, 0.0f32);
174 for run in buffer.layout_runs() {
175 width = width.max(run.line_w);
176 height = height.max(run.line_top + run.line_height);
177 }
178 [width, height]
179 }
180
181 fn shaped(&mut self, text: &str, size_px: f32) -> &Buffer {
183 let frame = self.frame;
184 let inner = self.buffers.entry(size_key(size_px)).or_default();
185 if !inner.contains_key(text) {
186 let metrics = Metrics::new(size_px, size_px * 1.2);
187 let mut buffer = Buffer::new(&mut self.font_system, metrics);
188 buffer.set_text(text, &Attrs::new(), Shaping::Advanced, None);
189 buffer.shape_until_scroll(&mut self.font_system, false);
190 inner.insert(
191 text.to_owned(),
192 CachedBuffer {
193 buffer,
194 last_used: frame,
195 },
196 );
197 }
198 let cached = inner.get_mut(text).expect("inserted above");
199 cached.last_used = frame;
200 &cached.buffer
201 }
202
203 pub fn collect_layers(&self, out: &mut BTreeSet<i16>) {
205 out.extend(self.queued.iter().filter_map(|q| q.layer));
206 }
207
208 pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
212 self.prepared.clear();
213 if self.queued.is_empty() {
214 return;
215 }
216 self.frame += 1;
217
218 let queued = std::mem::take(&mut self.queued);
221 for q in &queued {
222 self.shaped(&q.text, q.size_px);
223 }
224
225 let layers: BTreeSet<Option<i16>> = queued.iter().map(|q| q.layer).collect();
226 for layer in layers {
227 let areas = queued
228 .iter()
229 .filter(|q| q.layer == layer)
230 .map(|q| TextArea {
231 buffer: &self.buffers[&size_key(q.size_px)][&q.text].buffer,
232 left: q.left,
233 top: q.top,
234 scale: 1.0,
235 bounds: TextBounds::default(),
236 default_color: Color::rgba(q.color[0], q.color[1], q.color[2], q.color[3]),
237 custom_glyphs: &[],
238 });
239 let renderer = self.renderers.entry(layer).or_insert_with(|| {
240 TextRenderer::new(
241 &mut self.atlas,
242 device,
243 crate::target::multisample(self.sample_count),
244 self.depth_format.map(crate::target::no_write_depth),
245 )
246 });
247 match renderer.prepare(
248 device,
249 queue,
250 &mut self.font_system,
251 &mut self.atlas,
252 &self.viewport,
253 areas,
254 &mut self.swash_cache,
255 ) {
256 Ok(()) => {
257 self.prepared.insert(layer);
258 }
259 Err(e) => log::warn!("glyphon prepare failed: {e:?}"),
260 }
261 }
262 self.queued = queued;
264 self.queued.clear();
265 }
266
267 pub fn draw_layer(
270 &self,
271 layer: Option<i16>,
272 encoder: &mut wgpu::CommandEncoder,
273 targets: &Targets<'_>,
274 ) {
275 if !self.prepared.contains(&layer) {
276 return;
277 }
278 let Some(renderer) = self.renderers.get(&layer) else {
279 return;
280 };
281 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
282 label: Some("text.pass"),
283 color_attachments: &[Some(targets.color_attachment(wgpu::LoadOp::Load))],
284 depth_stencil_attachment: targets.depth_attachment(wgpu::LoadOp::Load),
285 occlusion_query_set: None,
286 timestamp_writes: None,
287 multiview_mask: None,
288 });
289 if let Err(e) = renderer.render(&self.atlas, &self.viewport, &mut pass) {
290 log::warn!("glyphon render failed: {e:?}");
291 }
292 }
293
294 pub fn finish(&mut self) {
297 if self.prepared.is_empty() {
298 return;
299 }
300 self.prepared.clear();
301 self.evict_stale();
302 self.atlas.trim();
303 }
304
305 fn evict_stale(&mut self) {
307 let frame = self.frame;
308 self.buffers.retain(|_, inner| {
309 inner.retain(|_, c| c.last_used + EVICT_AFTER_FRAMES >= frame);
310 !inner.is_empty()
311 });
312 }
313}