Skip to main content

macroquad_ply/
text.rs

1//! Functions to load fonts and draw text.
2
3use std::collections::HashMap;
4
5use crate::{
6    color::Color,
7    get_context, get_quad_context,
8    math::{vec3, Rect},
9    texture::{Image, TextureHandle},
10    Error,
11};
12
13use crate::color::WHITE;
14use glam::vec2;
15
16use std::sync::{Arc, Mutex};
17pub(crate) mod atlas;
18
19use atlas::{Atlas, SpriteKey};
20
21#[derive(Debug, Clone)]
22pub(crate) struct CharacterInfo {
23    pub offset_x: i32,
24    pub offset_y: i32,
25    pub advance: f32,
26    pub sprite: SpriteKey,
27}
28
29/// TTF font loaded to GPU
30#[derive(Clone)]
31pub struct Font {
32    font: Arc<fontdue::Font>,
33    atlas: Arc<Mutex<Atlas>>,
34    characters: Arc<Mutex<HashMap<(char, u16), CharacterInfo>>>,
35}
36
37/// World space dimensions of the text, measured by "measure_text" function
38#[derive(Debug, Default, Clone, Copy)]
39pub struct TextDimensions {
40    /// Distance from very left to very right of the rasterized text
41    pub width: f32,
42    /// Distance from the bottom to the top of the text.
43    pub height: f32,
44    /// Height offset from the baseline of the text.
45    /// "draw_text(.., X, Y, ..)" will be rendered in a "Rect::new(X, Y - dimensions.offset_y, dimensions.width, dimensions.height)"
46    /// For reference check "text_measures" example.
47    pub offset_y: f32,
48}
49
50#[allow(dead_code)]
51fn require_fn_to_be_send() {
52    fn require_send<T: Send>() {}
53    require_send::<Font>();
54}
55
56impl std::fmt::Debug for Font {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.debug_struct("Font")
59            .field("font", &"fontdue::Font")
60            .finish()
61    }
62}
63
64impl Font {
65    pub(crate) fn load_from_bytes(atlas: Arc<Mutex<Atlas>>, bytes: &[u8]) -> Result<Font, Error> {
66        Ok(Font {
67            font: Arc::new(fontdue::Font::from_bytes(
68                bytes,
69                fontdue::FontSettings::default(),
70            )?),
71            characters: Arc::new(Mutex::new(HashMap::new())),
72            atlas,
73        })
74    }
75
76    pub(crate) fn set_atlas(&mut self, atlas: Arc<Mutex<Atlas>>) {
77        self.atlas = atlas;
78    }
79
80    pub(crate) fn set_characters(
81        &mut self,
82        characters: Arc<Mutex<HashMap<(char, u16), CharacterInfo>>>,
83    ) {
84        self.characters = characters;
85    }
86
87    pub(crate) fn ascent(&self, font_size: f32) -> f32 {
88        self.font.horizontal_line_metrics(font_size).unwrap().ascent
89    }
90
91    pub(crate) fn descent(&self, font_size: f32) -> f32 {
92        self.font
93            .horizontal_line_metrics(font_size)
94            .unwrap()
95            .descent
96    }
97
98    pub(crate) fn cache_glyph(&self, character: char, size: u16) {
99        if self.contains(character, size) {
100            return;
101        }
102
103        let (metrics, bitmap) = self.font.rasterize(character, size as f32);
104
105        let (width, height) = (metrics.width as u16, metrics.height as u16);
106
107        let sprite = self.atlas.lock().unwrap().new_unique_id();
108        self.atlas.lock().unwrap().cache_sprite(
109            sprite,
110            Image {
111                bytes: bitmap
112                    .iter()
113                    .flat_map(|coverage| vec![255, 255, 255, *coverage])
114                    .collect(),
115                width,
116                height,
117            },
118        );
119        let advance = metrics.advance_width;
120
121        let (offset_x, offset_y) = (metrics.xmin, metrics.ymin);
122
123        let character_info = CharacterInfo {
124            advance,
125            offset_x,
126            offset_y,
127            sprite,
128        };
129
130        self.characters
131            .lock()
132            .unwrap()
133            .insert((character, size), character_info);
134    }
135
136    pub(crate) fn get(&self, character: char, size: u16) -> Option<CharacterInfo> {
137        self.characters
138            .lock()
139            .unwrap()
140            .get(&(character, size))
141            .cloned()
142    }
143    /// Returns whether the character has been cached
144    pub(crate) fn contains(&self, character: char, size: u16) -> bool {
145        self.characters
146            .lock()
147            .unwrap()
148            .contains_key(&(character, size))
149    }
150
151    pub(crate) fn measure_text(
152        &self,
153        text: impl AsRef<str>,
154        font_size: u16,
155        font_scale_x: f32,
156        font_scale_y: f32,
157        mut glyph_callback: impl FnMut(f32),
158    ) -> TextDimensions {
159        let text = text.as_ref();
160
161        let dpi_scaling = miniquad::window::dpi_scale();
162        let font_size = (font_size as f32 * dpi_scaling).ceil() as u16;
163
164        let mut width = 0.0;
165        let mut min_y = f32::MAX;
166        let mut max_y = f32::MIN;
167
168        for character in text.chars() {
169            if !self.contains(character, font_size) {
170                self.cache_glyph(character, font_size);
171            }
172
173            let font_data = &self.characters.lock().unwrap()[&(character, font_size)];
174            let offset_y = font_data.offset_y as f32 * font_scale_y;
175
176            let atlas = self.atlas.lock().unwrap();
177            let glyph = atlas.get(font_data.sprite).unwrap().rect;
178            let advance = font_data.advance * font_scale_x;
179            glyph_callback(advance);
180            width += advance;
181            min_y = min_y.min(offset_y);
182            max_y = max_y.max(glyph.h * font_scale_y + offset_y);
183        }
184
185        TextDimensions {
186            width: width / dpi_scaling,
187            height: (max_y - min_y) / dpi_scaling,
188            offset_y: max_y / dpi_scaling,
189        }
190    }
191}
192
193impl Font {
194    /// List of ascii characters, may be helpful in combination with "populate_font_cache"
195    pub fn ascii_character_list() -> Vec<char> {
196        (0..255).filter_map(::std::char::from_u32).collect()
197    }
198
199    /// List of latin characters
200    pub fn latin_character_list() -> Vec<char> {
201        "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890!@#$%^&*(){}[].,:"
202            .chars()
203            .collect()
204    }
205
206    pub fn populate_font_cache(&self, characters: &[char], size: u16) {
207        for character in characters {
208            self.cache_glyph(*character, size);
209        }
210    }
211
212    /// Sets the [FilterMode](https://docs.rs/miniquad/latest/miniquad/graphics/enum.FilterMode.html#) of this font's texture atlas.
213    ///
214    /// Use Nearest if you need integer-ratio scaling for pixel art, for example.
215    ///
216    /// # Example
217    /// ```
218    /// # use macroquad::prelude::*;
219    /// # #[macroquad::main("test")]
220    /// # async fn main() {
221    /// let mut font = get_default_font();
222    /// font.set_filter(FilterMode::Linear);
223    /// # }
224    /// ```
225    pub fn set_filter(&mut self, filter_mode: miniquad::FilterMode) {
226        self.atlas.lock().unwrap().set_filter(filter_mode);
227    }
228
229    // pub fn texture(&self) -> Texture2D {
230    //     let font = get_context().fonts_storage.get_font(*self);
231
232    //     font.font_texture
233    // }
234}
235
236impl Default for Font {
237    fn default() -> Self {
238        get_default_font()
239    }
240}
241
242/// Arguments for "draw_text_ex" function such as font, font_size etc
243#[derive(Debug, Clone)]
244pub struct TextParams<'a> {
245    pub font: Option<&'a Font>,
246    /// Base size for character height. The size in pixel used during font rasterizing.
247    pub font_size: u16,
248    /// The glyphs sizes actually drawn on the screen will be font_size * font_scale
249    /// However with font_scale too different from 1.0 letters may be blurry
250    pub font_scale: f32,
251    /// Font X axis would be scaled by font_scale * font_scale_aspect
252    /// and Y axis would be scaled by font_scale
253    /// Default is 1.0
254    pub font_scale_aspect: f32,
255    /// Text rotation in radian
256    /// Default is 0.0
257    pub rotation: f32,
258    pub color: Color,
259}
260
261impl<'a> Default for TextParams<'a> {
262    fn default() -> TextParams<'a> {
263        TextParams {
264            font: None,
265            font_size: 20,
266            font_scale: 1.0,
267            font_scale_aspect: 1.0,
268            color: WHITE,
269            rotation: 0.0,
270        }
271    }
272}
273
274/// Load font from file with "path"
275pub async fn load_ttf_font(path: &str) -> Result<Font, Error> {
276    let bytes = crate::file::load_file(path)
277        .await
278        .map_err(|_| Error::FontError("The Font file couldn't be loaded"))?;
279
280    load_ttf_font_from_bytes(&bytes[..])
281}
282
283/// Load font from bytes array, may be use in combination with include_bytes!
284/// ```ignore
285/// let font = load_ttf_font_from_bytes(include_bytes!("font.ttf"));
286/// ```
287pub fn load_ttf_font_from_bytes(bytes: &[u8]) -> Result<Font, Error> {
288    let atlas = Arc::new(Mutex::new(Atlas::new(
289        get_quad_context(),
290        miniquad::FilterMode::Linear,
291    )));
292
293    let mut font = Font::load_from_bytes(atlas.clone(), bytes)?;
294
295    font.populate_font_cache(&Font::ascii_character_list(), 15);
296
297    let ctx = get_context();
298
299    font.set_filter(ctx.default_filter_mode);
300
301    Ok(font)
302}
303
304/// Draw text with given font_size
305/// Returns text size
306pub fn draw_text(
307    text: impl AsRef<str>,
308    x: f32,
309    y: f32,
310    font_size: f32,
311    color: Color,
312) -> TextDimensions {
313    draw_text_ex(
314        text,
315        x,
316        y,
317        TextParams {
318            font_size: font_size as u16,
319            font_scale: 1.0,
320            color,
321            ..Default::default()
322        },
323    )
324}
325
326/// Draw text with custom params such as font, font size and font scale
327/// Returns text size
328pub fn draw_text_ex(text: impl AsRef<str>, x: f32, y: f32, params: TextParams) -> TextDimensions {
329    let text = text.as_ref();
330
331    if text.is_empty() {
332        return TextDimensions::default();
333    }
334
335    let font = params
336        .font
337        .unwrap_or(&get_context().fonts_storage.default_font);
338
339    let dpi_scaling = miniquad::window::dpi_scale();
340
341    let rot = params.rotation;
342    let font_scale_x = params.font_scale * params.font_scale_aspect;
343    let font_scale_y = params.font_scale;
344    let font_size = (params.font_size as f32 * dpi_scaling).ceil() as u16;
345
346    let mut total_width = 0.0;
347    let mut max_offset_y = f32::MIN;
348    let mut min_offset_y = f32::MAX;
349
350    for character in text.chars() {
351        if !font.contains(character, font_size) {
352            font.cache_glyph(character, font_size);
353        }
354
355        let char_data = &font.characters.lock().unwrap()[&(character, font_size)];
356        let offset_x = char_data.offset_x as f32 * font_scale_x;
357        let offset_y = char_data.offset_y as f32 * font_scale_y;
358
359        let mut atlas = font.atlas.lock().unwrap();
360        let glyph = atlas.get(char_data.sprite).unwrap().rect;
361        let glyph_scaled_h = glyph.h * font_scale_y;
362
363        min_offset_y = min_offset_y.min(offset_y);
364        max_offset_y = max_offset_y.max(glyph_scaled_h + offset_y);
365
366        let rot_cos = rot.cos();
367        let rot_sin = rot.sin();
368        let dest_x = (offset_x + total_width) * rot_cos + (glyph_scaled_h + offset_y) * rot_sin;
369        let dest_y = (offset_x + total_width) * rot_sin + (-glyph_scaled_h - offset_y) * rot_cos;
370
371        let dest = Rect::new(
372            dest_x / dpi_scaling + x,
373            dest_y / dpi_scaling + y,
374            glyph.w / dpi_scaling * font_scale_x,
375            glyph.h / dpi_scaling * font_scale_y,
376        );
377
378        total_width += char_data.advance * font_scale_x;
379
380        crate::texture::draw_texture_ex(
381            &crate::texture::Texture2D {
382                texture: TextureHandle::Unmanaged(atlas.texture()),
383            },
384            dest.x,
385            dest.y,
386            params.color,
387            crate::texture::DrawTextureParams {
388                dest_size: Some(vec2(dest.w, dest.h)),
389                source: Some(glyph),
390                rotation: rot,
391                pivot: Some(vec2(dest.x, dest.y)),
392                ..Default::default()
393            },
394        );
395    }
396
397    TextDimensions {
398        width: total_width / dpi_scaling,
399        height: (max_offset_y - min_offset_y) / dpi_scaling,
400        offset_y: max_offset_y / dpi_scaling,
401    }
402}
403
404/// Draw multiline text with the given font_size, line_distance_factor and color.
405/// If no line distance but a custom font is given, the fonts line gap will be used as line distance factor if it exists.
406pub fn draw_multiline_text(
407    text: impl AsRef<str>,
408    x: f32,
409    y: f32,
410    font_size: f32,
411    line_distance_factor: Option<f32>,
412    color: Color,
413) -> TextDimensions {
414    draw_multiline_text_ex(
415        text,
416        x,
417        y,
418        line_distance_factor,
419        TextParams {
420            font_size: font_size as u16,
421            font_scale: 1.0,
422            color,
423            ..Default::default()
424        },
425    )
426}
427
428/// Draw multiline text with the given line distance and custom params such as font, font size and font scale.
429/// If no line distance but a custom font is given, the fonts newline size will be used as line distance factor if it exists, else default to font size.
430pub fn draw_multiline_text_ex(
431    text: impl AsRef<str>,
432    mut x: f32,
433    mut y: f32,
434    line_distance_factor: Option<f32>,
435    params: TextParams,
436) -> TextDimensions {
437    let line_distance = match line_distance_factor {
438        Some(distance) => distance,
439        None => {
440            let mut font_line_distance = 0.0;
441            let font = if let Some(font) = params.font {
442                font
443            } else {
444                &get_default_font()
445            };
446            if let Some(metrics) = font.font.horizontal_line_metrics(1.0) {
447                font_line_distance = metrics.new_line_size;
448            }
449
450            font_line_distance
451        }
452    };
453
454    let mut dimensions = TextDimensions::default();
455    let y_step = line_distance * params.font_size as f32 * params.font_scale;
456
457    for line in text.as_ref().lines() {
458        let line_dimensions = draw_text_ex(line, x, y, params.clone());
459        x -= (line_distance * params.font_size as f32 * params.font_scale) * params.rotation.sin();
460        y += (line_distance * params.font_size as f32 * params.font_scale) * params.rotation.cos();
461
462        dimensions.width = f32::max(dimensions.width, line_dimensions.width);
463        dimensions.height += y_step;
464
465        if dimensions.offset_y == 0.0 {
466            dimensions.offset_y = line_dimensions.offset_y;
467        }
468    }
469
470    dimensions
471}
472
473/// Get the text center.
474pub fn get_text_center(
475    text: impl AsRef<str>,
476    font: Option<&Font>,
477    font_size: u16,
478    font_scale: f32,
479    rotation: f32,
480) -> crate::Vec2 {
481    let measure = measure_text(text, font, font_size, font_scale);
482
483    let x_center = measure.width / 2.0 * rotation.cos() + measure.height / 2.0 * rotation.sin();
484    let y_center = measure.width / 2.0 * rotation.sin() - measure.height / 2.0 * rotation.cos();
485
486    crate::Vec2::new(x_center, y_center)
487}
488
489pub fn measure_text(
490    text: impl AsRef<str>,
491    font: Option<&Font>,
492    font_size: u16,
493    font_scale: f32,
494) -> TextDimensions {
495    let font = font.unwrap_or_else(|| &get_context().fonts_storage.default_font);
496
497    font.measure_text(text, font_size, font_scale, font_scale, |_| {})
498}
499
500pub fn measure_multiline_text(
501    text: &str,
502    font: Option<&Font>,
503    font_size: u16,
504    font_scale: f32,
505    line_distance_factor: Option<f32>,
506) -> TextDimensions {
507    let font = font.unwrap_or_else(|| &get_context().fonts_storage.default_font);
508    let line_distance = match line_distance_factor {
509        Some(distance) => distance,
510        None => match font.font.horizontal_line_metrics(1.0) {
511            Some(metrics) => metrics.new_line_size,
512            None => 1.0,
513        },
514    };
515
516    let mut dimensions = TextDimensions::default();
517    let y_step = line_distance * font_size as f32 * font_scale;
518
519    for line in text.lines() {
520        let line_dimensions = font.measure_text(line, font_size, font_scale, font_scale, |_| {});
521
522        dimensions.width = f32::max(dimensions.width, line_dimensions.width);
523        dimensions.height += y_step;
524        if dimensions.offset_y == 0.0 {
525            dimensions.offset_y = line_dimensions.offset_y;
526        }
527    }
528
529    dimensions
530}
531
532/// Converts word breaks to newlines wherever the text would otherwise exceed the given length.
533pub fn wrap_text(
534    text: &str,
535    font: Option<&Font>,
536    font_size: u16,
537    font_scale: f32,
538    maximum_line_length: f32,
539) -> String {
540    let font = font.unwrap_or_else(|| &get_context().fonts_storage.default_font);
541
542    // This is always a bit too much memory, but it saves a lot of reallocations.
543    let mut new_text =
544        String::with_capacity(text.len() + text.chars().filter(|c| c.is_whitespace()).count());
545
546    let mut current_word_start = 0usize;
547    let mut current_word_end = 0usize;
548    let mut characters = text.char_indices();
549    let mut total_width = 0.0;
550    let mut word_width = 0.0;
551
552    font.measure_text(text, font_size, font_scale, font_scale, |mut width| {
553        // It's impossible this is called more often than the text has characters.
554        let (idx, c) = characters.next().unwrap();
555        let mut keep_char = true;
556
557        if c.is_whitespace() {
558            new_text.push_str(&text[current_word_start..idx + c.len_utf8()]);
559            current_word_start = idx + c.len_utf8();
560            word_width = 0.0;
561            keep_char = false;
562
563            // If we would wrap, ignore the whitespace.
564            if total_width + width > maximum_line_length {
565                width = 0.0;
566            }
567        }
568
569        // If a single word expands past the length limit, just break it up.
570        if word_width + width > maximum_line_length {
571            new_text.push_str(&text[current_word_start..current_word_end]);
572            new_text.push('\n');
573            current_word_start = current_word_end;
574            total_width = 0.0;
575            word_width = 0.0;
576        }
577
578        current_word_end = idx + c.len_utf8();
579        if keep_char {
580            word_width += width;
581        }
582
583        if c == '\n' {
584            total_width = 0.0;
585            word_width = 0.0;
586            return;
587        }
588
589        total_width += width;
590
591        if total_width > maximum_line_length {
592            new_text.push('\n');
593            total_width = word_width;
594        }
595    });
596
597    new_text.push_str(&text[current_word_start..current_word_end]);
598
599    new_text
600}
601
602pub(crate) struct FontsStorage {
603    default_font: Font,
604}
605
606impl FontsStorage {
607    pub(crate) fn new(ctx: &mut dyn miniquad::RenderingBackend) -> FontsStorage {
608        let atlas = Arc::new(Mutex::new(Atlas::new(ctx, miniquad::FilterMode::Linear)));
609
610        let default_font = Font::load_from_bytes(atlas, include_bytes!("ProggyClean.ttf")).unwrap();
611        FontsStorage { default_font }
612    }
613}
614
615/// Returns macroquads default font.
616pub fn get_default_font() -> Font {
617    let context = get_context();
618    context.fonts_storage.default_font.clone()
619}
620
621/// Replaces macroquads default font with `font`.
622pub fn set_default_font(font: Font) {
623    let context = get_context();
624    context.fonts_storage.default_font = font;
625}
626
627/// From given font size in world space gives
628/// (font_size, font_scale and font_aspect) params to make rasterized font
629/// looks good in currently active camera
630pub fn camera_font_scale(world_font_size: f32) -> (u16, f32, f32) {
631    let context = get_context();
632    let (scr_w, scr_h) = miniquad::window::screen_size();
633    let cam_space = context
634        .projection_matrix()
635        .inverse()
636        .transform_vector3(vec3(2., 2., 0.));
637    let (cam_w, cam_h) = (cam_space.x.abs(), cam_space.y.abs());
638
639    let screen_font_size = world_font_size * scr_h / cam_h;
640
641    let font_size = screen_font_size as u16;
642
643    (font_size, cam_h / scr_h, scr_h / scr_w * cam_w / cam_h)
644}