fontdue 0.3.0

A simple no_std font parser and rasterizer.
Documentation
use crate::unicode::{linebreak_property, read_utf8, wrap_mask};
use crate::Font;
use crate::{
    platform::{ceil, floor},
    Metrics,
};
use alloc::vec::*;
use core::borrow::Borrow;
use core::hash::{Hash, Hasher};

/// Horizontal alignment options for text when a max_width is provided.
#[derive(Copy, Clone, PartialEq)]
pub enum HorizontalAlign {
    /// Aligns text to the left of the region defined by the max_width.
    Left,
    /// Aligns text to the center of the region defined by the max_width.
    Center,
    /// Aligns text to the right of the region defined by the max_width.
    Right,
}

/// Vertical alignment options for text when a max_height is provided.
#[derive(Copy, Clone, PartialEq)]
pub enum VerticalAlign {
    /// Aligns text to the top of the region defined by the max_height.
    Top,
    /// Aligns text to the middle of the region defined by the max_height.
    Middle,
    /// Aligns text to the bottom of the region defined by the max_height.
    Bottom,
}

/// Wrap style is a hint for how strings of text should be wrapped to the next line. Line wrapping
/// can happen when the max width/height is reached.
#[derive(Copy, Clone, PartialEq)]
pub enum WrapStyle {
    /// Word will break lines by the Unicode line breaking algorithm (Standard Annex #14) This will
    /// generally break lines where you expect them to be broken at and will preserve words.
    Word,
    /// Letter will not preserve words, breaking into a new line after the nearest letter.
    Letter,
}

/// Settings to configure how text layout is constrained. Text layout is considered best effort and
/// layout may violate the constraints defined here if they prevent text from being laid out.
#[derive(Copy, Clone, PartialEq)]
pub struct LayoutSettings {
    /// The leftmost boundary of the text region.
    pub x: f32,
    /// The topmost boundary of the text region.
    pub y: f32,
    /// An optional rightmost boundary on the text region. A line of text that exceeds the
    /// max_width is wrapped to the line below. If the width of a glyph is larger than the
    /// max_width, the glyph will overflow past the max_width. The application is responsible for
    /// handling the overflow.
    pub max_width: Option<f32>,
    /// An optional bottom boundary on the text region. This is used for positioning the
    /// vertical_align option. Text that exceeds the defined max_height will overflow past it. The
    /// application is responsible for handling the overflow.
    pub max_height: Option<f32>,
    /// The default is Left. This option does nothing if the max_width isn't set.
    pub horizontal_align: HorizontalAlign,
    /// The default is Top. This option does nothing if the max_height isn't set.
    pub vertical_align: VerticalAlign,
    /// The default is Word. Wrap style is a hint for how strings of text should be wrapped to the
    /// next line. Line wrapping can happen when the max width/height is reached.
    pub wrap_style: WrapStyle,
    /// The default is true. This option enables hard breaks, like new line characters, to
    /// prematurely wrap lines. If false, hard breaks will not prematurely create a new line.
    pub wrap_hard_breaks: bool,
    /// The default is false. This option sets whether or not to include whitespace in the layout
    /// output. By default, whitespace is not included in the output as it's not renderable. You
    /// may want this enabled if you care about the positioning of whitespace for an interactable
    /// user interface.
    pub include_whitespace: bool,
}

impl Default for LayoutSettings {
    fn default() -> LayoutSettings {
        LayoutSettings {
            x: 0.0,
            y: 0.0,
            max_width: None,
            max_height: None,
            horizontal_align: HorizontalAlign::Left,
            vertical_align: VerticalAlign::Top,
            wrap_style: WrapStyle::Word,
            wrap_hard_breaks: true,
            include_whitespace: false,
        }
    }
}

/// Configuration for rasterizing a glyph. This struct is also a hashable key that can be used to
/// uniquely identify a rasterized glyph for applications that want to cache glyphs.
#[derive(Debug, Copy, Clone)]
pub struct GlyphRasterConfig {
    /// The character represented by the glyph being positioned.
    pub c: char,
    /// The scale of the glyph being positioned in px.
    pub px: f32,
    /// The index of the font used in layout to raster the glyph.
    pub font_index: usize,
}

