summaryrefslogtreecommitdiff
path: root/src/layout/shaping.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/layout/shaping.rs')
-rw-r--r--src/layout/shaping.rs198
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())
+}