summaryrefslogtreecommitdiff
path: root/src/layout/shaping.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-03-24 17:12:34 +0100
committerLaurenz <laurmaedje@gmail.com>2021-03-24 17:12:34 +0100
commit73615f7e3ce23f2ea656d04ea9f96184f5ebdc0a (patch)
tree7691b792e1e4b33469a72c40fc76854f1de0814e /src/layout/shaping.rs
parent6720520ec06dd0718f81049b2b11e81664f7ef62 (diff)
Text shaping 🚀
- Shapes text with rustybuzz - Font fallback with family list - Tofus are shown in the first font Co-Authored-By: Martin <mhaug@live.de>
Diffstat (limited to 'src/layout/shaping.rs')
-rw-r--r--src/layout/shaping.rs292
1 files changed, 108 insertions, 184 deletions
diff --git a/src/layout/shaping.rs b/src/layout/shaping.rs
index fd10b41e..f7eece92 100644
--- a/src/layout/shaping.rs
+++ b/src/layout/shaping.rs
@@ -1,205 +1,129 @@
-//! 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, FontVariant};
-use ttf_parser::{Face, GlyphId};
+use fontdock::FaceId;
+use rustybuzz::UnicodeBuffer;
+use ttf_parser::GlyphId;
+use super::{Element, Frame, ShapedText};
use crate::env::FontLoader;
-use crate::exec::FamilyMap;
-use crate::geom::{Dir, Length, Point, Size};
-use crate::layout::{Element, Fill, 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,
- /// The glyph fill color / texture.
- pub color: Fill,
-}
-
-impl Shaped {
- /// Create a new shape run with empty `text`, `glyphs` and `offsets`.
- pub fn new(face: FaceId, font_size: Length, color: Fill) -> Self {
- Self {
- text: String::new(),
- face,
- glyphs: vec![],
- offsets: vec![],
- font_size,
- color,
- }
- }
-
- /// 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 {
- Debug::fmt(&self.text, f)
- }
-}
-
-/// 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,
-}
+use crate::exec::FontProps;
+use crate::geom::{Length, Point, Size};
-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 [`ShapedText`] runs.
+pub fn shape(text: &str, loader: &mut FontLoader, props: &FontProps) -> Frame {
+ let mut frame = Frame::new(Size::new(Length::ZERO, Length::ZERO));
+ shape_segment(&mut frame, text, props.families.iter(), None, loader, props);
+ frame
}
-/// Shape text into a frame containing [`Shaped`] runs.
-pub fn shape(
+/// Shape text into a frame with font fallback using the `families` iterator.
+fn shape_segment<'a>(
+ frame: &mut Frame,
text: &str,
- dir: Dir,
- families: &FamilyMap,
- variant: FontVariant,
- font_size: Length,
- top_edge: VerticalFontMetric,
- bottom_edge: VerticalFontMetric,
- color: Fill,
+ mut families: impl Iterator<Item = &'a str> + Clone,
+ mut first: Option<FaceId>,
loader: &mut FontLoader,
-) -> Frame {
- let mut frame = Frame::new(Size::new(Length::ZERO, Length::ZERO));
- let mut shaped = Shaped::new(FaceId::MAX, font_size, color);
- 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
+ props: &FontProps,
+) {
+ // Select the font family.
+ let (id, fallback) = loop {
+ // Try to load the next available font family.
+ match families.next() {
+ Some(family) => match loader.query(family, props.variant) {
+ Some(id) => break (id, true),
+ None => {}
+ },
+ // We're out of families, so we don't do any more fallback and just
+ // shape the tofus with the first face we originally used.
+ None => match first {
+ Some(id) => break (id, false),
+ None => return,
+ },
+ }
};
- for c in chars {
- for family in families.iter() {
- if let Some(id) = loader.query(family, variant) {
- 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;
+ // Register that this is the first available font.
+ let face = loader.face(id);
+ if first.is_none() {
+ first = Some(id);
+ }
- // 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);
+ // Find out some metrics and prepare the shaped text container.
+ let ttf = face.ttf();
+ let units_per_em = f64::from(ttf.units_per_em().unwrap_or(1000));
+ let convert = |units| f64::from(units) / units_per_em * props.size;
+ let top = convert(i32::from(props.top_edge.lookup(ttf)));
+ let bottom = convert(i32::from(props.bottom_edge.lookup(ttf)));
+ let mut shaped = ShapedText::new(id, props.size, top, bottom, props.color);
+
+ // Fill the buffer with our text.
+ let mut buffer = UnicodeBuffer::new();
+ buffer.push_str(text);
+ buffer.guess_segment_properties();
+
+ // Find out the text direction.
+ // TODO: Replace this once we do BiDi.
+ let rtl = matches!(buffer.direction(), rustybuzz::Direction::RightToLeft);
+
+ // Shape!
+ let glyphs = rustybuzz::shape(face.buzz(), &[], buffer);
+ let info = glyphs.glyph_infos();
+ let pos = glyphs.glyph_positions();
+ let mut iter = info.iter().zip(pos).peekable();
+
+ while let Some((info, pos)) = iter.next() {
+ // Do font fallback if the glyph is a tofu.
+ if info.codepoint == 0 && fallback {
+ // Flush what we have so far.
+ if !shaped.glyphs.is_empty() {
+ place(frame, shaped);
+ shaped = ShapedText::new(id, props.size, top, bottom, props.color);
+ }
- shaped = Shaped::new(id, font_size, color);
- width = Length::ZERO;
- top = convert(f64::from(lookup_metric(face, top_edge)));
- bottom = convert(f64::from(lookup_metric(face, bottom_edge)));
+ // Determine the start and end cluster index of the tofu sequence.
+ let mut start = info.cluster as usize;
+ let mut end = info.cluster as usize;
+ while let Some((info, _)) = iter.peek() {
+ if info.codepoint != 0 {
+ break;
}
-
- shaped.text.push(c);
- shaped.glyphs.push(glyph);
- shaped.offsets.push(width);
- width += convert(f64::from(glyph_width));
- break;
+ end = info.cluster as usize;
+ iter.next();
}
- }
- }
- place(&mut frame, shaped, width, top, bottom);
-
- frame
-}
+ // Because Harfbuzz outputs glyphs in visual order, the start
+ // cluster actually corresponds to the last codepoint in
+ // right-to-left text.
+ if rtl {
+ assert!(end <= start);
+ std::mem::swap(&mut start, &mut end);
+ }
-/// 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);
+ // The end cluster index points right before the last character that
+ // mapped to the tofu sequence. So we have to offset the end by one
+ // char.
+ let offset = text[end ..].chars().next().unwrap().len_utf8();
+ let range = start .. end + offset;
+
+ // Recursively shape the tofu sequence with the next family.
+ shape_segment(frame, &text[range], families.clone(), first, loader, props);
+ } else {
+ // Add the glyph to the shaped output.
+ // TODO: Don't ignore y_advance and y_offset.
+ let glyph = GlyphId(info.codepoint as u16);
+ shaped.glyphs.push(glyph);
+ shaped.offsets.push(shaped.width + convert(pos.x_offset));
+ shaped.width += convert(pos.x_advance);
+ }
}
-}
-/// 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),
+ if !shaped.glyphs.is_empty() {
+ place(frame, shaped)
}
}
-/// 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())
+/// Place shaped text into a frame.
+fn place(frame: &mut Frame, shaped: ShapedText) {
+ let offset = frame.size.width;
+ frame.size.width += shaped.width;
+ frame.size.height = frame.size.height.max(shaped.top - shaped.bottom);
+ frame.push(Point::new(offset, shaped.top), Element::Text(shaped));
}