sugarloaf 0.4.4

Sugarloaf is Rio rendering engine, designed to be multiplatform. It is based on WebGPU, Rust library for Desktops and WebAssembly for Web (JavaScript). This project is created and maintained for Rio terminal purposes but feel free to use it.
// Copyright (c) 2023-present, Raphael Amorim.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

use crate::sugarloaf::primitives::is_private_user_area;
use crate::SpanStyle;
use rustc_hash::FxHashMap;
use swash::Attributes;
use unicode_width::UnicodeWidthChar;

/// Unscaled horizontal advance for a glyph + the font's units-per-em,
/// stored together so callers can recover pixels at any font size:
/// `advance_units * font_size / units_per_em`. One cache entry survives
/// font-size changes.
#[derive(Debug, Clone, Copy)]
pub struct AdvanceInfo {
    pub advance_units: f32,
    pub units_per_em: u16,
}

impl AdvanceInfo {
    /// Scaled horizontal advance in pixels. Returns 0.0 when
    /// `units_per_em` is 0 (malformed/missing font metrics) so callers
    /// don't need a separate guard.
    #[inline]
    pub fn scaled(&self, font_size: f32) -> f32 {
        if self.units_per_em > 0 {
            self.advance_units * font_size / self.units_per_em as f32
        } else {
            0.0
        }
    }
}

/// Resolved glyph metadata: which font owns it, how wide it is in
/// terminal cells, whether it lives in a Unicode private-use area
/// (Nerd Font icons, custom symbol fonts, etc.), and — lazily — the
/// horizontal advance in the winning font's design units.
///
/// `advance` starts `None` and is filled only on the first
/// `char_advance` query for this `(char, attrs)`. The terminal grid
/// hot path doesn't need per-char pixel advances (it uses
/// `width` — cell count — for layout), so we don't pay for an hmtx /
/// upem read on every unique cell glyph.
#[derive(Debug, Clone, Copy)]
pub struct ResolvedGlyph {
    pub font_id: usize,
    pub width: f32,
    pub is_pua: bool,
    pub advance: Option<AdvanceInfo>,
}

/// Plain hash-map glyph cache. `get` is `&self` (no LRU promotion),
/// so callers can hold an immutable reference without borrow
/// conflicts. Unbounded — the working set of `(char, attrs)` pairs
/// in a terminal session is finite and small relative to memory.
pub(crate) struct FontCache {
    cache: FxHashMap<(char, Attributes), ResolvedGlyph>,
}

impl FontCache {
    pub fn new() -> Self {
        Self {
            cache: FxHashMap::default(),
        }
    }

    #[inline]
    pub fn get(&self, key: &(char, Attributes)) -> Option<&ResolvedGlyph> {
        self.cache.get(key)
    }

    #[inline]
    pub fn insert(&mut self, key: (char, Attributes), value: ResolvedGlyph) {
        self.cache.insert(key, value);
    }

    pub fn clear(&mut self) {
        self.cache.clear();
    }

    /// Attach `advance` to the entry for `key`, if one exists. Used by
    /// `Sugarloaf::char_advance` after the first pixel-advance query
    /// for a `(char, attrs)` pair so subsequent queries can short-circuit.
    /// No-op when the entry hasn't been resolved yet (caller should
    /// call `resolve_with` first).
    #[inline]
    pub(crate) fn set_advance(&mut self, key: (char, Attributes), advance: AdvanceInfo) {
        if let Some(entry) = self.cache.get_mut(&key) {
            entry.advance = Some(advance);
        }
    }
}

/// Resolve a single glyph: read from `cache` if present, otherwise
/// walk the fallback chain via `font_lib` and store the result.
///
/// `font_lib.resolve_font_for_char` does the registered-font walk
/// first; on a miss it falls back to platform cascade discovery
/// (CoreText `CTFontCreateForString` on macOS, fontconfig
/// `FcFontSort`+`FcCharSet` on Linux, font-kit `all_fonts` walk on
/// Windows) and registers the discovered font under a new `font_id`.
/// Subsequent queries for any codepoint that the discovered font
/// covers then hit the registered-font fast path directly.
pub(crate) fn resolve_with(
    cache: &mut FontCache,
    font_lib: &crate::font::FontLibrary,
    ch: char,
    attrs: Attributes,
) -> ResolvedGlyph {
    if let Some(cached) = cache.get(&(ch, attrs)) {
        return *cached;
    }

    let style = SpanStyle {
        font_attrs: attrs,
        ..Default::default()
    };
    let mut width = ch.width().unwrap_or(1) as f32;

    // Cross-platform: `resolve_font_for_char` does the fast-path
    // registered-font walk first, then falls back to platform cascade
    // discovery (CoreText on macOS, fontconfig on Linux, font-kit walk
    // on Windows) and registers the discovered font on first miss so
    // subsequent codepoints in the same script hit the fast path.
    // No route context here — this cache is for layout/width measurement,
    // not pane rendering. PUA codepoints fall through to the regular
    // font lookup, which returns a sensible width.
    let (font_id, is_emoji) = font_lib.resolve_font_for_char(ch, &style, None);

    if is_emoji {
        width = 2.0;
    }

    let resolved = ResolvedGlyph {
        font_id,
        width,
        is_pua: is_private_user_area(&ch),
        advance: None,
    };
    cache.insert((ch, attrs), resolved);
    resolved
}

/// Compute the unscaled glyph advance for `ch` in the font registered
/// under `font_id`. Returns `None` when the font data isn't available
/// (font id unregistered or the SFNT bytes failed to parse); the
/// caller is responsible for picking a rendering fallback.
#[cfg(not(target_os = "macos"))]
pub(crate) fn compute_advance(
    font_ctx: &crate::font::FontLibraryData,
    font_id: usize,
    ch: char,
) -> Option<AdvanceInfo> {
    let (data, offset, _key) = font_ctx.get_data(&font_id)?;
    let font_ref = swash::FontRef::from_index(&data, offset as usize)?;
    let glyph_id = font_ref.charmap().map(ch as u32);
    let metrics = font_ref.glyph_metrics(&[]);
    Some(AdvanceInfo {
        advance_units: metrics.advance_width(glyph_id),
        units_per_em: font_ref.metrics(&[]).units_per_em,
    })
}

/// macOS variant: derive the advance from CoreText without ever touching
/// the font's raw bytes. bytes-free font handling on
/// mac.
#[cfg(target_os = "macos")]
pub(crate) fn compute_advance(
    font_ctx: &crate::font::FontLibraryData,
    font_id: usize,
    ch: char,
) -> Option<AdvanceInfo> {
    let font = font_ctx.try_get(&font_id)?;
    let handle = if let Some(path) = font.path() {
        crate::font::macos::FontHandle::from_path(path)
    } else if let Some(bytes) = font.data() {
        crate::font::macos::FontHandle::from_bytes(bytes.as_ref())
    } else {
        None
    }?;
    let (advance_units, units_per_em) =
        crate::font::macos::advance_units_for_char(&handle, ch)?;
    Some(AdvanceInfo {
        advance_units,
        units_per_em,
    })
}