azul-text-layout 0.0.4

Text layout algorithms Azul desktop GUI framework
Documentation
//! Contains functions for laying out single words (uses HarfBuzz for context-aware font shaping).

use std::{slice, ptr, u32, ops::Deref, os::raw::{c_char, c_uint}};
use harfbuzz_sys::{
    hb_blob_create, hb_blob_destroy,
    hb_font_create, hb_font_destroy,
    hb_face_create, hb_face_destroy,
    hb_buffer_create, hb_buffer_destroy,
    hb_shape, hb_font_set_scale, hb_buffer_add_utf8, hb_ot_font_set_funcs,
    hb_buffer_get_glyph_infos, hb_buffer_get_glyph_positions,
    hb_buffer_guess_segment_properties, hb_buffer_allocation_successful,
    hb_blob_t, hb_memory_mode_t, hb_buffer_t,
    hb_glyph_position_t, hb_glyph_info_t, hb_font_t, hb_face_t,
    hb_feature_t, hb_tag_t,
    HB_MEMORY_MODE_READONLY,
};
use azul_core::{
    display_list::GlyphInstance,
    app_resources::{GlyphInfo, FontMetrics, GlyphPosition},
};
use azul_css::{LayoutPoint, LayoutSize};

const MEMORY_MODE_READONLY: hb_memory_mode_t = HB_MEMORY_MODE_READONLY;
pub(crate) const HB_SCALE_FACTOR: f32 = 128.0;

// NOTE: hb_tag_t = u32
// See: https://github.com/tangrams/harfbuzz-example/blob/master/src/hbshaper.h
//
// Translation of the original HB_TAG macro, defined in:
// https://github.com/harfbuzz/harfbuzz/blob/90dd255e570bf8ea3436e2f29242068845256e55/src/hb-common.h#L89
//
// NOTE: Minimum required rustc version for const fn is 1.31.
const fn create_hb_tag(tag: (char, char, char, char)) -> hb_tag_t {
    (((tag.0 as hb_tag_t) & 0xFF) << 24) |
    (((tag.1 as hb_tag_t) & 0xFF) << 16) |
    (((tag.2 as hb_tag_t) & 0xFF) << 8)  |
    (((tag.3 as hb_tag_t) & 0xFF) << 0)
}

// Kerning operations
const KERN_TAG: hb_tag_t = create_hb_tag(('k', 'e', 'r', 'n'));
// Standard ligature substitution
const LIGA_TAG: hb_tag_t = create_hb_tag(('l', 'i', 'g', 'a'));
// Contextual ligature substitution
const CLIG_TAG: hb_tag_t = create_hb_tag(('c', 'l', 'i', 'g'));

const FEATURE_KERNING_ON: hb_feature_t   = hb_feature_t { tag: KERN_TAG, value: 1, start: 0, end: u32::MAX };
const FEATURE_LIGATURE_ON: hb_feature_t  = hb_feature_t { tag: LIGA_TAG, value: 1, start: 0, end: u32::MAX };
const FEATURE_CLIG_ON: hb_feature_t      = hb_feature_t { tag: CLIG_TAG, value: 1, start: 0, end: u32::MAX };
// const FEATURE_KERNING_OFF: hb_feature_t  = hb_feature_t { tag: KERN_TAG, value: 0, start: 0, end: u32::MAX };
// const FEATURE_LIGATURE_OFF: hb_feature_t = hb_feature_t { tag: LIGA_TAG, value: 0, start: 0, end: u32::MAX };
// const FEATURE_CLIG_OFF: hb_feature_t     = hb_feature_t { tag: CLIG_TAG, value: 0, start: 0, end: u32::MAX };

// NOTE: kerning is a "feature" and has to be specifically turned on.
static ACTIVE_HB_FEATURES: [hb_feature_t;3] = [
    FEATURE_KERNING_ON,
    FEATURE_LIGATURE_ON,
    FEATURE_CLIG_ON,
];

#[derive(Debug, Clone)]
pub struct ShapedWord {
    pub glyph_infos: Vec<GlyphInfo>,
    pub glyph_positions: Vec<GlyphPosition>,
}

#[derive(Debug)]
pub struct HbFont<'a> {
    font_bytes: &'a [u8],
    font_index: u32,
    hb_face_bytes: *mut hb_blob_t,
    hb_face: *mut hb_face_t,
    hb_font: *mut hb_font_t,
}

