1use std::sync::{Arc, Mutex, MutexGuard};
11
12use cotis_defaults::element_configs::text_config::TextConfig;
13use cotis_utils::math::Dimensions;
14use glyphon::{
15 Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache,
16 TextArea, TextAtlas, TextBounds, TextRenderer, Viewport,
17};
18use wgpu::MultisampleState;
19
20use crate::pipeline::UIPosition;
21
22struct QueuedTextLine {
23 line: Buffer,
24 left: f32,
25 top: f32,
26 color: Color,
27 bounds: Option<(UIPosition, UIPosition)>,
28}
29
30pub struct TextState {
35 font_system: Arc<Mutex<FontSystem>>,
36 swash_cache: SwashCache,
37 viewport: Viewport,
38 atlas: TextAtlas,
39 text_renderer: TextRenderer,
40 measurement_buffer: Buffer,
41 lines: Vec<QueuedTextLine>,
42 dpi_scale: f32,
43}
44
45impl TextState {
46 pub fn new(
48 device: &wgpu::Device,
49 queue: &wgpu::Queue,
50 pixel_format: wgpu::TextureFormat,
51 size: (u32, u32),
52 dpi_scale: f32,
53 ) -> Self {
54 let font_system = Arc::new(Mutex::new(FontSystem::new()));
55 let swash_cache = SwashCache::new();
56 let cache = Cache::new(device);
57 let viewport = Viewport::new(device, &cache);
58 let mut atlas = TextAtlas::new(device, queue, &cache, pixel_format);
59 let text_renderer = TextRenderer::new(
60 &mut atlas,
61 device,
62 MultisampleState::default(),
63 Some(wgpu::DepthStencilState {
64 format: wgpu::TextureFormat::Depth32Float,
65 depth_write_enabled: true,
66 depth_compare: wgpu::CompareFunction::Less,
67 stencil: wgpu::StencilState::default(),
68 bias: wgpu::DepthBiasState::default(),
69 }),
70 );
71
72 let measurement_buffer = {
73 let mut locked = lock_font_system(&font_system);
74 Buffer::new(&mut locked, Metrics::new(30.0, 42.0))
75 };
76
77 let _ = size;
78
79 Self {
80 font_system,
81 swash_cache,
82 viewport,
83 atlas,
84 text_renderer,
85 measurement_buffer,
86 lines: Vec::new(),
87 dpi_scale,
88 }
89 }
90
91 pub fn set_dpi_scale(&mut self, dpi_scale: f32) {
93 self.dpi_scale = dpi_scale;
94 }
95
96 pub fn font_system(&self) -> Arc<Mutex<FontSystem>> {
100 self.font_system.clone()
101 }
102
103 #[allow(clippy::too_many_arguments)]
108 pub fn queue_text(
109 &mut self,
110 text: &str,
111 font_size: f32,
112 line_height: f32,
113 position: UIPosition,
114 bounds: Option<(UIPosition, UIPosition)>,
115 color: Color,
116 draw_order: f32,
117 ) {
118 let mut font_system = lock_font_system(&self.font_system);
119 let mut line = Buffer::new(&mut font_system, Metrics::new(font_size, line_height));
120 line.set_text(
121 &mut font_system,
122 text,
123 Attrs::new()
124 .family(Family::SansSerif)
125 .metadata((draw_order * 10_000.0) as usize),
126 Shaping::Advanced,
127 );
128 line.shape_until_scroll(&mut font_system, false);
129
130 self.lines.push(QueuedTextLine {
131 line,
132 left: position.x,
133 top: position.y,
134 color,
135 bounds,
136 });
137 }
138
139 pub fn flush(
144 &mut self,
145 device: &wgpu::Device,
146 queue: &wgpu::Queue,
147 render_pass: &mut wgpu::RenderPass<'_>,
148 surface_config: &wgpu::SurfaceConfiguration,
149 ) {
150 if self.lines.is_empty() {
151 return;
152 }
153
154 self.atlas.trim();
155 self.viewport.update(
156 queue,
157 Resolution {
158 width: surface_config.width,
159 height: surface_config.height,
160 },
161 );
162
163 let areas = self.lines.iter().map(|text_line| TextArea {
164 buffer: &text_line.line,
165 left: text_line.left,
166 top: text_line.top,
167 scale: 1.0,
168 bounds: match text_line.bounds {
169 Some((position, bounds)) => TextBounds {
170 left: position.x as i32,
171 top: position.y as i32,
172 right: (position.x + bounds.x) as i32,
173 bottom: (position.y + bounds.y) as i32,
174 },
175 None => TextBounds {
176 left: 0,
177 top: 0,
178 right: surface_config.width as i32,
179 bottom: surface_config.height as i32,
180 },
181 },
182 default_color: text_line.color,
183 custom_glyphs: &[],
184 });
185
186 let mut font_system = lock_font_system(&self.font_system);
187 if let Err(error) = self.text_renderer.prepare_with_depth(
188 device,
189 queue,
190 &mut font_system,
191 &mut self.atlas,
192 &self.viewport,
193 areas,
194 &mut self.swash_cache,
195 |metadata| metadata as f32 / 10_000.0,
196 ) {
197 log::warn!("glyphon prepare failed: {error}");
198 self.lines.clear();
199 return;
200 }
201
202 if let Err(error) = self
203 .text_renderer
204 .render(&self.atlas, &self.viewport, render_pass)
205 {
206 log::warn!("glyphon render failed: {error}");
207 }
208
209 self.lines.clear();
210 }
211
212 pub fn measure(&mut self, text: &str, config: &TextConfig) -> Dimensions {
217 let font_size = config.font_size * self.dpi_scale;
218 let line_height = if config.line_height > 0.0 {
219 config.line_height * self.dpi_scale
220 } else {
221 font_size * 1.5
222 };
223
224 let mut font_system = lock_font_system(&self.font_system);
225 self.measurement_buffer.set_metrics_and_size(
226 &mut font_system,
227 Metrics::new(font_size, line_height),
228 None,
229 None,
230 );
231 self.measurement_buffer.set_text(
232 &mut font_system,
233 text,
234 Attrs::new().family(Family::SansSerif),
235 Shaping::Advanced,
236 );
237 self.measurement_buffer
238 .shape_until_scroll(&mut font_system, false);
239
240 let line_width = self
241 .measurement_buffer
242 .layout_runs()
243 .next()
244 .map(|run| run.line_w)
245 .unwrap_or(0.0);
246 Dimensions::new(line_width, self.measurement_buffer.metrics().line_height)
247 }
248}
249
250fn lock_font_system(font_system: &Arc<Mutex<FontSystem>>) -> MutexGuard<'_, FontSystem> {
251 match font_system.lock() {
252 Ok(guard) => guard,
253 Err(error) => {
254 log::error!("font system mutex poisoned: {error}");
255 error.into_inner()
256 }
257 }
258}