mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Less style chains lookups during paragraph layout
This commit is contained in:
parent
34fa8df044
commit
029b87b0a9
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
Loading…
x
Reference in New Issue
Block a user