diff options
Diffstat (limited to 'src/layout/shaping.rs')
| -rw-r--r-- | src/layout/shaping.rs | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/src/layout/shaping.rs b/src/layout/shaping.rs new file mode 100644 index 00000000..141fa10e --- /dev/null +++ b/src/layout/shaping.rs @@ -0,0 +1,198 @@ +//! Super-basic text shaping. +//! +//! This is really only suited for simple Latin text. It picks the most suitable +//! font for each individual character. When the direction is right-to-left, the +//! word is spelled backwards. Vertical shaping is not supported. + +use std::fmt::{self, Debug, Display, Formatter}; + +use fontdock::{FaceId, FaceQuery, FallbackTree, FontVariant}; +use ttf_parser::{Face, GlyphId}; + +use crate::env::FontLoader; +use crate::geom::{Dir, Length, Point, Size}; +use crate::layout::{Element, Frame}; + +/// A shaped run of text. +#[derive(Clone, PartialEq)] +pub struct Shaped { + /// The shaped text. + pub text: String, + /// The font face the text was shaped with. + pub face: FaceId, + /// The shaped glyphs. + pub glyphs: Vec<GlyphId>, + /// The horizontal offsets of the glyphs. This is indexed parallel to + /// `glyphs`. Vertical offsets are not yet supported. + pub offsets: Vec<Length>, + /// The font size. + pub font_size: Length, +} + +impl Shaped { + /// Create a new shape run with empty `text`, `glyphs` and `offsets`. + pub fn new(face: FaceId, font_size: Length) -> Self { + Self { + text: String::new(), + face, + glyphs: vec![], + offsets: vec![], + font_size, + } + } + + /// Encode the glyph ids into a big-endian byte buffer. + pub fn encode_glyphs_be(&self) -> Vec<u8> { + let mut bytes = Vec::with_capacity(2 * self.glyphs.len()); + for &GlyphId(g) in &self.glyphs { + bytes.push((g >> 8) as u8); + bytes.push((g & 0xff) as u8); + } + bytes + } +} + +impl Debug for Shaped { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "Shaped({})", self.text) + } +} + +/// Identifies a vertical metric of a font. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub enum VerticalFontMetric { + /// The distance from the baseline to the typographic ascender. + /// + /// Corresponds to the typographic ascender from the `OS/2` table if present + /// and falls back to the ascender from the `hhea` table otherwise. + Ascender, + /// The approximate height of uppercase letters. + CapHeight, + /// The approximate height of non-ascending lowercase letters. + XHeight, + /// The baseline on which the letters rest. + Baseline, + /// The distance from the baseline to the typographic descender. + /// + /// Corresponds to the typographic descender from the `OS/2` table if + /// present and falls back to the descender from the `hhea` table otherwise. + Descender, +} + +impl Display for VerticalFontMetric { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad(match self { + Self::Ascender => "ascender", + Self::CapHeight => "cap-height", + Self::XHeight => "x-height", + Self::Baseline => "baseline", + Self::Descender => "descender", + }) + } +} + +/// Shape text into a frame containing [`Shaped`] runs. +pub fn shape( + text: &str, + dir: Dir, + fallback: &FallbackTree, + variant: FontVariant, + font_size: Length, + top_edge: VerticalFontMetric, + bottom_edge: VerticalFontMetric, + loader: &mut FontLoader, +) -> Frame { + let mut frame = Frame::new(Size::new(Length::ZERO, Length::ZERO)); + let mut shaped = Shaped::new(FaceId::MAX, font_size); + let mut width = Length::ZERO; + let mut top = Length::ZERO; + let mut bottom = Length::ZERO; + + // Create an iterator with conditional direction. + let mut forwards = text.chars(); + let mut backwards = text.chars().rev(); + let chars: &mut dyn Iterator<Item = char> = if dir.is_positive() { + &mut forwards + } else { + &mut backwards + }; + + for c in chars { + let query = FaceQuery { fallback: fallback.iter(), variant, c }; + if let Some(id) = loader.query(query) { + let face = loader.face(id).get(); + let (glyph, glyph_width) = match lookup_glyph(face, c) { + Some(v) => v, + None => continue, + }; + + let units_per_em = f64::from(face.units_per_em().unwrap_or(1000)); + let convert = |units| units / units_per_em * font_size; + + // Flush the buffer and reset the metrics if we use a new font face. + if shaped.face != id { + place(&mut frame, shaped, width, top, bottom); + + shaped = Shaped::new(id, font_size); + width = Length::ZERO; + top = convert(f64::from(lookup_metric(face, top_edge))); + bottom = convert(f64::from(lookup_metric(face, bottom_edge))); + } + + shaped.text.push(c); + shaped.glyphs.push(glyph); + shaped.offsets.push(width); + width += convert(f64::from(glyph_width)); + } + } + + place(&mut frame, shaped, width, top, bottom); + + frame +} + +/// Place shaped text into a frame. +fn place(frame: &mut Frame, shaped: Shaped, width: Length, top: Length, bottom: Length) { + if !shaped.text.is_empty() { + frame.push(Point::new(frame.size.width, top), Element::Text(shaped)); + frame.size.width += width; + frame.size.height = frame.size.height.max(top - bottom); + } +} + +/// Look up the glyph for `c` and returns its index alongside its advance width. +fn lookup_glyph(face: &Face, c: char) -> Option<(GlyphId, u16)> { + let glyph = face.glyph_index(c)?; + let width = face.glyph_hor_advance(glyph)?; + Some((glyph, width)) +} + +/// Look up a vertical metric. +fn lookup_metric(face: &Face, metric: VerticalFontMetric) -> i16 { + match metric { + VerticalFontMetric::Ascender => lookup_ascender(face), + VerticalFontMetric::CapHeight => face + .capital_height() + .filter(|&h| h > 0) + .unwrap_or_else(|| lookup_ascender(face)), + VerticalFontMetric::XHeight => face + .x_height() + .filter(|&h| h > 0) + .unwrap_or_else(|| lookup_ascender(face)), + VerticalFontMetric::Baseline => 0, + VerticalFontMetric::Descender => lookup_descender(face), + } +} + +/// The ascender of the face. +fn lookup_ascender(face: &Face) -> i16 { + // We prefer the typographic ascender over the Windows ascender because + // it can be overly large if the font has large glyphs. + face.typographic_ascender().unwrap_or_else(|| face.ascender()) +} + +/// The descender of the face. +fn lookup_descender(face: &Face) -> i16 { + // See `lookup_ascender` for reason. + face.typographic_descender().unwrap_or_else(|| face.descender()) +} |
