diff --git a/src/frame.rs b/src/frame.rs index 9613e485d..bda9307fa 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -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::().at(self.size) } } diff --git a/src/library/text/par.rs b/src/library/text/par.rs index 765c3bf54..a2f8160f0 100644 --- a/src/library/text/par.rs +++ b/src/library/text/par.rs @@ -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> { @@ -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 diff --git a/src/library/text/shaping.rs b/src/library/text/shaping.rs index 32177f0a6..a73e0effa 100644 --- a/src/library/text/shaping.rs +++ b/src/library/text/shaping.rs @@ -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::() - .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, ) -> 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::().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, used: Vec, styles: StyleChain<'a>, + size: Length, variant: FontVariant, tags: Vec, 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::().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(