impl Hash for GlyphRasterConfig {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.c.hash(state);
        self.px.to_bits().hash(state);
        self.font_index.hash(state);
    }
}

impl PartialEq for GlyphRasterConfig {
    fn eq(&self, other: &Self) -> bool {
        self.c == other.c && self.px == other.px && self.font_index == other.font_index
    }
}

impl Eq for GlyphRasterConfig {}

/// A positioned scaled glyph.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct GlyphPosition {
    /// Hashable key that can be used to uniquely identify a rasterized glyph.
    pub key: GlyphRasterConfig,
    /// The left side of the glyph bounding box. Dimensions are in pixels, and are alawys whole
    /// numebrs.
    pub x: f32,
    /// The bottom side of the glyph bounding box. Dimensions are in pixels and are always whole
    /// numbers.
    pub y: f32,
    /// The width of the glyph. Dimensions are in pixels.
    pub width: usize,
    /// The height of the glyph. Dimensions are in pixels.
    pub height: usize,
}

/// A style description for a segment of text.
pub struct TextStyle<'a> {
    /// The text to layout.
    pub text: &'a str,
    /// The scale of the text in pixel units.
    pub px: f32,
    /// The font to layout the text in.
    pub font_index: usize,
}

impl<'a> TextStyle<'a> {
    pub fn new(text: &'a str, px: f32, font_index: usize) -> TextStyle<'a> {
        TextStyle {
            text,
            px,
            font_index,
        }
    }
}

#[derive(Debug, Copy, Clone)]
struct LineMetrics {
    pub padding: f32,
    pub ascent: f32,
    pub new_line_size: f32,
    pub end_index: usize,
}

/// Text layout requires a small amount of heap usage which is contained in the Layout struct. This
/// context is reused between layout calls. Reusing the Layout struct will greatly reduce memory
/// allocations and is advisable for performance.
pub struct Layout {
    line_metrics: Vec<LineMetrics>,
}

impl Layout {
    pub fn new() -> Layout {
        Layout {
            line_metrics: Vec::new(),
        }
    }

    fn wrap_mask_from_settings(settings: &LayoutSettings) -> u8 {
        let wrap_soft_breaks = settings.wrap_style == WrapStyle::Word;
        let wrap_hard_breaks = settings.wrap_hard_breaks;
        let wrap = settings.max_width.is_some() && (wrap_soft_breaks || wrap_hard_breaks);
        wrap_mask(wrap, wrap_soft_breaks, wrap_hard_breaks)
    }

    fn horizontal_padding(settings: &LayoutSettings, remaining_width: f32) -> f32 {
        if settings.max_width.is_none() {
            0.0
        } else {
            match settings.horizontal_align {
                HorizontalAlign::Left => 0.0,
                HorizontalAlign::Center => floor(remaining_width / 2.0),
                HorizontalAlign::Right => floor(remaining_width),
            }
        }
    }

    fn vertical_padding(settings: &LayoutSettings, height: f32) -> f32 {
        if let Some(max_height) = settings.max_height {
            if height >= max_height {
                0.0
            } else {
                match settings.vertical_align {
                    VerticalAlign::Top => 0.0,
                    VerticalAlign::Middle => floor((max_height - height) / 2.0),
                    VerticalAlign::Bottom => floor(max_height - height),
                }
            }
        } else {
            0.0
        }
    }

