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 {
/// The width of the text run.
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);
// 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
/// we're just checking which range an index falls into.
ranges: &'a [Range],
/// The size of the line.
size: Size,
/// The baseline of the line.
baseline: Length,
/// The width of the line.
width: Length,
/// The sum of fractions in the line.
fr: Fraction,
/// 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
// into the stack and rebuild the line from its end. The resulting
// 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() {
lines.push(last_attempt);
start = last_end;
@ -468,7 +466,7 @@ fn linebreak_simple<'a>(
// 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
// since then no shorter line will be possible.
if mandatory || !width.fits(attempt.size.x) {
if mandatory || !width.fits(attempt.width) {
lines.push(attempt);
start = end;
last = None;
@ -547,7 +545,7 @@ fn linebreak_optimized<'a>(
// Determine how much the line's spaces would need to be stretched
// to make it the desired width.
let delta = width - attempt.size.x;
let delta = width - attempt.width;
let mut ratio = delta / attempt.stretch();
if ratio.is_infinite() {
ratio = delta / (em / 2.0);
@ -796,8 +794,6 @@ fn line<'a>(
}
let mut width = Length::zero();
let mut top = Length::zero();
let mut bottom = Length::zero();
let mut fr = Fraction::zero();
// Measure the size of the line.
@ -805,16 +801,8 @@ fn line<'a>(
match item {
ParItem::Absolute(v) => width += *v,
ParItem::Fractional(v) => fr += *v,
ParItem::Text(shaped) => {
width += shaped.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());
}
ParItem::Text(shaped) => width += shaped.width,
ParItem::Frame(frame) => width += frame.size.x,
}
}
@ -825,8 +813,7 @@ fn line<'a>(
items,
last,
ranges: &p.ranges[first_idx ..= last_idx],
size: Size::new(width, top + bottom),
baseline: top,
width,
fr,
mandatory,
dash,
@ -836,7 +823,7 @@ fn line<'a>(
/// Combine layouted lines into one frame per region.
fn stack(
lines: &[Line],
fonts: &FontStore,
fonts: &mut FontStore,
regions: &Regions,
styles: StyleChain,
) -> Vec<Arc<Frame>> {
@ -848,7 +835,7 @@ fn stack(
// should expand or there's fractional spacing, fit-to-width otherwise.
let mut width = regions.first.x;
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.
@ -859,7 +846,10 @@ fn stack(
// Stack the lines into one frame per region.
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));
output = Frame::new(Size::with_x(width));
regions.next();
@ -870,12 +860,11 @@ fn stack(
output.size.y += leading;
}
let frame = commit(line, fonts, width, align, justify);
let pos = Point::with_y(output.size.y);
output.size.y += frame.size.y;
output.size.y += height;
output.merge_frame(pos, frame);
regions.first.y -= line.size.y + leading;
regions.first.y -= height + leading;
first = false;
}
@ -886,13 +875,12 @@ fn stack(
/// Commit to a line and build its frame.
fn commit(
line: &Line,
fonts: &FontStore,
fonts: &mut FontStore,
width: Length,
align: Align,
justify: bool,
) -> Frame {
let size = Size::new(width, line.size.y);
let mut remaining = width - line.size.x;
let mut remaining = width - line.width;
let mut offset = Length::zero();
// Reorder the line from logical to visual order.
@ -903,8 +891,7 @@ fn commit(
if let Some(glyph) = text.glyphs.first() {
if text.styles.get(TextNode::OVERHANG) {
let start = text.dir.is_positive();
let em = text.styles.get(TextNode::SIZE);
let amount = overhang(glyph.c, start) * glyph.x_advance.at(em);
let amount = overhang(glyph.c, start) * glyph.x_advance.at(text.size);
offset -= amount;
remaining += amount;
}
@ -918,8 +905,7 @@ fn commit(
&& (reordered.len() > 1 || text.glyphs.len() > 1)
{
let start = !text.dir.is_positive();
let em = text.styles.get(TextNode::SIZE);
let amount = overhang(glyph.c, start) * glyph.x_advance.at(em);
let amount = overhang(glyph.c, start) * glyph.x_advance.at(text.size);
remaining += amount;
}
}
@ -940,24 +926,41 @@ fn commit(
}
}
let mut output = Frame::new(size);
output.baseline = Some(line.baseline);
let mut top = Length::zero();
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 {
let mut position = |frame: Frame| {
let x = offset + align.position(remaining);
let y = line.baseline - frame.baseline();
offset += frame.size.x;
output.merge_frame(Point::new(x, y), frame);
let frame = match item {
ParItem::Absolute(v) => {
offset += *v;
continue;
}
ParItem::Fractional(v) => {
offset += v.share(line.fr, remaining);
continue;
}
ParItem::Text(shaped) => shaped.build(fonts, justification),
ParItem::Frame(frame) => frame.clone(),
};
match item {
ParItem::Absolute(v) => offset += *v,
ParItem::Fractional(v) => offset += v.share(line.fr, remaining),
ParItem::Text(shaped) => position(shaped.build(fonts, justification)),
ParItem::Frame(frame) => position(frame.clone()),
let width = frame.size.x;
top.set_max(frame.baseline());
bottom.set_max(frame.size.y - frame.baseline());
frames.push((offset, frame));
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

View File

@ -20,10 +20,12 @@ pub struct ShapedText<'a> {
pub dir: Dir,
/// The text's style properties.
pub styles: StyleChain<'a>,
/// The size of the text's bounding box.
pub size: Size,
/// The baseline from the top of the frame.
pub baseline: Length,
/// The font variant.
pub variant: FontVariant,
/// The font size.
pub size: Length,
/// The width of the text's bounding box.
pub width: Length,
/// The shaped glyphs.
pub glyphs: Cow<'a, [ShapedGlyph]>,
}
@ -74,15 +76,17 @@ impl<'a> ShapedText<'a> {
///
/// The `justification` defines how much extra advance width each
/// [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 frame = Frame::new(self.size);
frame.baseline = Some(self.baseline);
let mut frame = Frame::new(size);
frame.baseline = Some(top);
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 glyphs = group
.iter()
@ -91,7 +95,7 @@ impl<'a> ShapedText<'a> {
x_advance: glyph.x_advance
+ if glyph.is_justifiable() {
frame.size.x += justification;
Em::from_length(justification, size)
Em::from_length(justification, self.size)
} else {
Em::zero()
},
@ -99,7 +103,7 @@ impl<'a> ShapedText<'a> {
})
.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 width = text.width();
@ -120,6 +124,40 @@ impl<'a> ShapedText<'a> {
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.
pub fn justifiables(&self) -> usize {
self.glyphs.iter().filter(|g| g.is_justifiable()).count()
@ -132,7 +170,7 @@ impl<'a> ShapedText<'a> {
.filter(|g| g.is_justifiable())
.map(|g| g.x_advance)
.sum::<Em>()
.at(self.styles.get(TextNode::SIZE))
.at(self.size)
}
/// Reshape a range of the shaped text, reusing information from this
@ -143,13 +181,13 @@ impl<'a> ShapedText<'a> {
text_range: Range<usize>,
) -> ShapedText<'a> {
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
let (size, baseline) = measure(fonts, &glyphs, self.styles);
Self {
text: Cow::Borrowed(&self.text[text_range]),
dir: self.dir,
styles: self.styles,
size,
baseline,
size: self.size,
variant: self.variant,
width: glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(self.size),
glyphs: Cow::Borrowed(glyphs),
}
} else {
@ -159,16 +197,14 @@ impl<'a> ShapedText<'a> {
/// Push a hyphen to end of the text.
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| {
let face_id = fonts.select(family, variant)?;
let face_id = fonts.select(family, self.variant)?;
let face = fonts.get(face_id);
let ttf = face.ttf();
let glyph_id = ttf.glyph_index('-')?;
let x_advance = face.to_em(ttf.glyph_hor_advance(glyph_id)?);
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 {
face_id,
glyph_id: glyph_id.0,
@ -247,6 +283,7 @@ struct ShapingContext<'a> {
glyphs: Vec<ShapedGlyph>,
used: Vec<FaceId>,
styles: StyleChain<'a>,
size: Length,
variant: FontVariant,
tags: Vec<rustybuzz::Feature>,
fallback: bool,
@ -260,6 +297,7 @@ pub fn shape<'a>(
styles: StyleChain<'a>,
dir: Dir,
) -> ShapedText<'a> {
let size = styles.get(TextNode::SIZE);
let text = match styles.get(TextNode::CASE) {
Some(case) => Cow::Owned(case.apply(text)),
None => Cow::Borrowed(text),
@ -267,6 +305,7 @@ pub fn shape<'a>(
let mut ctx = ShapingContext {
fonts,
size,
glyphs: vec![],
used: vec![],
styles,
@ -282,14 +321,13 @@ pub fn shape<'a>(
track_and_space(&mut ctx);
let (size, baseline) = measure(ctx.fonts, &ctx.glyphs, styles);
ShapedText {
text,
dir,
styles,
variant: ctx.variant,
size,
baseline,
width: ctx.glyphs.iter().map(|g| g.x_advance).sum::<Em>().at(size),
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.
fn track_and_space(ctx: &mut ShapingContext) {
let em = ctx.styles.get(TextNode::SIZE);
let tracking = Em::from_length(ctx.styles.get(TextNode::TRACKING), em);
let spacing = ctx.styles.get(TextNode::SPACING).map(|abs| Em::from_length(abs, em));
let tracking = Em::from_length(ctx.styles.get(TextNode::TRACKING), ctx.size);
let spacing = ctx
.styles
.get(TextNode::SPACING)
.map(|abs| Em::from_length(abs, ctx.size));
if tracking.is_zero() && spacing.is_one() {
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.
fn variant(styles: StyleChain) -> FontVariant {
let mut variant = FontVariant::new(