dexterior_visuals/pipelines/
text.rs1use glyphon as gh;
2use nalgebra as na;
3use std::sync::Arc;
4
5use crate::{
6 camera::Camera2D,
7 layout::Viewport,
8 render_window::{ActiveRenderWindow, RenderContext},
9};
10
11pub use gh::Color as TextColor;
12
13pub(crate) struct TextPipeline {
14 font_system: gh::FontSystem,
15 swash_cache: gh::SwashCache,
16 viewport: gh::Viewport,
17 atlas: gh::TextAtlas,
18 renderer: gh::TextRenderer,
19 pub draw_queue: Vec<TextBuffer>,
22}
23
24#[derive(Clone, Debug)]
26pub struct TextParams<'a> {
27 pub text: &'a str,
29 pub position: na::Vector2<f64>,
31 pub anchor: TextAnchor,
33 pub area_width: Option<f32>,
36 pub area_height: Option<f32>,
39 pub font_size: f32,
41 pub line_height: f32,
43 pub color: palette::LinSrgb,
45 pub attrs: gh::Attrs<'a>,
50}
51
52impl<'a> Default for TextParams<'a> {
53 fn default() -> Self {
54 Self {
55 text: "",
56 position: na::SVector::zeros(),
57 anchor: TextAnchor::Center,
58 area_width: None,
59 area_height: None,
60 font_size: 30.,
61 line_height: 42.,
62 color: palette::named::BLACK.into(),
63 attrs: default_text_attrs(),
64 }
65 }
66}
67
68pub fn default_text_attrs() -> gh::Attrs<'static> {
81 gh::Attrs::new().family(gh::Family::Name("JetBrains Mono"))
82}
83
84fn color_to_text_color(color: palette::LinSrgb) -> gh::Color {
88 let col_u8 = palette::Srgb::<u8>::from(color);
89 gh::Color::rgb(col_u8.red, col_u8.green, col_u8.blue)
90}
91
92#[derive(Clone, Copy, Debug)]
103#[allow(missing_docs)]
104pub enum TextAnchor {
105 TopLeft,
106 TopMid,
107 TopRight,
108 MidLeft,
109 Center,
110 MidRight,
111 BottomLeft,
112 BottomMid,
113 BottomRight,
114}
115
116pub(crate) struct TextBuffer {
118 buffer: gh::Buffer,
119 position: na::Vector2<f32>,
121 width: f32,
122 height: f32,
123 color: TextColor,
124}
125
126impl TextPipeline {
127 pub fn new(window: &ActiveRenderWindow) -> Self {
128 let font_system = gh::FontSystem::new_with_fonts([
132 gh::fontdb::Source::Binary(Arc::new(include_bytes!(
133 "../../fonts/JetBrainsMono-Regular.ttf"
134 ))),
135 gh::fontdb::Source::Binary(Arc::new(include_bytes!(
136 "../../fonts/JetBrainsMono-Italic.ttf"
137 ))),
138 gh::fontdb::Source::Binary(Arc::new(include_bytes!(
139 "../../fonts/JetBrainsMono-Bold.ttf"
140 ))),
141 ]);
142
143 let swash_cache = gh::SwashCache::new();
144 let cache = gh::Cache::new(&window.device);
145 let viewport = gh::Viewport::new(&window.device, &cache);
146 let mut atlas = gh::TextAtlas::new(
147 &window.device,
148 &window.queue,
149 &cache,
150 window.swapchain_format(),
151 );
152 let renderer =
153 gh::TextRenderer::new(&mut atlas, &window.device, window.multisample_state(), None);
154
155 Self {
156 font_system,
157 swash_cache,
158 viewport,
159 atlas,
160 renderer,
161 draw_queue: Vec::new(),
162 }
163 }
164
165 pub fn queue_text(&mut self, viewport: Viewport, camera: &Camera2D, params: TextParams<'_>) {
172 let mut buffer = gh::Buffer::new(
173 &mut self.font_system,
174 gh::Metrics {
175 font_size: params.font_size,
176 line_height: params.line_height,
177 },
178 );
179
180 buffer.set_size(&mut self.font_system, params.area_width, params.area_height);
181 buffer.set_text(
182 &mut self.font_system,
183 params.text,
184 ¶ms.attrs,
185 gh::Shaping::Advanced,
186 );
187 buffer.shape_until_scroll(&mut self.font_system, false);
188
189 let view_proj = camera.view_projection_matrix(viewport.size());
192 let half_vp = (viewport.width as f32 / 2., viewport.height as f32 / 2.);
193
194 let anchor_pos_ndc = view_proj
196 * na::Vector4::new(params.position[0] as f32, params.position[1] as f32, 0., 1.);
197 let anchor_pixel = na::Vector2::new(
198 anchor_pos_ndc.x * half_vp.0 + half_vp.0,
199 -anchor_pos_ndc.y * half_vp.1 + half_vp.1,
200 );
201
202 let (width, height) = buffer.size();
203 let width = match width {
204 Some(w) => w,
205 None => buffer
207 .layout_runs()
208 .map(|l| l.line_w)
209 .max_by(f32::total_cmp)
210 .unwrap_or(0.),
211 };
212 let height = match height {
213 Some(h) => h,
214 None => buffer
215 .layout_runs()
216 .last()
217 .map(|l| l.line_top + l.line_height)
218 .unwrap_or(0.),
219 };
220
221 use TextAnchor::*;
222 let top_left_pixel = anchor_pixel
223 - match params.anchor {
224 TopLeft => na::Vector2::zeros(),
225 TopMid => na::Vector2::new(width / 2., 0.),
226 TopRight => na::Vector2::new(width, 0.),
227 MidLeft => na::Vector2::new(0., height / 2.),
228 Center => na::Vector2::new(width / 2., height / 2.),
229 MidRight => na::Vector2::new(width, height / 2.),
230 BottomLeft => na::Vector2::new(0., height),
231 BottomMid => na::Vector2::new(width / 2., height),
232 BottomRight => na::Vector2::new(width, height),
233 };
234
235 let vp_offset = na::Vector2::new(viewport.x as f32, viewport.y as f32);
236
237 self.draw_queue.push(TextBuffer {
238 buffer,
239 position: vp_offset + top_left_pixel,
240 width,
241 height,
242 color: color_to_text_color(params.color),
243 });
244 }
245
246 pub fn draw(&mut self, ctx: &mut RenderContext<'_>) {
248 self.viewport.update(
249 ctx.queue,
250 gh::Resolution {
251 width: ctx.viewport_size.0,
252 height: ctx.viewport_size.1,
253 },
254 );
255
256 let areas = self.draw_queue.iter().map(|buf| gh::TextArea {
257 buffer: &buf.buffer,
258 left: buf.position.x,
259 top: buf.position.y,
260 scale: 1.,
261 bounds: gh::TextBounds {
262 left: buf.position.x as i32,
263 top: buf.position.y as i32,
264 right: (buf.position.x + buf.width + 1.) as i32,
265 bottom: (buf.position.y + buf.height + 1.) as i32,
266 },
267 default_color: buf.color,
268 custom_glyphs: &[],
269 });
270
271 self.renderer
272 .prepare(
273 ctx.device,
274 ctx.queue,
275 &mut self.font_system,
276 &mut self.atlas,
277 &self.viewport,
278 areas,
279 &mut self.swash_cache,
280 )
281 .expect("Failed to render text");
282
283 self.renderer
284 .render(&self.atlas, &self.viewport, &mut ctx.pass)
285 .expect("Failed to render text");
286
287 self.draw_queue.clear();
288 }
289}