fyrox_ui/font/
mod.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21#![allow(clippy::unnecessary_to_owned)] // false-positive
22
23use crate::core::{
24    algebra::Vector2, rectpack::RectPacker, reflect::prelude::*, uuid::Uuid, uuid_provider,
25    visitor::prelude::*, TypeUuidProvider,
26};
27use fxhash::FxHashMap;
28use fyrox_core::math::Rect;
29use fyrox_resource::{
30    embedded_data_source, io::ResourceIo, manager::BuiltInResource, untyped::UntypedResource,
31    Resource, ResourceData,
32};
33use lazy_static::lazy_static;
34use std::{
35    error::Error,
36    fmt::{Debug, Formatter},
37    hash::{Hash, Hasher},
38    ops::Deref,
39    path::Path,
40};
41
42pub mod loader;
43
44#[derive(Debug)]
45pub struct FontGlyph {
46    pub bitmap_top: f32,
47    pub bitmap_left: f32,
48    pub bitmap_width: f32,
49    pub bitmap_height: f32,
50    pub advance: f32,
51    pub tex_coords: [Vector2<f32>; 4],
52    pub page_index: usize,
53    pub bounds: Rect<f32>,
54}
55
56/// Page is a storage for rasterized glyphs.
57pub struct Page {
58    pub pixels: Vec<u8>,
59    pub texture: Option<UntypedResource>,
60    pub rect_packer: RectPacker<usize>,
61    pub modified: bool,
62}
63
64impl Debug for Page {
65    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
66        f.debug_struct("Page")
67            .field("Pixels", &self.pixels)
68            .field("Texture", &self.texture)
69            .field("Modified", &self.modified)
70            .finish()
71    }
72}
73
74/// Atlas is a storage for glyphs of a particular size, each atlas could have any number of pages to
75/// store the rasterized glyphs.
76#[derive(Default, Debug)]
77pub struct Atlas {
78    pub glyphs: Vec<FontGlyph>,
79    pub char_map: FxHashMap<char, usize>,
80    pub pages: Vec<Page>,
81}
82
83impl Atlas {
84    fn glyph(
85        &mut self,
86        font: &fontdue::Font,
87        unicode: char,
88        height: FontHeight,
89        page_size: usize,
90    ) -> Option<&FontGlyph> {
91        let border = 2;
92
93        match self.char_map.get(&unicode) {
94            Some(glyph_index) => self.glyphs.get(*glyph_index),
95            None => {
96                // Char might be missing, because it wasn't requested earlier. Try to find
97                // it in the inner font and render/pack it.
98
99                if let Some(char_index) = font.chars().get(&unicode) {
100                    let (metrics, glyph_raster) =
101                        font.rasterize_indexed(char_index.get(), height.0);
102
103                    // Find a page, that is capable to fit the new character or create a new
104                    // page and put the character there.
105                    let mut placement_info =
106                        self.pages
107                            .iter_mut()
108                            .enumerate()
109                            .find_map(|(page_index, page)| {
110                                page.rect_packer
111                                    .find_free(metrics.width + border, metrics.height + border)
112                                    .map(|bounds| (page_index, bounds))
113                            });
114
115                    // No space for the character in any of the existing pages, create a new page.
116                    if placement_info.is_none() {
117                        let mut page = Page {
118                            pixels: vec![0; page_size * page_size],
119                            texture: None,
120                            rect_packer: RectPacker::new(page_size, page_size),
121                            modified: true,
122                        };
123
124                        let page_index = self.pages.len();
125
126                        match page
127                            .rect_packer
128                            .find_free(metrics.width + border, metrics.height + border)
129                        {
130                            Some(bounds) => {
131                                placement_info = Some((page_index, bounds));
132
133                                self.pages.push(page);
134                            }
135                            None => {
136                                // No free space in the given page size (requested glyph is too big).
137                                return None;
138                            }
139                        }
140                    }
141
142                    let (page_index, placement_rect) = placement_info?;
143                    let page = &mut self.pages[page_index];
144                    let glyph_index = self.glyphs.len();
145
146                    // Raise a flag to notify users that the content of the page has changed, and
147                    // it should be re-uploaded to GPU (if needed).
148                    page.modified = true;
149
150                    let mut glyph = FontGlyph {
151                        bitmap_left: metrics.xmin as f32,
152                        bitmap_top: metrics.ymin as f32,
153                        advance: metrics.advance_width,
154                        tex_coords: Default::default(),
155                        bitmap_width: metrics.width as f32,
156                        bitmap_height: metrics.height as f32,
157                        bounds: Rect::new(
158                            metrics.bounds.xmin,
159                            metrics.bounds.ymin,
160                            metrics.bounds.width,
161                            metrics.bounds.height,
162                        ),
163                        page_index,
164                    };
165
166                    let k = 1.0 / page_size as f32;
167
168                    let bw = placement_rect.w().saturating_sub(border);
169                    let bh = placement_rect.h().saturating_sub(border);
170                    let bx = placement_rect.x() + border / 2;
171                    let by = placement_rect.y() + border / 2;
172
173                    let tw = bw as f32 * k;
174                    let th = bh as f32 * k;
175                    let tx = bx as f32 * k;
176                    let ty = by as f32 * k;
177
178                    glyph.tex_coords[0] = Vector2::new(tx, ty);
179                    glyph.tex_coords[1] = Vector2::new(tx + tw, ty);
180                    glyph.tex_coords[2] = Vector2::new(tx + tw, ty + th);
181                    glyph.tex_coords[3] = Vector2::new(tx, ty + th);
182
183                    let row_end = by + bh;
184                    let col_end = bx + bw;
185
186                    // Copy glyph pixels to the atlas pixels
187                    for (src_row, row) in (by..row_end).enumerate() {
188                        for (src_col, col) in (bx..col_end).enumerate() {
189                            page.pixels[row * page_size + col] =
190                                glyph_raster[src_row * bw + src_col];
191                        }
192                    }
193
194                    self.glyphs.push(glyph);
195
196                    // Map the new glyph to its unicode position.
197                    self.char_map.insert(unicode, glyph_index);
198
199                    self.glyphs.get(glyph_index)
200                } else {
201                    None
202                }
203            }
204        }
205    }
206}
207
208#[derive(Default, Debug, Reflect, Visit)]
209#[reflect(hide_all)]
210pub struct Font {
211    #[visit(skip)]
212    pub inner: Option<fontdue::Font>,
213    #[visit(skip)]
214    pub atlases: FxHashMap<FontHeight, Atlas>,
215    #[visit(skip)]
216    pub page_size: usize,
217}
218
219uuid_provider!(Font = "692fec79-103a-483c-bb0b-9fc3a349cb48");
220
221impl ResourceData for Font {
222    fn type_uuid(&self) -> Uuid {
223        <Self as TypeUuidProvider>::type_uuid()
224    }
225
226    fn save(&mut self, _path: &Path) -> Result<(), Box<dyn Error>> {
227        Ok(())
228    }
229
230    fn can_be_saved(&self) -> bool {
231        false
232    }
233}
234
235#[derive(Copy, Clone, Default, Debug)]
236pub struct FontHeight(pub f32);
237
238impl From<f32> for FontHeight {
239    fn from(value: f32) -> Self {
240        Self(value)
241    }
242}
243
244impl PartialEq for FontHeight {
245    fn eq(&self, other: &Self) -> bool {
246        fyrox_core::value_as_u8_slice(&self.0) == fyrox_core::value_as_u8_slice(&other.0)
247    }
248}
249
250impl Eq for FontHeight {}
251
252impl Hash for FontHeight {
253    fn hash<H: Hasher>(&self, state: &mut H) {
254        // Don't care about "genius" Rust decision to make f32 non-hashable. If a user is dumb enough
255        // to put NaN or any other special value as a glyph height, then it is their choice.
256        fyrox_core::hash_as_bytes(&self.0, state)
257    }
258}
259
260pub type FontResource = Resource<Font>;
261
262lazy_static! {
263    pub static ref BUILT_IN_FONT: BuiltInResource<Font> =
264        BuiltInResource::new(embedded_data_source!("./built_in_font.ttf"), |data| {
265            FontResource::new_ok(
266                "__BUILT_IN_FONT__".into(),
267                Font::from_memory(data.to_vec(), 1024).unwrap(),
268            )
269        });
270}
271
272impl Font {
273    pub fn from_memory(
274        data: impl Deref<Target = [u8]>,
275        page_size: usize,
276    ) -> Result<Self, &'static str> {
277        let fontdue_font = fontdue::Font::from_bytes(data, fontdue::FontSettings::default())?;
278        Ok(Font {
279            inner: Some(fontdue_font),
280            atlases: Default::default(),
281            page_size,
282        })
283    }
284
285    pub async fn from_file<P: AsRef<Path>>(
286        path: P,
287        page_size: usize,
288        io: &dyn ResourceIo,
289    ) -> Result<Self, &'static str> {
290        if let Ok(file_content) = io.load_file(path.as_ref()).await {
291            Self::from_memory(file_content, page_size)
292        } else {
293            Err("Unable to read file")
294        }
295    }
296
297    /// Tries to get a glyph at the given unicode position of the given height. If there's no rendered
298    /// glyph, this method tries to render the glyph and put into a suitable atlas (see [`Atlas`] docs
299    /// for more info). If the given unicode position has no representation in the font, [`None`] will
300    /// be returned. If the requested size of the glyph is too big to fit into the page size of the
301    /// font, [`None`] will be returned. Keep in mind, that this method is free to create as many atlases
302    /// with any number of pages in them. Each atlas corresponds to a particular glyph size, each glyph
303    /// in the atlas could be rendered at any page in the atlas.
304    #[inline]
305    pub fn glyph(&mut self, unicode: char, height: f32) -> Option<&FontGlyph> {
306        self.atlases
307            .entry(FontHeight(height))
308            .or_insert_with(|| Atlas {
309                glyphs: Default::default(),
310                char_map: Default::default(),
311                pages: Default::default(),
312            })
313            .glyph(
314                self.inner
315                    .as_ref()
316                    .expect("Font reader must be initialized!"),
317                unicode,
318                FontHeight(height),
319                self.page_size,
320            )
321    }
322
323    #[inline]
324    pub fn ascender(&self, height: f32) -> f32 {
325        self.inner
326            .as_ref()
327            .unwrap()
328            .horizontal_line_metrics(height)
329            .map(|m| m.ascent)
330            .unwrap_or_default()
331    }
332
333    #[inline]
334    pub fn descender(&self, height: f32) -> f32 {
335        self.inner
336            .as_ref()
337            .unwrap()
338            .horizontal_line_metrics(height)
339            .map(|m| m.descent)
340            .unwrap_or_default()
341    }
342
343    #[inline]
344    pub fn horizontal_kerning(&self, height: f32, left: char, right: char) -> Option<f32> {
345        self.inner
346            .as_ref()
347            .unwrap()
348            .horizontal_kern(left, right, height)
349    }
350
351    #[inline]
352    pub fn page_size(&self) -> usize {
353        self.page_size
354    }
355
356    #[inline]
357    pub fn glyph_advance(&mut self, unicode: char, height: f32) -> f32 {
358        self.glyph(unicode, height)
359            .map_or(height, |glyph| glyph.advance)
360    }
361}
362
363/// Font builder allows you to load fonts in declarative manner.
364pub struct FontBuilder {
365    page_size: usize,
366}
367
368impl FontBuilder {
369    /// Creates a default FontBuilder.
370    pub fn new() -> Self {
371        Self { page_size: 1024 }
372    }
373
374    /// Creates a new font from the data at the specified path.
375    pub async fn build_from_file(
376        self,
377        path: impl AsRef<Path>,
378        io: &dyn ResourceIo,
379    ) -> Result<Font, &'static str> {
380        Font::from_file(path, self.page_size, io).await
381    }
382
383    /// Creates a new font from bytes in memory.
384    pub fn build_from_memory(self, data: impl Deref<Target = [u8]>) -> Result<Font, &'static str> {
385        Font::from_memory(data, self.page_size)
386    }
387}