    /// Performs layout for text horizontally, and wrapping vertically. This makes a best effort
    /// attempt at laying out the text defined in the given styles with the provided layout
    /// settings. Text may overflow out of the bounds defined in the layout settings and it's up
    /// to the application to decide how to deal with this. Works with &[Font] or &[&Font].
    ///
    /// Characters from the input string can only be omitted from the output, they are never
    /// reordered. The output buffer will always contain characters in the order they were defined
    /// in the styles.
    pub fn layout_horizontal<T: Borrow<Font>>(
        &mut self,
        fonts: &[T],
        styles: &[&TextStyle],
        settings: &LayoutSettings,
        output: &mut Vec<GlyphPosition>,
    ) {
        // Reset internal buffers.
        unsafe {
            self.line_metrics.set_len(0);
            output.set_len(0);
        }

        // There is a lot of context.
        let wrap_mask = Self::wrap_mask_from_settings(settings); // Wrap mask based on settings.
        let max_width = settings.max_width.unwrap_or(core::f32::MAX); // The max width of the bounding box.
        let mut state: u8 = 0; // Current linebreak state.
        let mut last_linebreak_state = 0; // Last highest ranked linebreak state for the given line.
        let mut last_linebreak_x = 0.0; // X position of the last linebreak.
        let mut last_linebreak_index = 0; // Glyph position of the last linebreak.
        let mut current_x = 0.0; // Starting x for the current line.
        let mut caret_x = 0.0; // Total x for the whole text.
        let mut total_height = 0.0; // Total y for the whole text.
        let mut next_line = LineMetrics {
            padding: 0.0,
            ascent: 0.0,
            new_line_size: 0.0,
            end_index: core::usize::MAX,
        };
        let mut current_ascent = 0.0; // Ascent for the current style.
        let mut current_new_line_size = 0.0; // New line height for the current style.
        for style in styles {
            let mut byte_offset = 0;
            let font = &fonts[style.font_index];
            if let Some(metrics) = font.borrow().horizontal_line_metrics(style.px) {
                current_ascent = ceil(metrics.ascent);
                current_new_line_size = ceil(metrics.new_line_size);
                if current_ascent > next_line.ascent {
                    next_line.ascent = current_ascent;
                }
                if current_new_line_size > next_line.new_line_size {
                    next_line.new_line_size = current_new_line_size;
                }
            }
            while byte_offset < style.text.len() {
                let character = read_utf8(style.text, &mut byte_offset);
                let linebreak_state = linebreak_property(&mut state, character) & wrap_mask;
                let metrics = if character as u32 > 0x1F {
                    font.borrow().metrics(character, style.px)
                } else {
                    Metrics::default()
                };
                let advance = ceil(metrics.advance_width);
                if linebreak_state >= last_linebreak_state {
                    last_linebreak_state = linebreak_state;
                    last_linebreak_x = caret_x;
                    last_linebreak_index = output.len();
                }
                if caret_x - current_x + advance >= max_width || last_linebreak_state == 2 {
                    total_height += next_line.new_line_size;
                    next_line.padding = max_width - (last_linebreak_x - current_x);
                    next_line.end_index = last_linebreak_index;
                    self.line_metrics.push(next_line);
                    next_line.ascent = current_ascent;
                    next_line.new_line_size = current_new_line_size;
                    last_linebreak_state = 0;
                    current_x = last_linebreak_x;
                }
                if settings.include_whitespace || metrics.width != 0 {
                    output.push(GlyphPosition {
                        key: GlyphRasterConfig {
                            c: character,
                            px: style.px,
                            font_index: style.font_index,
                        },
                        x: caret_x + floor(metrics.bounds.xmin),
                        y: floor(metrics.bounds.ymin),
                        width: metrics.width,
                        height: metrics.height,
                    });
                }
                caret_x += advance;
            }
        }
        total_height += next_line.new_line_size;
        next_line.padding = max_width - (caret_x - current_x);
        next_line.end_index = core::usize::MAX;
        self.line_metrics.push(next_line);

        let mut line_metrics_index = 0;
        let mut next_line_index;
        let mut current_index = 0;
        let mut current_ascent;
        let mut current_new_line_size;
        let mut x_base = settings.x;
        let mut y_base = settings.y - Self::vertical_padding(settings, total_height);
        let line = self.line_metrics[0];
        next_line_index = line.end_index;
        current_ascent = line.ascent;
        current_new_line_size = line.new_line_size;
        x_base += Self::horizontal_padding(settings, line.padding);
        for glyph in output {
            if current_index == next_line_index {
                line_metrics_index += 1;
                let line = self.line_metrics[line_metrics_index];
                x_base = settings.x - glyph.x;
                y_base -= current_new_line_size;
                next_line_index = line.end_index;
                current_ascent = line.ascent;
                current_new_line_size = line.new_line_size;
                x_base += Self::horizontal_padding(settings, line.padding);
            }
            glyph.x += x_base;
            glyph.y += y_base - current_ascent;
            current_index += 1;
        }
    }
}