three_d_text_builder/
text_ref.rs

1// External Dependencies ------------------------------------------------------
2// ----------------------------------------------------------------------------
3use three_d::{Srgba, Mat4, vec2, Vec2, vec3, vec4, SquareMatrix, Transform};
4use fontdue::{
5    Font,
6    layout::{GlyphPosition, Layout, LayoutSettings, TextStyle}
7};
8
9
10// Internal Dependencies ------------------------------------------------------
11// ----------------------------------------------------------------------------
12use crate::{TextAlign, TextPosition};
13use crate::geometry::mesh::TextMeshData;
14
15
16///
17/// Description of how a string slice should be rendered as a text mesh.
18///
19pub struct TextRef<'a> {
20    /// Text to render
21    pub text: &'a str,
22    /// Font size used by the resulting mesh
23    pub size: f32,
24    /// Font size used for glyph rasterization; when `None`, [``TextRef::size``] will be used
25    pub raster_size: Option<f32>,
26    /// Line height scaling used by the resulting mesh
27    pub line_height: Option<f32>,
28    /// Padding used by the resuling mesh
29    pub padding: Vec2,
30    /// Font color used by the resulting mesh
31    pub color: Srgba,
32    /// Shadow color and offset user by the resulting mesh
33    pub shadow: Option<(Srgba, Vec2)>,
34    /// Function to generate per-character colors
35    pub character_color: Option<&'a dyn Fn(usize, usize, char) -> Srgba>,
36    /// Alignment used by the resulting mesh.
37    pub align: TextAlign,
38    /// Position used by the resulting; see [``TextAlign``] for how this is computed
39    pub position: TextPosition,
40    /// Transform used by the resulting mesh
41    pub transform: Mat4,
42    /// Function to generate per-character position and vertex transforms
43    pub character_transform: Option<&'a dyn Fn(usize, usize, char) -> (Mat4, Mat4)>
44}
45
46impl<'a> Default for TextRef<'a> {
47    fn default() -> Self {
48        Self {
49            text: "Text",
50            size: 32.0,
51            raster_size: None,
52            line_height: None,
53            padding: vec2(0.0, 0.0),
54            color: Srgba::WHITE,
55            character_color: None,
56            align: TextAlign::default(),
57            shadow: None,
58            position: TextPosition::Pixels(vec2(0.0, 0.0)),
59            transform: Mat4::identity(),
60            character_transform: None
61        }
62    }
63}
64
65impl<'a> TextRef<'a> {
66    /// Creates a new text description for the given string slice.
67    pub fn new(text: &'a str) -> Self {
68        Self {
69            text,
70            ..Default::default()
71        }
72    }
73
74    /// Specifies the font size used by the resulting mesh.
75    pub fn size(mut self, size: f32) -> Self {
76        self.size = size;
77        self
78    }
79
80    /// Specifies the font size used for glyph rasterization; when `None`, [``TextRef::size``] will be used.
81    pub fn raster_size(mut self, raster_size: f32) -> Self {
82        self.raster_size = Some(raster_size);
83        self
84    }
85
86    /// Specifies the line height scaling used by the resulting mesh.
87    pub fn line_height(mut self, line_height: f32) -> Self {
88        self.line_height = Some(line_height);
89        self
90    }
91
92    /// Specifies the padding used by the resulting mesh.
93    pub fn padding(mut self, padding: Vec2) -> Self {
94        self.padding = padding;
95        self
96    }
97
98    /// Specifies the font color used by the resulting mesh.
99    pub fn color(mut self, color: Srgba) -> Self {
100        self.color = color;
101        self
102    }
103
104    /// Specifies the shadow color and offset user by the resulting mesh.
105    pub fn shadow(mut self, color: Srgba, offset: Vec2) -> Self {
106        self.shadow = Some((color, offset));
107        self
108    }
109
110    /// Specifies the function to generate per-character colors.
111    ///
112    /// These colors do overide the color set by [``TextRef::color``].
113    pub fn character_color(mut self, color: &'a dyn Fn(usize, usize, char) -> Srgba) -> Self {
114        self.character_color = Some(color);
115        self
116    }
117
118    /// Specifies the alignment used by the resulting mesh.
119    pub fn align(mut self, align: TextAlign) -> Self {
120        self.align = align;
121        self
122    }
123
124    /// Specifies the position used by the resulting; see [``TextAlign``] for how this is computed.
125    pub fn position(mut self, position: TextPosition) -> Self {
126        self.position = position;
127        self
128    }
129
130    /// Specifies the transform used by the resulting mesh.
131    pub fn transform(mut self, transform: Mat4) -> Self {
132        self.transform = transform;
133        self
134    }
135
136    /// Specifies the function to generate per-character position and vertex transforms
137    pub fn character_transform(mut self, transform: &'a dyn Fn(usize, usize, char) -> (Mat4, Mat4)) -> Self {
138        self.character_transform = Some(transform);
139        self
140    }
141}
142
143impl<'a> TextRef<'a> {
144    pub(crate) fn glyph_raster_size(&self) -> f32 {
145        self.raster_size.unwrap_or(self.size)
146    }
147
148    pub(crate) fn walk_glyphs<
149        T,
150        C: FnMut(
151            usize,
152            usize,
153            &GlyphPosition,
154            Option<&'a dyn Fn(usize, usize, char) -> (Mat4, Mat4)>,
155            Option<&'a dyn Fn(usize, usize, char) -> Srgba>
156
157        ) -> Option<T>
158
159    >(&self, font: &Font, layout: &mut Layout, line_height: f32, mut callback: C) -> (Vec<T>, Vec2) {
160        layout.reset(&LayoutSettings {
161            line_height: self.line_height.unwrap_or(line_height),
162            ..Default::default()
163        });
164        layout.append(&[font], &TextStyle::new(self.text, self.size, 0));
165
166        // Fontdue generates incosistent widths for the last character in a text.
167        //
168        // This can cause visible jitter when this character changes very frequently.
169        //
170        // To avoid this we, push another blank character onto the string, this will
171        // cause fontdue to provide consistent width calculations and results
172        // in stable positioning of the generated text meshes.
173        layout.append(&[font], &TextStyle::new(" ", self.size, 0));
174
175        let count = layout.glyphs().len();
176        let mut results = Vec::with_capacity(count);
177        let mut bounds = vec2(0.0f32, 0.0);
178        for (index, glyph) in layout.glyphs().iter().enumerate() {
179            if let Some(result) = callback(index, count, glyph, self.character_transform, self.character_color) {
180                results.push(result);
181            }
182            bounds.x = bounds.x.max(glyph.x + glyph.width as f32);
183            bounds.y = bounds.y.max(glyph.y.abs());
184        }
185        (results, bounds + self.padding * 2.0)
186    }
187
188    pub(crate) fn extend_mesh(
189        &self,
190        mesh_data: &mut TextMeshData,
191        viewport: Vec2,
192        global_offset: Vec2,
193        local_offset: Vec2,
194        dimensions: Vec2
195    ) {
196        // Vertex Data
197        let index = mesh_data.positions.len() as u32;
198        mesh_data.indices.extend_from_slice(&[index, index + 1, index + 2, index + 2, index + 3, index]);
199
200        let color = vec4(0.0, 0.0, 0.0, 0.0);
201        mesh_data.colors.extend_from_slice(&[color, color, color, color]);
202        mesh_data.quad_uvs.extend_from_slice(&[vec2(1.0, 1.0), vec2(1.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 1.0)]);
203        mesh_data.glyph_uvs.extend_from_slice(&[vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0), vec2(0.0, 0.0)]);
204
205        // Build vertices so they match the bounding box without the padding
206        let width = dimensions.x - self.padding.x * 2.0;
207        let height = dimensions.y - self.padding.y * 2.0;
208        let offset = vec3(self.padding.x, -height - self.padding.y, 0.0);
209
210        // Compute quad vertices
211        let tl = offset + vec3(0.0, 0.0, 0.0);
212        let br = offset + vec3(width, height, 0.0);
213        let bl = offset + vec3(0.0, height, 0.0);
214        let tr = offset + vec3(width, 0.0, 0.0);
215
216        // Calculate the center of the text
217        let center = vec3(local_offset.x.round(), local_offset.y.round(), 0.0);
218
219        // Apply global text transform and translation
220        let g = self.position.to_pixels(viewport) + global_offset;
221        let translation = vec3(g.x.round(), g.y.round(), 0.0);
222        mesh_data.positions.extend_from_slice(&[
223            self.transform.transform_vector(center + br) + translation,
224            self.transform.transform_vector(center + tr) + translation,
225            self.transform.transform_vector(center + tl) + translation,
226            self.transform.transform_vector(center + bl) + translation
227        ]);
228    }
229}
230