kaku/
text.rs

1//! Types for creating and configuring drawable text objects.
2//!
3//! The main type here is [Text], which can be created using [TextRenderer::create_text]. This is a
4//! piece of text which can be drawn to the screen with a variety of effects.
5
6use ab_glyph::{Font, PxScale};
7use wgpu::util::DeviceExt;
8
9use crate::{FontId, TextRenderer};
10
11/// Options for a text outline.
12#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
13pub(crate) struct Outline {
14    pub(crate) color: [f32; 4],
15    pub(crate) width: f32,
16}
17
18#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
19pub(crate) struct SdfTextData {
20    pub(crate) radius: f32,
21    pub(crate) outline: Option<Outline>,
22}
23
24#[derive(Debug, Clone, PartialEq, PartialOrd)]
25pub(crate) struct TextData {
26    pub(crate) text: String,
27    pub(crate) font: FontId,
28    pub(crate) position: [f32; 2],
29    pub(crate) color: [f32; 4],
30    pub(crate) scale: f32,
31    pub(crate) halign: HorizontalAlignment,
32    pub(crate) valign: VerticalAlignment,
33
34    pub(crate) sdf: Option<SdfTextData>,
35}
36
37impl TextData {
38    fn settings_uniform(&self) -> SettingsUniform {
39        SettingsUniform {
40            color: self.color,
41            text_position: self.position,
42            _padding: [0.; 2],
43        }
44    }
45
46    fn sdf_settings_uniform(&self) -> SdfSettingsUniform {
47        let sdf = &self
48            .sdf
49            .expect("sdf_settings_uniform called but no sdf data found");
50        let outline_color = sdf.outline.map(|o| o.color).unwrap_or([0.; 4]);
51        let outline_width = sdf.outline.map(|o| o.width).unwrap_or(0.);
52        let sdf_radius = sdf.radius;
53
54        SdfSettingsUniform {
55            color: self.color,
56            outline_color,
57            text_position: self.position,
58            outline_width,
59            sdf_radius,
60            image_scale: self.scale,
61            _padding: [0.; 3],
62        }
63    }
64}
65
66/// Settings for font size.
67#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
68pub enum FontSize {
69    /// A font's size in pt.
70    Pt(f32),
71    /// A font's size in px.
72    Px(f32),
73}
74
75impl FontSize {
76    pub(crate) fn scale(&self, font: &impl Font) -> PxScale {
77        match self {
78            FontSize::Px(px) => font.pt_to_px_scale(*px * (72. / 96.)).unwrap(),
79            FontSize::Pt(pt) => font.pt_to_px_scale(*pt).unwrap(),
80        }
81    }
82
83    pub(crate) fn px_size(&self, font: &impl Font) -> f32 {
84        self.scale(font).y
85    }
86}
87
88/// Settings for horizontal text alignment
89///
90/// These control where the text drawn is with respect to its position
91#[derive(Copy, Clone, Debug, Default, PartialEq, PartialOrd)]
92pub enum HorizontalAlignment {
93    /// Anchors the position at the left side of the text.
94    ///
95    /// Text is drawn starting at the render position.
96    #[default]
97    Left,
98    /// Anchors the position to the middle of the text.
99    Center,
100    /// Anchors the position at the right side of the text.
101    ///
102    /// Text is drawn ending at the render position.
103    Right,
104    /// Anchors the text position at some point between the start and end of the text.
105    ///
106    /// A value of 0 is Left alignment, a value of 1 is Right alignment, and values in between
107    /// shift between the two continuously (e.g., a value of 0.5 is Center alignment).
108    ///
109    /// Values outside the range of 0-1 will be clamped within it.
110    Ratio(f32),
111}
112
113impl HorizontalAlignment {
114    /// The proportion of the alignment.
115    ///
116    /// This ranges from 0-1, where 0 is Left alignment and 1 is Right alignment.
117    pub fn proportion(&self) -> f32 {
118        match self {
119            Self::Left => 0.,
120            Self::Right => 1.,
121            Self::Center => 0.5,
122            Self::Ratio(r) => r.clamp(0., 1.),
123        }
124    }
125}
126
127/// Settings for vertical text alignment.
128///
129/// See <https://freetype.org/freetype2/docs/glyphs/glyphs-3.html> for more info on font metrics.
130#[derive(Default, Copy, Clone, Debug, PartialEq, PartialOrd)]
131pub enum VerticalAlignment {
132    /// Anchors the position to the baseline of the text.
133    ///
134    /// In the roman alphabet, the baseline is usually at the bottom of characters such as a, b, c,
135    /// etc. Characters like g or j usually go below the baseline.
136    #[default]
137    Baseline,
138    /// Anchors the position to the highest point of the font.
139    ///
140    /// This means characters will never rise above the render position.
141    Top,
142    /// Anchors the position to be exactly halfway between the highest and lowest points of the
143    /// font.
144    Middle,
145    /// Anchors the position to the lowest point of the font.
146    ///
147    /// This means characters will never go below the render position
148    Bottom,
149    /// Anchors the position at some point between the highest and lowest points of the font.
150    ///
151    /// A value of 0 is Bottom alignment, a value of 1 is Top alignment, and values in between
152    /// shift between the two continuously (e.g., a value of 0.5 is Middle alignment).
153    ///
154    /// Values outside the range of 0-1 will be clamped within it.
155    Ratio(f32),
156}
157
158/// A builder for a [Text] struct.
159#[derive(Debug, Clone, PartialEq, PartialOrd)]
160pub struct TextBuilder {
161    text: String,
162    font: FontId,
163    position: [f32; 2],
164    outline: Option<Outline>,
165    color: [f32; 4],
166    scale: f32,
167    custom_font_size: Option<FontSize>,
168    halign: HorizontalAlignment,
169    valign: VerticalAlignment,
170}
171
172impl TextBuilder {
173    /// Creates a new TextBuilder.
174    pub fn new(text: impl Into<String>, font: FontId, position: [f32; 2]) -> Self {
175        Self {
176            text: text.into(),
177            font,
178            position,
179
180            outline: None,
181            color: [0., 0., 0., 1.],
182            scale: 1.,
183            custom_font_size: None,
184            halign: Default::default(),
185            valign: Default::default(),
186        }
187    }
188
189    /// Creates a new Text object from the current configuration and uploads any necessary data
190    /// to the GPU.
191    pub fn build(
192        &self,
193        device: &wgpu::Device,
194        queue: &wgpu::Queue,
195        text_renderer: &mut TextRenderer,
196    ) -> Text {
197        let scale = match self.custom_font_size {
198            None => self.scale,
199            Some(size) => {
200                let self_size = size.px_size(&text_renderer.fonts.get(self.font).font);
201                let font_size = text_renderer.fonts.get(self.font).px_size;
202
203                self.scale * (self_size / font_size)
204            }
205        };
206
207        let data = TextData {
208            text: self.text.clone(),
209            font: self.font,
210            position: self.position,
211            color: self.color,
212            scale,
213            halign: self.halign,
214            valign: self.valign,
215
216            sdf: text_renderer.font_uses_sdf(self.font).then(|| SdfTextData {
217                radius: text_renderer
218                    .fonts
219                    .get(self.font)
220                    .sdf_settings
221                    .unwrap()
222                    .radius,
223                outline: self.outline,
224            }),
225        };
226        Text::new(data, device, queue, text_renderer)
227    }
228
229    /// Sets the content of the text.
230    pub fn text(&mut self, text: String) -> &mut Self {
231        self.text = text;
232        self
233    }
234
235    /// Sets the font the text will be drawn with.
236    pub fn font(&mut self, font: FontId) -> &mut Self {
237        self.font = font;
238        self
239    }
240
241    /// Sets the position of the text on the screen, in pixel coordinates.
242    pub fn position(&mut self, position: [f32; 2]) -> &mut Self {
243        self.position = position;
244        self
245    }
246
247    /// Sets the horizontal alignment of the text.
248    ///
249    /// See [HorizontalAlignment] for details.
250    pub fn horizontal_align(&mut self, halign: HorizontalAlignment) -> &mut Self {
251        self.halign = halign;
252        self
253    }
254
255    /// Sets the vertical alignment of the text.
256    ///
257    /// See [VerticalAlignment] for details.
258    pub fn vertical_align(&mut self, valign: VerticalAlignment) -> &mut Self {
259        self.valign = valign;
260        self
261    }
262
263    /// Adds an outline to the text, with given colour and width. If the width is less than or
264    /// equal to zero, this turns off the outline.
265    ///
266    /// Text can only be outlined if it is drawn using sdf, so if the font is not sdf-enabled then
267    /// this won't do anything. The outline can only be as wide as the sdf radius of the font. If
268    /// you want a wider outline, use a wider radius (see [crate::SdfSettings]).
269    pub fn outlined(&mut self, color: [f32; 4], width: f32) -> &mut Self {
270        if width > 0. {
271            self.outline = Some(Outline { color, width });
272        } else {
273            self.outline = None;
274        }
275
276        self
277    }
278
279    /// Sets this text to have no outline.
280    ///
281    /// Text will not be outlined by default, so only use this if you've already set the outline
282    /// and want to get rid of it e.g. when building another text object.
283    pub fn no_outline(&mut self) -> &mut Self {
284        self.outline = None;
285        self
286    }
287
288    /// Sets the colour of the text, in RGBA (values are in the range 0-1). The default is solid
289    /// black.
290    pub fn color(&mut self, color: [f32; 4]) -> &mut Self {
291        self.color = color;
292        self
293    }
294
295    /// Sets the scale of the text. The default is 1.0.
296    ///
297    /// If the font is not sdf-enabled, it will be scaled up bilinearly, and you may get
298    /// pixellation/bluriness. If it is sdf-enabled, it will be cleaner but you may still get
299    /// artefacts at high scale.
300    pub fn scale(&mut self, scale: f32) -> &mut Self {
301        self.scale = scale;
302        self
303    }
304
305    /// Adjusts the text scale so that it is drawn at a certain font size. If the argument is None,
306    /// it resets the text to the default size of the font (the size it was loaded into the text
307    /// renderer with).
308    ///
309    /// If the font is not SDF-enabled, then upscaling will be done with bilinear filtering,
310    /// and will not look very good.
311    ///
312    /// Note that this is multiplicative with the scale option; e.g. if the font size is set to be
313    /// 40pt and the scale is set to 2.0, then the font will be drawn at 80pt size.
314    pub fn font_size(&mut self, size: Option<FontSize>) -> &mut Self {
315        self.custom_font_size = size;
316        self
317    }
318}
319
320#[repr(C)]
321#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
322pub(crate) struct SettingsUniform {
323    color: [f32; 4],
324    text_position: [f32; 2],
325    _padding: [f32; 2],
326}
327
328#[repr(C)]
329#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
330pub(crate) struct SdfSettingsUniform {
331    color: [f32; 4],
332    outline_color: [f32; 4],
333    text_position: [f32; 2],
334    outline_width: f32,
335    sdf_radius: f32,
336    image_scale: f32,
337    _padding: [f32; 3],
338}
339
340/// A piece of text that can be rendered to the screen.
341///
342/// Create one of these using a [TextBuilder], then render it to a wgpu render pass using
343/// [TextRenderer::draw_text].
344#[derive(Debug)]
345pub struct Text {
346    pub(crate) data: TextData,
347    pub(crate) instance_buffer: wgpu::Buffer,
348    pub(crate) settings_bind_group: wgpu::BindGroup,
349
350    settings_buffer: wgpu::Buffer,
351    instance_capacity: usize,
352}
353
354impl Text {
355    /// Creates a new [Text] object and uploads all necessary data to the GPU.
356    fn new(
357        data: TextData,
358        device: &wgpu::Device,
359        queue: &wgpu::Queue,
360        text_renderer: &mut TextRenderer,
361    ) -> Self {
362        text_renderer.generate_char_textures(data.text.chars(), data.font, device, queue);
363        let instances = text_renderer.create_text_instances(&data);
364
365        let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
366            label: Some("kaku text instance buffer"),
367            contents: bytemuck::cast_slice(&instances),
368            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
369        });
370
371        let (settings_buffer, settings_bind_group) = if text_renderer.font_uses_sdf(data.font) {
372            let text_settings = data.sdf_settings_uniform();
373            let settings_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
374                label: Some("kaku sdf text settings uniform buffer"),
375                contents: bytemuck::cast_slice(&[text_settings]),
376                usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
377            });
378
379            let settings_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
380                label: Some("kaku sdf text settings uniform bind group"),
381                layout: &text_renderer.sdf_settings_layout,
382                entries: &[wgpu::BindGroupEntry {
383                    binding: 0,
384                    resource: settings_buffer.as_entire_binding(),
385                }],
386            });
387
388            (settings_buffer, settings_bind_group)
389        } else {
390            let text_settings = data.settings_uniform();
391
392            let settings_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
393                label: Some("kaku text settings uniform buffer"),
394                contents: bytemuck::cast_slice(&[text_settings]),
395                usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
396            });
397
398            let settings_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
399                label: Some("kaku text settings uniform bind group"),
400                layout: &text_renderer.settings_layout,
401                entries: &[wgpu::BindGroupEntry {
402                    binding: 0,
403                    resource: settings_buffer.as_entire_binding(),
404                }],
405            });
406
407            (settings_buffer, settings_bind_group)
408        };
409
410        Self {
411            data,
412            instance_buffer,
413            settings_bind_group,
414            settings_buffer,
415            instance_capacity: instances.len(),
416        }
417    }
418
419    /// Changes the text displayed by this text object.
420    ///
421    /// This is faster than recreating the object because it may reuse its existing gpu buffer
422    /// instead of recreating it.
423    pub fn set_text(
424        &mut self,
425        text: String,
426        device: &wgpu::Device,
427        queue: &wgpu::Queue,
428        text_renderer: &mut TextRenderer,
429    ) {
430        text_renderer.generate_char_textures(text.chars(), self.data.font, device, queue);
431        self.data.text = text;
432        let new_instances = text_renderer.create_text_instances(&self.data);
433
434        if new_instances.len() > self.instance_capacity {
435            self.instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
436                label: Some("kaku text instance buffer"),
437                contents: bytemuck::cast_slice(&new_instances),
438                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
439            });
440
441            self.instance_capacity = new_instances.len();
442        } else {
443            queue.write_buffer(
444                &self.instance_buffer,
445                0,
446                bytemuck::cast_slice(&new_instances),
447            );
448        }
449    }
450
451    // Uploads the current settings (as described in self.data) to the settings buffer on the GPU.
452    fn update_settings_buffer(&self, queue: &wgpu::Queue) {
453        if self.data.sdf.is_some() {
454            queue.write_buffer(
455                &self.settings_buffer,
456                0,
457                bytemuck::cast_slice(&[self.data.sdf_settings_uniform()]),
458            );
459        } else {
460            queue.write_buffer(
461                &self.settings_buffer,
462                0,
463                bytemuck::cast_slice(&[self.data.settings_uniform()]),
464            );
465        }
466    }
467
468    /// Changes the color of the text.
469    pub fn set_color(&mut self, color: [f32; 4], queue: &wgpu::Queue) {
470        self.data.color = color;
471        self.update_settings_buffer(queue);
472    }
473
474    /// Changes the scale of the text.
475    pub fn set_scale(&mut self, scale: f32, queue: &wgpu::Queue) {
476        self.data.scale = scale;
477        self.update_settings_buffer(queue);
478    }
479
480    /// Changes the position of the text on the screen.
481    pub fn set_position(&mut self, position: [f32; 2], queue: &wgpu::Queue) {
482        self.data.position = position;
483        self.update_settings_buffer(queue);
484    }
485
486    /// Sets the outline to be on with the given options. If the width is less than or equal to zero, it turns
487    /// the outline off.
488    ///
489    /// This does nothing if the font is not rendered with sdf.
490    pub fn set_outline(&mut self, color: [f32; 4], width: f32, queue: &wgpu::Queue) {
491        if let Some(sdf) = &mut self.data.sdf {
492            if width > 0. {
493                sdf.outline = Some(Outline { color, width });
494            } else {
495                sdf.outline = None;
496            }
497        }
498
499        self.update_settings_buffer(queue);
500    }
501
502    /// Removes the outline from the text, if there was one.
503    ///
504    /// This does nothing if the font is not rendered with sdf.
505    pub fn set_no_outline(&mut self, queue: &wgpu::Queue) {
506        if let Some(sdf) = &mut self.data.sdf {
507            sdf.outline = None;
508        }
509
510        self.update_settings_buffer(queue)
511    }
512}