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