impl<'a> HbFont<'a> {
    pub fn from_bytes(font_bytes: &'a [u8], font_index: u32) -> Self {

        // Create a HbFont with no destroy function (font is cleaned up by Rust destructor)

        let user_data_ptr = ptr::null_mut();
        let destroy_func = None;

        let font_ptr = font_bytes.as_ptr() as *const c_char;
        let hb_face_bytes = unsafe {
            hb_blob_create(font_ptr, font_bytes.len() as u32, MEMORY_MODE_READONLY, user_data_ptr, destroy_func)
        };
        let hb_face = unsafe { hb_face_create(hb_face_bytes, font_index as c_uint) };
        let hb_font = unsafe { hb_font_create(hb_face) };
        unsafe { hb_ot_font_set_funcs(hb_font) };

        Self {
            font_bytes,
            font_index,
            hb_face_bytes,
            hb_face,
            hb_font,
        }
    }
}

impl<'a> Drop for HbFont<'a> {
    fn drop(&mut self) {
        unsafe { hb_font_destroy(self.hb_font) };
        unsafe { hb_face_destroy(self.hb_face) };
        // TODO: Is this safe - memory may be deleted twice?
        unsafe { hb_blob_destroy(self.hb_face_bytes) };
    }
}

#[derive(Debug)]
pub struct HbScaledFont<'a> {
    pub font: &'a HbFont<'a>,
    pub font_size_px: f32,
}

impl<'a> HbScaledFont<'a> {
    /// Create a `HbScaledFont` from a
    pub fn from_font(font: &'a HbFont<'a>, font_size_px: f32) -> Self {
        let px = (font_size_px * HB_SCALE_FACTOR) as i32;
        unsafe { hb_font_set_scale(font.hb_font, px, px) };
        Self {
            font,
            font_size_px,
        }
    }
}

#[derive(Debug)]
pub struct HbBuffer<'a> {
    words: &'a str,
    hb_buffer: *mut hb_buffer_t,
}

impl<'a> HbBuffer<'a> {
    pub fn from_str(words: &'a str) -> Self {

        let hb_buffer = unsafe { hb_buffer_create() };
        unsafe { hb_buffer_allocation_successful(hb_buffer); };
        let word_ptr = words.as_ptr() as *const c_char; // HB handles UTF-8

        let word_len = words.len() as i32;

        // NOTE: It's not possible to take a sub-string into a UTF-8 buffer!

        unsafe {
            hb_buffer_add_utf8(hb_buffer, word_ptr, word_len, 0, word_len);
            // Guess the script, language and direction from the buffer
            hb_buffer_guess_segment_properties(hb_buffer);
        }

        Self {
            words,
            hb_buffer,
        }
    }
}

impl<'a> Drop for HbBuffer<'a> {
    fn drop(&mut self) {
        unsafe { hb_buffer_destroy(self.hb_buffer) };
    }
}

// The glyph infos are allocated by HarfBuzz and freed
// when the font is destroyed. This is a convenience wrapper that
// directly dereferences the internal hb_glyph_info_t and
// hb_glyph_position_t, to avoid extra allocations.
#[derive(Debug)]
pub struct CVec<T> {
    ptr: *const T,
    len: usize,
}

impl<T> Deref for CVec<T> {
    type Target = [T];

    fn deref(&self) -> &[T] {
        unsafe { slice::from_raw_parts(self.ptr, self.len) }
    }
}

pub type HbGlyphInfo = hb_glyph_info_t;
pub type HbGlyphPosition = hb_glyph_position_t;

/// Shaped word - memory of the glyph_infos and glyph_positions is owned by HarfBuzz,
/// therefore the `buf` and `font` have to live as least as long as the word is in use.
#[derive(Debug)]
pub struct HbShapedWord<'a> {
    pub buf: &'a HbBuffer<'a>,
    pub scaled_font: &'a HbScaledFont<'a>,
    pub glyph_infos: CVec<HbGlyphInfo>,
    pub glyph_positions: CVec<HbGlyphPosition>,
}

pub(crate) fn shape_word_hb<'a>(
    text: &'a HbBuffer<'a>,
    scaled_font: &'a HbScaledFont<'a>,
) -> HbShapedWord<'a> {

    let features = if ACTIVE_HB_FEATURES.is_empty() {
        ptr::null()
    } else {
        &ACTIVE_HB_FEATURES as *const _
    };

    let num_features = ACTIVE_HB_FEATURES.len() as u32;

    unsafe { hb_shape(scaled_font.font.hb_font, text.hb_buffer, features, num_features) };

    let mut glyph_count = 0;
    let glyph_infos = unsafe { hb_buffer_get_glyph_infos(text.hb_buffer, &mut glyph_count) };

    let mut position_count = glyph_count;
    let glyph_positions = unsafe { hb_buffer_get_glyph_positions(text.hb_buffer, &mut position_count) };

    // Assert that there are as many glyph infos as there are glyph positions
    assert_eq!(glyph_count, position_count);

    HbShapedWord {
        buf: text,
        scaled_font,
        glyph_infos: CVec {
            ptr: glyph_infos,
            len: glyph_count as usize,
        },
        glyph_positions: CVec {
            ptr: glyph_positions,
            len: glyph_count as usize,
        },
    }
}

