Skip to main content

orbfont/
lib.rs

1// SPDX-License-Identifier: MIT
2
3#![cfg_attr(not(feature = "std"), no_std)]
4
5#[cfg(not(feature = "std"))]
6extern crate alloc;
7
8#[cfg(not(feature = "std"))]
9use num_traits::float::FloatCore;
10
11#[cfg(all(feature = "std", not(target_os = "redox")))]
12pub use font_loader::{
13    self,
14    system_fonts::{self, FontProperty, FontPropertyBuilder},
15};
16
17#[cfg(not(feature = "std"))]
18use alloc::string::{String, ToString};
19#[cfg(not(feature = "std"))]
20use alloc::vec::Vec;
21
22use orbclient::{Color, Renderer};
23
24#[derive(Clone)]
25pub struct Font {
26    inner: rusttype::Font<'static>,
27}
28
29impl Font {
30    /// Find a font from an optional type, family, and style, such as "Mono", "Fira", "Regular"
31    #[cfg(all(feature = "std", target_os = "redox"))]
32    pub fn find(
33        typeface: Option<&str>,
34        family: Option<&str>,
35        style: Option<&str>,
36    ) -> Result<Font, String> {
37        Font::from_path(&format!(
38            "/usr/share/fonts/{}/{}/{}.ttf",
39            typeface.unwrap_or("Mono"),
40            family.unwrap_or("Fira"),
41            style.unwrap_or("Regular")
42        ))
43    }
44
45    // A funciton to automate the process of building a font property  from "typeface, family, style"
46    #[cfg(all(feature = "std", not(target_os = "redox")))]
47    fn build_fontproperty(
48        typeface: Option<&str>,
49        family: Option<&str>,
50        style: Option<&str>,
51    ) -> FontProperty {
52        let mut font = FontPropertyBuilder::new();
53        if let Some(style) = style {
54            let style_caps = &style.to_uppercase();
55            let italic = style_caps.contains("ITALIC");
56            let oblique = style_caps.contains("OBLIQUE");
57            let bold = style_caps.contains("BOLD");
58            if italic {
59                font = font.italic();
60            }
61            if oblique {
62                font = font.oblique();
63            }
64            if bold {
65                font = font.bold();
66            }
67        }
68        if let Some(typeface) = typeface {
69            // FontProperty has no support for differentiating Sans and Serif.
70            let typeface_caps = &typeface.to_uppercase();
71            if typeface_caps.contains("MONO") {
72                font = font.monospace();
73            }
74        }
75        if let Some(family) = family {
76            if let Some(typeface) = typeface {
77                let typeface_caps = &typeface.to_uppercase();
78                // manually adding Serif and Sans
79                if typeface_caps.contains("SERIF") {
80                    font = font.family(&[family, "Serif"].concat());
81                } else if typeface_caps.contains("SANS") {
82                    font = font.family(&[family, "Sans"].concat());
83                }
84            } else {
85                font = font.family(family);
86            }
87        }
88        font.build()
89    }
90
91    #[cfg(all(feature = "std", not(target_os = "redox")))]
92    pub fn find(
93        typeface: Option<&str>,
94        family: Option<&str>,
95        style: Option<&str>,
96    ) -> Result<Font, String> {
97        // This funciton attempts to use the rust-font-loader library, a frontend
98        // to the ubiquitous C library fontconfig, to find and load the specified
99        // font.
100        let mut font = Font::build_fontproperty(typeface, family, style);
101        // font_loader::query specific returns an empty vector if there are no matches
102        // and does not tag the result with associated data like "italic", merely returns
103        // the name of the font if it exists.
104        let fonts = system_fonts::query_specific(&mut font); // Returns an empty vector if there are no matches.
105                                                             // Confirm that a font matched:
106        if !fonts.is_empty() {
107            // get the matched font straight from the data:
108            let font_data = system_fonts::get(&font); // Getting font data from properties
109            match font_data {
110                Some((data, _)) => {
111                    if let Some(font) = rusttype::Font::try_from_vec(data) {
112                        Ok(Font { inner: font })
113                    } else {
114                        Err("error constructing a Font from bytes".to_string())
115                    }
116                }
117                None => Err(format!("Could not get font {} from data", &fonts[0])),
118            }
119        } else {
120            // If no font matched, try again with no family, as concatenating "Sans" or "Serif" may rule out legitimate fonts
121            let mut font = Font::build_fontproperty(None, family, style);
122            let fonts = system_fonts::query_specific(&mut font);
123            if !fonts.is_empty() {
124                let font_data = system_fonts::get(&font);
125                match font_data {
126                    Some((data, _)) => {
127                        if let Some(font) = rusttype::Font::try_from_vec(data) {
128                            Ok(Font { inner: font })
129                        } else {
130                            Err("error constructing a Font from bytes".to_string())
131                        }
132                    }
133                    None => Err(format!("Could not get font {} from data", &fonts[0])),
134                }
135            } else {
136                // If no font matched, try to load the default font manually
137                Font::from_path("/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf")
138            }
139        }
140    }
141
142    /// Load a font from file path
143    #[cfg(feature = "std")]
144    pub fn from_path<P: AsRef<std::path::Path>>(path: P) -> Result<Font, String> {
145        let data = std::fs::read(path).map_err(|err| format!("failed to read font: {}", err))?;
146        if let Some(font) = rusttype::Font::try_from_vec(data) {
147            Ok(Font { inner: font })
148        } else {
149            Err("error constructing a Font from bytes".to_string())
150        }
151    }
152
153    /// Load a font from a slice
154    pub fn from_data(data: &'static [u8]) -> Result<Font, String> {
155        if let Some(font) = rusttype::Font::try_from_bytes(data) {
156            Ok(Font { inner: font })
157        } else {
158            Err("error constructing a Font from bytes".to_string())
159        }
160    }
161
162    /// Render provided text using the font
163    pub fn render<'a>(&'a self, text: &str, height: f32) -> Text<'a> {
164        let scale = rusttype::Scale::uniform(height);
165
166        // The origin of a line of text is at the baseline (roughly where non-descending letters sit).
167        // We don't want to clip the text, so we shift it down with an offset when laying it out.
168        // v_metrics.ascent is the distance between the baseline and the highest edge of any glyph in
169        // the font. That's enough to guarantee that there's no clipping.
170        let v_metrics = self.inner.v_metrics(scale);
171        let offset = rusttype::point(0.0, v_metrics.ascent);
172
173        // Glyphs to draw for "RustType". Feel free to try other strings.
174        let glyphs: Vec<rusttype::PositionedGlyph> =
175            self.inner.layout(text, scale, offset).collect();
176
177        // Find the most visually pleasing width to display
178        let width = glyphs
179            .iter()
180            .rev()
181            .find_map(|g| {
182                g.pixel_bounding_box()
183                    .map(|b| b.min.x as f32 + g.unpositioned().h_metrics().advance_width)
184            })
185            .unwrap_or(0.0);
186
187        Text {
188            w: width.ceil() as u32,
189            h: height.ceil() as u32,
190            glyphs,
191        }
192    }
193}
194
195pub struct Text<'a> {
196    w: u32,
197    h: u32,
198    glyphs: Vec<rusttype::PositionedGlyph<'a>>,
199}
200
201impl<'a> Text<'a> {
202    /// Return width of the text
203    pub fn width(&self) -> u32 {
204        self.w
205    }
206
207    /// Return height of the text
208    pub fn height(&self) -> u32 {
209        self.h
210    }
211
212    /// Draw the text onto a window and clipp the text to the given bounds
213    pub fn draw_clipped<R: Renderer + ?Sized>(
214        &self,
215        renderer: &mut R,
216        x: i32,
217        y: i32,
218        bounds_x: i32,
219        bounds_width: u32,
220        color: Color,
221    ) {
222        for g in &self.glyphs {
223            if let Some(bb) = g.pixel_bounding_box() {
224                g.draw(|off_x, off_y, v| {
225                    let off_x = off_x as i32 + bb.min.x;
226                    let off_y = off_y as i32 + bb.min.y;
227                    // There's still a possibility that the glyph clips the boundaries of the bitmap
228                    if off_x >= 0
229                        && off_x < self.w as i32
230                        && off_y >= 0
231                        && off_y < self.h as i32
232                        && x + off_x >= bounds_x
233                        && x + off_x <= bounds_x + bounds_width as i32
234                    {
235                        let c = (v * 255.0) as u32;
236                        renderer.pixel(
237                            x + off_x,
238                            y + off_y,
239                            Color {
240                                data: c << 24 | (color.data & 0x00FF_FFFF),
241                            },
242                        );
243                    }
244                });
245            }
246        }
247    }
248
249    /// Draw the text onto a window
250    pub fn draw<R: Renderer + ?Sized>(&self, renderer: &mut R, x: i32, y: i32, color: Color) {
251        self.draw_clipped(renderer, x, y, x, self.w, color);
252    }
253}