diff options
| author | Laurenz <laurmaedje@gmail.com> | 2021-04-05 22:32:09 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2021-04-05 22:32:09 +0200 |
| commit | de20a21a584a90da682a64e9a79cd18a95195b70 (patch) | |
| tree | 2c7ade7d1078111ebbce889a0fca8cee388d6135 /src/layout/shaping.rs | |
| parent | a86cf7bd8c58b0ec26f8f53eb90f0dc6c95a1070 (diff) | |
Reshaping with unsafe-to-break ⚡
Co-Authored-By: Martin <mhaug@live.de>
Diffstat (limited to 'src/layout/shaping.rs')
| -rw-r--r-- | src/layout/shaping.rs | 363 |
1 files changed, 250 insertions, 113 deletions
diff --git a/src/layout/shaping.rs b/src/layout/shaping.rs index 7ead0dff..b062f602 100644 --- a/src/layout/shaping.rs +++ b/src/layout/shaping.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt::{self, Debug, Formatter}; use std::ops::Range; @@ -5,86 +6,179 @@ use fontdock::FaceId; use rustybuzz::UnicodeBuffer; use ttf_parser::GlyphId; -use super::{Element, Frame, ShapedText}; +use super::{Element, Frame, Glyph, Text}; use crate::env::FontLoader; use crate::exec::FontProps; +use crate::font::FaceBuf; use crate::geom::{Dir, Length, Point, Size}; +use crate::util::SliceExt; -/// Shape text into a frame containing [`ShapedText`] runs. -pub fn shape<'a>( - text: &'a str, - dir: Dir, - loader: &mut FontLoader, - props: &'a FontProps, -) -> ShapeResult<'a> { - let iter = props.families.iter(); - let mut results = vec![]; - shape_segment(&mut results, text, dir, loader, props, iter, None); +/// The result of shaping text. +/// +/// This type contains owned or borrowed shaped text runs, which can be +/// measured, used to reshape substrings more quickly and converted into a +/// frame. +pub struct ShapedText<'a> { + /// The text that was shaped. + pub text: &'a str, + /// The text direction. + pub dir: Dir, + /// The properties used for font selection. + pub props: &'a FontProps, + /// The font size. + pub size: Size, + /// The baseline from the top of the frame. + pub baseline: Length, + /// The shaped glyphs. + pub glyphs: Cow<'a, [ShapedGlyph]>, +} - let mut top = Length::ZERO; - let mut bottom = Length::ZERO; - for result in &results { - top = top.max(result.top); - bottom = bottom.max(result.bottom); - } +/// A single glyph resulting from shaping. +#[derive(Debug, Copy, Clone)] +pub struct ShapedGlyph { + /// The font face the glyph is contained in. + pub face_id: FaceId, + /// The glyph's ID in the face. + pub id: GlyphId, + /// The advance width of the glyph. + pub x_advance: i32, + /// The horizontal offset of the glyph. + pub x_offset: i32, + /// The start index of the glyph in the source text. + pub text_index: usize, + /// Whether splitting the shaping result before this glyph would yield the + /// same results as shaping the parts to both sides of `text_index` + /// separately. + pub safe_to_break: bool, +} - let mut frame = Frame::new(Size::new(Length::ZERO, top + bottom), top); +impl<'a> ShapedText<'a> { + /// Build the shaped text's frame. + pub fn build(&self, loader: &mut FontLoader) -> Frame { + let mut frame = Frame::new(self.size, self.baseline); + let mut x = Length::ZERO; - for shaped in results { - let offset = frame.size.width; - frame.size.width += shaped.width; + for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) { + let face = loader.face(face_id); - if !shaped.glyphs.is_empty() { - frame.push(Point::new(offset, top), Element::Text(shaped)); - } - } + let pos = Point::new(x, self.baseline); + let mut text = Text { + face_id, + size: self.props.size, + color: self.props.color, + glyphs: vec![], + }; - ShapeResult { frame, text, dir, props } -} + for glyph in group { + let x_advance = face.convert(self.props.size, glyph.x_advance); + let x_offset = face.convert(self.props.size, glyph.x_offset); + text.glyphs.push(Glyph { id: glyph.id, x_advance, x_offset }); + x += x_advance; + } -#[derive(Clone)] -pub struct ShapeResult<'a> { - frame: Frame, - text: &'a str, - dir: Dir, - props: &'a FontProps, -} + frame.push(pos, Element::Text(text)); + } -impl<'a> ShapeResult<'a> { + frame + } + + /// Reshape a range of the shaped text, reusing information from this + /// shaping process if possible. pub fn reshape( - &self, - range: Range<usize>, + &'a self, + text_range: Range<usize>, loader: &mut FontLoader, - ) -> ShapeResult<'_> { - if range.start == 0 && range.end == self.text.len() { - self.clone() + ) -> ShapedText<'a> { + if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { + let (size, baseline) = measure(glyphs, loader, self.props); + Self { + text: &self.text[text_range], + dir: self.dir, + props: self.props, + size, + baseline, + glyphs: Cow::Borrowed(glyphs), + } } else { - shape(&self.text[range], self.dir, loader, self.props) + shape(&self.text[text_range], self.dir, loader, self.props) } } - pub fn text(&self) -> &'a str { - self.text - } + /// Find the subslice of glyphs that represent the given text range if both + /// sides are safe to break. + fn slice_safe_to_break(&self, text_range: Range<usize>) -> Option<&[ShapedGlyph]> { + let mut start = self.find_safe_to_break(text_range.start)?; + let mut end = self.find_safe_to_break(text_range.end)?; + + if !self.dir.is_positive() { + std::mem::swap(&mut start, &mut end); + } + + // TODO: Expand to left and right if necessary because + // find_safe_to_break may find any glyph with the text_index. - pub fn measure(&self) -> (Size, Length) { - (self.frame.size, self.frame.baseline) + Some(&self.glyphs[start .. end]) } - pub fn build(&self) -> Frame { - self.frame.clone() + /// Find the glyph slice offset at the text index if it's safe to break. + fn find_safe_to_break(&self, text_index: usize) -> Option<usize> { + let ltr = self.dir.is_positive(); + + // Handle edge cases. + let len = self.glyphs.len(); + if text_index == 0 { + return Some(if ltr { 0 } else { len }); + } else if text_index == self.text.len() { + return Some(if ltr { len } else { 0 }); + } + + // TODO: Do binary search. Take care that RTL needs reversed ordering. + let idx = self + .glyphs + .iter() + .position(|g| g.text_index == text_index) + .filter(|&i| self.glyphs[i].safe_to_break)?; + + // RTL needs offset one because the the start of the range should + // be exclusive and the end inclusive. + Some(if ltr { idx } else { idx + 1 }) } } -impl Debug for ShapeResult<'_> { +impl Debug for ShapedText<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "Shaped({:?})", self.text) } } -/// Shape text into a frame with font fallback using the `families` iterator. +/// Shape text into [`ShapedText`]. +pub fn shape<'a>( + text: &'a str, + dir: Dir, + loader: &mut FontLoader, + props: &'a FontProps, +) -> ShapedText<'a> { + let mut glyphs = vec![]; + let families = props.families.iter(); + if !text.is_empty() { + shape_segment(&mut glyphs, 0, text, dir, loader, props, families, None); + } + + let (size, baseline) = measure(&glyphs, loader, props); + ShapedText { + text, + dir, + props, + size, + baseline, + glyphs: Cow::Owned(glyphs), + } +} + +/// Shape text with font fallback using the `families` iterator. fn shape_segment<'a>( - results: &mut Vec<ShapedText>, + glyphs: &mut Vec<ShapedGlyph>, + base: usize, text: &str, dir: Dir, loader: &mut FontLoader, @@ -93,7 +187,7 @@ fn shape_segment<'a>( mut first: Option<FaceId>, ) { // Select the font family. - let (id, fallback) = loop { + let (face_id, fallback) = loop { // Try to load the next available font family. match families.next() { Some(family) => match loader.query(family, props.variant) { @@ -111,22 +205,7 @@ fn shape_segment<'a>( // Register that this is the first available font. if first.is_none() { - first = Some(id); - } - - // Find out some metrics and prepare the shaped text container. - let face = loader.face(id); - 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); - - // For empty text, we want a zero-width box with the correct height. - if text.is_empty() { - results.push(shaped); - return; + first = Some(face_id); } // Fill the buffer with our text. @@ -139,59 +218,117 @@ fn shape_segment<'a>( }); // 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() { - results.push(shaped); - shaped = ShapedText::new(id, props.size, top, bottom, props.color); - } + let buffer = rustybuzz::shape(loader.face(face_id).buzz(), &[], buffer); + let infos = buffer.glyph_infos(); + let pos = buffer.glyph_positions(); - // 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; - } - end = info.cluster as usize; - iter.next(); - } + // Collect the shaped glyphs, reshaping with the next font if necessary. + let mut i = 0; + while i < infos.len() { + let info = &infos[i]; + let cluster = info.cluster as usize; - // Because Harfbuzz outputs glyphs in visual order, the start - // cluster actually corresponds to the last codepoint in - // right-to-left text. - if !dir.is_positive() { - assert!(end <= start); - std::mem::swap(&mut start, &mut end); + if info.codepoint != 0 || !fallback { + // Add the glyph to the shaped output. + // TODO: Don't ignore y_advance and y_offset. + glyphs.push(ShapedGlyph { + face_id, + id: GlyphId(info.codepoint as u16), + x_advance: pos[i].x_advance, + x_offset: pos[i].x_offset, + text_index: base + cluster, + safe_to_break: !info.unsafe_to_break(), + }); + } else { + // Do font fallback if the glyph is a tofu. + // + // First, search for the end of the tofu sequence. + let k = i; + while infos.get(i + 1).map_or(false, |info| info.codepoint == 0) { + i += 1; } - // 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; - let part = &text[range]; + // Determine the source text range for the tofu sequence. + let range = { + // Examples + // + // Here, _ is a tofu. + // Note that the glyph cluster length is greater than 1 char! + // + // Left-to-right clusters: + // h a l i h a l l o + // A _ _ C E + // 0 2 4 6 8 + // + // Right-to-left clusters: + // O L L A H I L A H + // E C _ _ A + // 8 6 4 2 0 + + let ltr = dir.is_positive(); + let first = if ltr { k } else { i }; + let start = infos[first].cluster as usize; + + let last = if ltr { i.checked_add(1) } else { k.checked_sub(1) }; + let end = last + .and_then(|last| infos.get(last)) + .map(|info| info.cluster as usize) + .unwrap_or(text.len()); + + start .. end + }; // Recursively shape the tofu sequence with the next family. - shape_segment(results, part, dir, loader, props, families.clone(), first); - } 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); + shape_segment( + glyphs, + base + range.start, + &text[range], + dir, + loader, + props, + families.clone(), + first, + ); } + + i += 1; } +} + +/// Measure the size and baseline of a run of shaped glyphs with the given +/// properties. +fn measure( + glyphs: &[ShapedGlyph], + loader: &mut FontLoader, + props: &FontProps, +) -> (Size, Length) { + let mut top = Length::ZERO; + let mut bottom = Length::ZERO; + let mut width = Length::ZERO; + let mut vertical = |face: &FaceBuf| { + top = top.max(face.vertical_metric(props.size, props.top_edge)); + bottom = bottom.max(-face.vertical_metric(props.size, props.bottom_edge)); + }; + + if glyphs.is_empty() { + // When there are no glyphs, we just use the vertical metrics of the + // first available font. + for family in props.families.iter() { + if let Some(face_id) = loader.query(family, props.variant) { + vertical(loader.face(face_id)); + break; + } + } + } else { + for (face_id, group) in glyphs.group_by_key(|g| g.face_id) { + let face = loader.face(face_id); + vertical(face); - if !shaped.glyphs.is_empty() { - results.push(shaped); + for glyph in group { + width += face.convert(props.size, glyph.x_advance); + } + } } + + (Size::new(width, top + bottom), top) } |
