Less style chains lookups during paragraph layout

This commit is contained in:
Laurenz 2022-04-10 23:24:09 +02:00
parent 34fa8df044
commit 029b87b0a9
3 changed files with 118 additions and 121 deletions

View File

@ -197,7 +197,7 @@ pub struct Text {
impl Text { impl Text {
/// The width of the text run. /// The width of the text run.
pub fn width(&self) -> Length { pub fn width(&self) -> Length {
self.glyphs.iter().map(|g| g.x_advance.at(self.size)).sum() self.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size)
} }
} }

View File

@ -104,7 +104,7 @@ impl Layout for ParNode {
let lines = linebreak(&p, &mut ctx.fonts, regions.first.x); let lines = linebreak(&p, &mut ctx.fonts, regions.first.x);
// Stack the lines into one frame per region. // Stack the lines into one frame per region.
Ok(stack(&lines, &ctx.fonts, regions, styles)) Ok(stack(&lines, &mut ctx.fonts, regions, styles))
} }
} }
@ -300,10 +300,8 @@ struct Line<'a> {
/// and `last` aren't trimmed to the line, but it doesn't matter because /// and `last` aren't trimmed to the line, but it doesn't matter because
/// we're just checking which range an index falls into. /// we're just checking which range an index falls into.
ranges: &'a [Range], ranges: &'a [Range],
/// The size of the line. /// The width of the line.
size: Size, width: Length,
/// The baseline of the line.
baseline: Length,
/// The sum of fractions in the line. /// The sum of fractions in the line.
fr: Fraction, fr: Fraction,
/// Whether the line ends at a mandatory break. /// Whether the line ends at a mandatory break.
@ -457,7 +455,7 @@ fn linebreak_simple<'a>(
// If the line doesn't fit anymore, we push the last fitting attempt // If the line doesn't fit anymore, we push the last fitting attempt
// into the stack and rebuild the line from its end. The resulting // into the stack and rebuild the line from its end. The resulting
// line cannot be broken up further. // line cannot be broken up further.
if !width.fits(attempt.size.x) { if !width.fits(attempt.width) {
if let Some((last_attempt, last_end)) = last.take() { if let Some((last_attempt, last_end)) = last.take() {
lines.push(last_attempt); lines.push(last_attempt);
start = last_end; start = last_end;
@ -468,7 +466,7 @@ fn linebreak_simple<'a>(
// Finish the current line if there is a mandatory line break (i.e. // Finish the current line if there is a mandatory line break (i.e.
// due to "\n") or if the line doesn't fit horizontally already // due to "\n") or if the line doesn't fit horizontally already
// since then no shorter line will be possible. // since then no shorter line will be possible.
if mandatory || !width.fits(attempt.size.x) { if mandatory || !width.fits(attempt.width) {
lines.push(attempt); lines.push(attempt);
start = end; start = end;
last = None; last = None;
@ -547,7 +545,7 @@ fn linebreak_optimized<'a>(
// Determine how much the line's spaces would need to be stretched // Determine how much the line's spaces would need to be stretched
// to make it the desired width. // to make it the desired width.
let delta = width - attempt.size.x; let delta = width - attempt.width;
let mut ratio = delta / attempt.stretch(); let mut ratio = delta / attempt.stretch();
if ratio.is_infinite() { if ratio.is_infinite() {
ratio = delta / (em / 2.0); ratio = delta / (em / 2.0);
@ -796,8 +794,6 @@ fn line<'a>(
} }
let mut width = Length::zero(); let mut width = Length::zero();
let mut top = Length::zero();
let mut bottom = Length::zero();
let mut fr = Fraction::zero(); let mut fr = Fraction::zero();
// Measure the size of the line. // Measure the size of the line.
@ -805,16 +801,8 @@ fn line<'a>(
match item { match item {
ParItem::Absolute(v) => width += *v, ParItem::Absolute(v) => width += *v,
ParItem::Fractional(v) => fr += *v, ParItem::Fractional(v) => fr += *v,
ParItem::Text(shaped) => { ParItem::Text(shaped) => width += shaped.width,
width += shaped.size.x; ParItem::Frame(frame) => width += frame.size.x,
top.set_max(shaped.baseline);
bottom.set_max(shaped.size.y - shaped.baseline);
}
ParItem::Frame(frame) => {
width += frame.size.x;
top.set_max(frame.baseline());
bottom.set_max(frame.size.y - frame.baseline());
}
} }
} }
@ -825,8 +813,7 @@ fn line<'a>(
items, items,
last, last,
ranges: &p.ranges[first_idx ..= last_idx], ranges: &p.ranges[first_idx ..= last_idx],
size: Size::new(width, top + bottom), width,
baseline: top,
fr, fr,
mandatory, mandatory,
dash, dash,
@ -836,7 +823,7 @@ fn line<'a>(
/// Combine layouted lines into one frame per region. /// Combine layouted lines into one frame per region.
fn stack( fn stack(
lines: &[Line], lines: &[Line],
fonts: &FontStore, fonts: &mut FontStore,
regions: &Regions, regions: &Regions,
styles: StyleChain, styles: StyleChain,
) -> Vec<Arc<Frame>> { ) -> Vec<Arc<Frame>> {
@ -848,7 +835,7 @@ fn stack(
// should expand or there's fractional spacing, fit-to-width otherwise. // should expand or there's fractional spacing, fit-to-width otherwise.
let mut width = regions.first.x; let mut width = regions.first.x;
if !regions.expand.x && lines.iter().all(|line| line.fr.is_zero()) { if !regions.expand.x && lines.iter().all(|line| line.fr.is_zero()) {
width = lines.iter().map(|line| line.size.x).max().unwrap_or_default(); width = lines.iter().map(|line| line.width).max().unwrap_or_default();
} }
// State for final frame building. // State for final frame building.
@ -859,7 +846,10 @@ fn stack(
// Stack the lines into one frame per region. // Stack the lines into one frame per region.
for line in lines { for line in lines {
while !regions.first.y.fits(line.size.y) && !regions.in_last() { let frame = commit(line, fonts, width, align, justify);
let height = frame.size.y;
while !regions.first.y.fits(height) && !regions.in_last() {
finished.push(Arc::new(output)); finished.push(Arc::new(output));
output = Frame::new(Size::with_x(width)); output = Frame::new(Size::with_x(width));
regions.next(); regions.next();
@ -870,12 +860,11 @@ fn stack(
output.size.y += leading; output.size.y += leading;
} }
let frame = commit(line, fonts, width, align, justify);
let pos = Point::with_y(output.size.y); let pos = Point::with_y(output.size.y);
output.size.y += frame.size.y; output.size.y += height;
output.merge_frame(pos, frame); output.merge_frame(pos, frame);
regions.first.y -= line.size.y + leading; regions.first.y -= height + leading;
first = false; first = false;
} }
@ -886,13 +875,12 @@ fn stack(
/// Commit to a line and build its frame. /// Commit to a line and build its frame.
fn commit( fn commit(
line: &Line, line: &Line,
fonts: &FontStore, fonts: &mut FontStore,
width: Length, width: Length,
align: Align, align: Align,
justify: bool, justify: bool,
) -> Frame { ) -> Frame {
let size = Size::new(width, line.size.y); let mut remaining = width - line.width;
let mut remaining = width - line.size.x;
let mut offset = Length::zero(); let mut offset = Length::zero();
// Reorder the line from logical to visual order. // Reorder the line from logical to visual order.
@ -903,8 +891,7 @@ fn commit(
if let Some(glyph) = text.glyphs.first() { if let Some(glyph) = text.glyphs.first() {
if text.styles.get(TextNode::OVERHANG) { if text.styles.get(TextNode::OVERHANG) {
let start = text.dir.is_positive(); let start = text.dir.is_positive();
let em = text.styles.get(TextNode::SIZE); let amount = overhang(glyph.c, start) * glyph.x_advance.at(text.size);
let amount = overhang(glyph.c, start) * glyph.x_advance.at(em);
offset -= amount; offset -= amount;
remaining += amount; remaining += amount;
} }
@ -918,8 +905,7 @@ fn commit(
&& (reordered.len() > 1 || text.glyphs.len() > 1) && (reordered.len() > 1 || text.glyphs.len() > 1)
{ {
let start = !text.dir.is_positive(); let start = !text.dir.is_positive();
let em = text.styles.get(TextNode::SIZE); let amount = overhang(glyph.c, start) * glyph.x_advance.at(text.size);
let amount = overhang(glyph.c, start) * glyph.x_advance.at(em);
remaining += amount; remaining += amount;
} }
} }
@ -940,24 +926,41 @@ fn commit(
} }
} }
let mut output = Frame::new(size); let mut top = Length::zero();
output.baseline = Some(line.baseline); let mut bottom = Length::zero();
// Construct the line's frame from left to right. // Build the frames and determine the height and baseline.
let mut frames = vec![];
for item in reordered { for item in reordered {
let mut position = |frame: Frame| { let frame = match item {
let x = offset + align.position(remaining); ParItem::Absolute(v) => {
let y = line.baseline - frame.baseline(); offset += *v;
offset += frame.size.x; continue;
output.merge_frame(Point::new(x, y), frame); }
ParItem::Fractional(v) => {
offset += v.share(line.fr, remaining);
continue;
}
ParItem::Text(shaped) => shaped.build(fonts, justification),
ParItem::Frame(frame) => frame.clone(),
}; };
match item { let width = frame.size.x;
ParItem::Absolute(v) => offset += *v, top.set_max(frame.baseline());
ParItem::Fractional(v) => offset += v.share(line.fr, remaining), bottom.set_max(frame.size.y - frame.baseline());
ParItem::Text(shaped) => position(shaped.build(fonts, justification)), frames.push((offset, frame));
ParItem::Frame(frame) => position(frame.clone()), offset += width;
} }
let size = Size::new(width, top + bottom);
let mut output = Frame::new(size);
output.baseline = Some(top);
// Construct the line's frame.
for (offset, frame) in frames {
let x = offset + align.position(remaining);
let y = top - frame.baseline();
output.merge_frame(Point::new(x, y), frame);
} }
output output

View File

@ -20,10 +20,12 @@ pub struct ShapedText<'a> {
pub dir: Dir, pub dir: Dir,
/// The text's style properties. /// The text's style properties.
pub styles: StyleChain<'a>, pub styles: StyleChain<'a>,
/// The size of the text's bounding box. /// The font variant.
pub size: Size, pub variant: FontVariant,
/// The baseline from the top of the frame. /// The font size.
pub baseline: Length, pub size: Length,
/// The width of the text's bounding box.
pub width: Length,
/// The shaped glyphs. /// The shaped glyphs.
pub glyphs: Cow<'a, [ShapedGlyph]>, pub glyphs: Cow<'a, [ShapedGlyph]>,
} }
@ -74,15 +76,17 @@ impl<'a> ShapedText<'a> {
/// ///
/// The `justification` defines how much extra advance width each /// The `justification` defines how much extra advance width each
/// [justifiable glyph](ShapedGlyph::is_justifiable) will get. /// [justifiable glyph](ShapedGlyph::is_justifiable) will get.
pub fn build(&self, fonts: &FontStore, justification: Length) -> Frame { pub fn build(&self, fonts: &mut FontStore, justification: Length) -> Frame {
let (top, bottom) = self.measure(fonts);
let size = Size::new(self.width, top + bottom);
let mut offset = Length::zero(); let mut offset = Length::zero();
let mut frame = Frame::new(self.size); let mut frame = Frame::new(size);
frame.baseline = Some(self.baseline); frame.baseline = Some(top);
for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) { for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
let pos = Point::new(offset, self.baseline); let pos = Point::new(offset, top);
let size = self.styles.get(TextNode::SIZE);
let fill = self.styles.get(TextNode::FILL); let fill = self.styles.get(TextNode::FILL);
let glyphs = group let glyphs = group
.iter() .iter()
@ -91,7 +95,7 @@ impl<'a> ShapedText<'a> {
x_advance: glyph.x_advance x_advance: glyph.x_advance
+ if glyph.is_justifiable() { + if glyph.is_justifiable() {
frame.size.x += justification; frame.size.x += justification;
Em::from_length(justification, size) Em::from_length(justification, self.size)
} else { } else {
Em::zero() Em::zero()
}, },
@ -99,7 +103,7 @@ impl<'a> ShapedText<'a> {
}) })
.collect(); .collect();
let text = Text { face_id, size, fill, glyphs }; let text = Text { face_id, size: self.size, fill, glyphs };
let text_layer = frame.layer(); let text_layer = frame.layer();
let width = text.width(); let width = text.width();
@ -120,6 +124,40 @@ impl<'a> ShapedText<'a> {
frame frame
} }
/// Measure the top and bottom extent of a this text.
fn measure(&self, fonts: &mut FontStore) -> (Length, Length) {
let mut top = Length::zero();
let mut bottom = Length::zero();
let top_edge = self.styles.get(TextNode::TOP_EDGE);
let bottom_edge = self.styles.get(TextNode::BOTTOM_EDGE);
// Expand top and bottom by reading the face's vertical metrics.
let mut expand = |face: &Face| {
let metrics = face.metrics();
top.set_max(top_edge.resolve(self.styles, metrics));
bottom.set_max(-bottom_edge.resolve(self.styles, metrics));
};
if self.glyphs.is_empty() {
// When there are no glyphs, we just use the vertical metrics of the
// first available font.
for family in families(self.styles) {
if let Some(face_id) = fonts.select(family, self.variant) {
expand(fonts.get(face_id));
break;
}
}
} else {
for (face_id, _) in self.glyphs.group_by_key(|g| g.face_id) {
let face = fonts.get(face_id);
expand(face);
}
}
(top, bottom)
}
/// How many justifiable glyphs the text contains. /// How many justifiable glyphs the text contains.
pub fn justifiables(&self) -> usize { pub fn justifiables(&self) -> usize {
self.glyphs.iter().filter(|g| g.is_justifiable()).count() self.glyphs.iter().filter(|g| g.is_justifiable()).count()
@ -132,7 +170,7 @@ impl<'a> ShapedText<'a> {
.filter(|g| g.is_justifiable()) .filter(|g| g.is_justifiable())
.map(|g| g.x_advance) .map(|g| g.x_advance)
.sum::<Em>() .sum::<Em>()
.at(self.styles.get(TextNode::SIZE)) .at(self.size)
} }
/// Reshape a range of the shaped text, reusing information from this /// Reshape a range of the shaped text, reusing information from this
@ -143,13 +181,13 @@ impl<'a> ShapedText<'a> {
text_range: Range<usize>, text_range: Range<usize>,
) -> ShapedText<'a> { ) -> ShapedText<'a> {
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
let (size, baseline) = measure(fonts, &glyphs, self.styles);
Self { Self {
text: Cow::Borrowed(&self.text[text_range]), text: Cow::Borrowed(&self.text[text_range]),
dir: self.dir, dir: self.dir,
styles: self.styles, styles: self.styles,
size, size: self.size,
baseline, variant: self.variant,
width: glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size),
glyphs: Cow::Borrowed(glyphs), glyphs: Cow::Borrowed(glyphs),
} }
} else { } else {
@ -159,16 +197,14 @@ impl<'a> ShapedText<'a> {
/// Push a hyphen to end of the text. /// Push a hyphen to end of the text.
pub fn push_hyphen(&mut self, fonts: &mut FontStore) { pub fn push_hyphen(&mut self, fonts: &mut FontStore) {
let size = self.styles.get(TextNode::SIZE);
let variant = variant(self.styles);
families(self.styles).find_map(|family| { families(self.styles).find_map(|family| {
let face_id = fonts.select(family, variant)?; let face_id = fonts.select(family, self.variant)?;
let face = fonts.get(face_id); let face = fonts.get(face_id);
let ttf = face.ttf(); let ttf = face.ttf();
let glyph_id = ttf.glyph_index('-')?; let glyph_id = ttf.glyph_index('-')?;
let x_advance = face.to_em(ttf.glyph_hor_advance(glyph_id)?); let x_advance = face.to_em(ttf.glyph_hor_advance(glyph_id)?);
let cluster = self.glyphs.last().map(|g| g.cluster).unwrap_or_default(); let cluster = self.glyphs.last().map(|g| g.cluster).unwrap_or_default();
self.size.x += x_advance.at(size); self.width += x_advance.at(self.size);
self.glyphs.to_mut().push(ShapedGlyph { self.glyphs.to_mut().push(ShapedGlyph {
face_id, face_id,
glyph_id: glyph_id.0, glyph_id: glyph_id.0,
@ -247,6 +283,7 @@ struct ShapingContext<'a> {
glyphs: Vec<ShapedGlyph>, glyphs: Vec<ShapedGlyph>,
used: Vec<FaceId>, used: Vec<FaceId>,
styles: StyleChain<'a>, styles: StyleChain<'a>,
size: Length,
variant: FontVariant, variant: FontVariant,
tags: Vec<rustybuzz::Feature>, tags: Vec<rustybuzz::Feature>,
fallback: bool, fallback: bool,
@ -260,6 +297,7 @@ pub fn shape<'a>(
styles: StyleChain<'a>, styles: StyleChain<'a>,
dir: Dir, dir: Dir,
) -> ShapedText<'a> { ) -> ShapedText<'a> {
let size = styles.get(TextNode::SIZE);
let text = match styles.get(TextNode::CASE) { let text = match styles.get(TextNode::CASE) {
Some(case) => Cow::Owned(case.apply(text)), Some(case) => Cow::Owned(case.apply(text)),
None => Cow::Borrowed(text), None => Cow::Borrowed(text),
@ -267,6 +305,7 @@ pub fn shape<'a>(
let mut ctx = ShapingContext { let mut ctx = ShapingContext {
fonts, fonts,
size,
glyphs: vec![], glyphs: vec![],
used: vec![], used: vec![],
styles, styles,
@ -282,14 +321,13 @@ pub fn shape<'a>(
track_and_space(&mut ctx); track_and_space(&mut ctx);
let (size, baseline) = measure(ctx.fonts, &ctx.glyphs, styles);
ShapedText { ShapedText {
text, text,
dir, dir,
styles, styles,
variant: ctx.variant,
size, size,
baseline, width: ctx.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(size),
glyphs: Cow::Owned(ctx.glyphs), glyphs: Cow::Owned(ctx.glyphs),
} }
} }
@ -443,9 +481,11 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, face_id: FaceI
/// Apply tracking and spacing to a slice of shaped glyphs. /// Apply tracking and spacing to a slice of shaped glyphs.
fn track_and_space(ctx: &mut ShapingContext) { fn track_and_space(ctx: &mut ShapingContext) {
let em = ctx.styles.get(TextNode::SIZE); let tracking = Em::from_length(ctx.styles.get(TextNode::TRACKING), ctx.size);
let tracking = Em::from_length(ctx.styles.get(TextNode::TRACKING), em); let spacing = ctx
let spacing = ctx.styles.get(TextNode::SPACING).map(|abs| Em::from_length(abs, em)); .styles
.get(TextNode::SPACING)
.map(|abs| Em::from_length(abs, ctx.size));
if tracking.is_zero() && spacing.is_one() { if tracking.is_zero() && spacing.is_one() {
return; return;
@ -463,52 +503,6 @@ fn track_and_space(ctx: &mut ShapingContext) {
} }
} }
/// Measure the size and baseline of a run of shaped glyphs with the given
/// properties.
fn measure(
fonts: &mut FontStore,
glyphs: &[ShapedGlyph],
styles: StyleChain,
) -> (Size, Length) {
let mut width = Length::zero();
let mut top = Length::zero();
let mut bottom = Length::zero();
let size = styles.get(TextNode::SIZE);
let top_edge = styles.get(TextNode::TOP_EDGE);
let bottom_edge = styles.get(TextNode::BOTTOM_EDGE);
// Expand top and bottom by reading the face's vertical metrics.
let mut expand = |face: &Face| {
let metrics = face.metrics();
top.set_max(top_edge.resolve(styles, metrics));
bottom.set_max(-bottom_edge.resolve(styles, metrics));
};
if glyphs.is_empty() {
// When there are no glyphs, we just use the vertical metrics of the
// first available font.
let variant = variant(styles);
for family in families(styles) {
if let Some(face_id) = fonts.select(family, variant) {
expand(fonts.get(face_id));
break;
}
}
} else {
for (face_id, group) in glyphs.group_by_key(|g| g.face_id) {
let face = fonts.get(face_id);
expand(face);
for glyph in group {
width += glyph.x_advance.at(size);
}
}
}
(Size::new(width, top + bottom), top)
}
/// Resolve the font variant with `STRONG` and `EMPH` factored in. /// Resolve the font variant with `STRONG` and `EMPH` factored in.
fn variant(styles: StyleChain) -> FontVariant { fn variant(styles: StyleChain) -> FontVariant {
let mut variant = FontVariant::new( let mut variant = FontVariant::new(