Skip to main content

dexterior_visuals/pipelines/
text.rs

1use 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    /// Glyphon requires us to draw all the text in one call,
20    /// so we collect each text request into a queue and flush it at the end of a frame.
21    pub draw_queue: Vec<TextBuffer>,
22}
23
24/// Parameters for displaying text.
25#[derive(Clone, Debug)]
26pub struct TextParams<'a> {
27    /// Content of the text to be displayed. Default: "".
28    pub text: &'a str,
29    /// Position of the anchor point in simulation space. Default: origin.
30    pub position: na::Vector2<f64>,
31    /// Placement of the anchor point relative to the text. Default: center.
32    pub anchor: TextAnchor,
33    /// Fixed width for the text area in pixels. Default: None.
34    /// If not given, the text is laid out based on its own dimensions.
35    pub area_width: Option<f32>,
36    /// Fixed height for the text area in pixels. Default: None.
37    /// If not given, the text is laid out based on its own dimensions.
38    pub area_height: Option<f32>,
39    /// Font size of the text. Default: 30.
40    pub font_size: f32,
41    /// Line height of the text. Default: 42.
42    pub line_height: f32,
43    /// Color of the text. Default: black.
44    pub color: palette::LinSrgb,
45    /// Set of cosmic-text attributes for the text buffer.
46    /// Default: [`default_text_attrs`].
47    ///
48    /// Types for the fields can be accessed through the library's re-export of [`glyphon`].
49    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
68/// The default [`Attrs`][gh::Attrs] used for text rendering.
69///
70/// These are the `cosmic_text` defaults,
71/// except with the font family changed to `JetBrains Mono`.
72/// This font is shipped with the library
73/// (under the SIL Open Font License 1.1)
74/// and therefore works also in WebAssembly builds,
75/// where you don't have access to system fonts.
76/// On native platforms you can change the font to anything
77/// that you have installed on your system;
78/// on the web we don't currently have a way to supply custom fonts
79/// (outside of forking and modifying the `TextPipeline::new` function).
80pub fn default_text_attrs() -> gh::Attrs<'static> {
81    gh::Attrs::new().family(gh::Family::Name("JetBrains Mono"))
82}
83
84/// Convert a `palette` color to a `glyphon` color.
85/// We should use `palette` colors in public APIs
86/// and use this for conversions internally.
87fn 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/// Where to place a text field
93/// relative to the position given in [`TextParams`].
94///
95/// For example, with an anchor of `TopMid`, the text will be placed
96/// relative to position `x` as follows:
97/// ```text
98/// ----x----
99/// |content|
100/// ---------
101/// ```
102#[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
116/// A text buffer ready for rendering, created with [`TextPipeline::create_buffer`].
117pub(crate) struct TextBuffer {
118    buffer: gh::Buffer,
119    /// position of the top left corner in screen space
120    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        // we need to include fonts in the binary
129        // for this to work on the web,
130        // since the wasm target doesn't have access to system fonts
131        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    /// Queue a text buffer for rendering this frame.
166    ///
167    /// We redo layout each frame because caching doesn't interact well
168    /// with the immediate mode user API.
169    /// This hasn't been a performance issue so far;
170    /// look into caching again if it becomes one.
171    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            &params.attrs,
185            gh::Shaping::Advanced,
186        );
187        buffer.shape_until_scroll(&mut self.font_system, false);
188
189        // compute buffer positioning on the screen
190
191        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        // TODO: option to position text in screen space
195        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            // no explicit width given, compute from layout
206            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    /// Draw all text buffers that have been queued during this frame.
247    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}