pub(crate) fn get_word_visual_width_hb(glyph_positions: &[GlyphPosition]) -> f32 {
    glyph_positions.iter().map(|pos| pos.x_advance as f32 / HB_SCALE_FACTOR).sum()
}

pub(crate) fn get_glyph_instances_hb(
    glyph_infos: &[GlyphInfo],
    glyph_positions: &[GlyphPosition],
) -> Vec<GlyphInstance> {

    let mut current_cursor_x = 0.0;
    let mut current_cursor_y = 0.0;

    glyph_infos.iter().zip(glyph_positions.iter()).map(|(glyph_info, glyph_pos)| {
        let glyph_index = glyph_info.codepoint;

        let x_offset = glyph_pos.x_offset as f32 / HB_SCALE_FACTOR;
        let y_offset = glyph_pos.y_offset as f32 / HB_SCALE_FACTOR;
        let x_advance = glyph_pos.x_advance as f32 / HB_SCALE_FACTOR;
        let y_advance = glyph_pos.y_advance as f32 / HB_SCALE_FACTOR;

        let point = LayoutPoint::new(current_cursor_x + x_offset, current_cursor_y + y_offset);
        let size = LayoutSize::new(x_advance, y_advance);

        current_cursor_x += x_advance;
        current_cursor_y += y_advance;

        GlyphInstance {
            index: glyph_index,
            point,
            size,
        }
    }).collect()
}

/// Get the baseline for a font, you'll have to scale the
/// font size then later on for your given font size
pub fn get_font_metrics_freetype(font_bytes: &[u8], font_index: i32) -> FontMetrics {

    use std::convert::TryInto;
    use freetype::freetype::{
        FT_Long, FT_F26Dot6,
        FT_Init_FreeType, FT_Done_FreeType, FT_New_Memory_Face,
        FT_Done_Face, FT_Set_Char_Size, FT_Library, FT_Face,
    };

    const FT_ERR_OK: i32 = 0;
    const FAKE_FONT_SIZE: FT_F26Dot6 = 1000;

    let mut baseline = FontMetrics {
        font_size: FAKE_FONT_SIZE as usize,
        x_ppem: 0,
        y_ppem: 0,
        x_scale: 0,
        y_scale: 0,
        ascender: 0,
        descender: 0,
        height: 0,
        max_advance: 0,
    };

    let buf_len: FT_Long = match font_bytes.len().try_into().ok() {
        Some(s) => s,
        None => return baseline, // font too large for freetype
    };

    unsafe {
        // Initialize library
        let mut ft_library: FT_Library = ptr::null_mut();
        let error = FT_Init_FreeType(&mut ft_library);
        if error != FT_ERR_OK {
            return baseline;
        }

        // Load font
        let mut ft_face: FT_Face = ptr::null_mut();
        let error = FT_New_Memory_Face(ft_library, font_bytes.as_ptr(), buf_len, font_index as FT_Long, &mut ft_face);

        if error != FT_ERR_OK {
            FT_Done_FreeType(ft_library);
            return baseline;
        }

        const DPI: u32 = 72;

        // Set font size to fake 1000px
        let font_size = match FAKE_FONT_SIZE.try_into().ok() { Some(s) => s, None => return baseline };
        let error = FT_Set_Char_Size(ft_face, 0, font_size, DPI, DPI);
        if error != FT_ERR_OK {
            FT_Done_Face(ft_face);
            FT_Done_FreeType(ft_library);
            return baseline;
        }

        let ft_face_ref = &*ft_face;
        let ft_size_ref = &*ft_face_ref.size;
        let metrics = ft_size_ref.metrics;

        baseline = FontMetrics {
            font_size: FAKE_FONT_SIZE as usize,
            x_ppem: metrics.x_ppem,
            y_ppem: metrics.y_ppem,
            x_scale: metrics.x_scale as i64,
            y_scale: metrics.y_scale as i64,
            ascender: metrics.ascender as i64,
            descender: metrics.descender as i64,
            height: metrics.height as i64,
            max_advance: metrics.max_advance as i64,
        };

        FT_Done_Face(ft_face);
        FT_Done_FreeType(ft_library);
    }

    baseline
}