Basic justification

This commit is contained in:
Laurenz 2022-02-25 20:48:38 +01:00
parent efde5cac88
commit b0f4b13f6d
5 changed files with 657 additions and 553 deletions

View File

@ -27,6 +27,11 @@ impl Em {
Self(Scalar(units.into() / units_per_em))
}
/// Create an em length from a length at the given font size.
pub fn from_length(length: Length, font_size: Length) -> Self {
Self(Scalar(length / font_size))
}
/// Convert to a length at the given font size.
pub fn resolve(self, font_size: Length) -> Length {
self.get() * font_size

View File

@ -32,6 +32,8 @@ impl ParNode {
pub const DIR: Dir = Dir::LTR;
/// How to align text and inline objects in their line.
pub const ALIGN: Align = Align::Left;
/// Whether to justify text in its line.
pub const JUSTIFY: bool = false;
/// The spacing between lines (dependent on scaled font size).
pub const LEADING: Linear = Relative::new(0.65).into();
/// The extra spacing between paragraphs (dependent on scaled font size).
@ -75,6 +77,7 @@ impl ParNode {
styles.set_opt(Self::DIR, dir);
styles.set_opt(Self::ALIGN, align);
styles.set_opt(Self::JUSTIFY, args.named("justify")?);
styles.set_opt(Self::LEADING, args.named("leading")?);
styles.set_opt(Self::SPACING, args.named("spacing")?);
styles.set_opt(Self::INDENT, args.named("indent")?);
@ -83,103 +86,6 @@ impl ParNode {
}
}
impl Layout for ParNode {
fn layout(
&self,
ctx: &mut Context,
regions: &Regions,
styles: StyleChain,
) -> TypResult<Vec<Arc<Frame>>> {
// Collect all text into one string used for BiDi analysis.
let text = self.collect_text();
let level = Level::from_dir(styles.get(Self::DIR));
let bidi = BidiInfo::new(&text, level);
// Prepare paragraph layout by building a representation on which we can
// do line breaking without layouting each and every line from scratch.
let par = ParLayout::new(ctx, self, bidi, regions, &styles)?;
let fonts = &mut ctx.fonts;
let em = styles.get(TextNode::SIZE).abs;
let align = styles.get(ParNode::ALIGN);
let leading = styles.get(ParNode::LEADING).resolve(em);
// The already determined lines and the current line attempt.
let mut lines = vec![];
let mut start = 0;
let mut last = None;
// Find suitable line breaks.
for (end, mandatory) in LineBreakIterator::new(&text) {
// Compute the line and its size.
let mut line = par.line(fonts, start .. end);
// 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 !regions.first.x.fits(line.size.x) {
if let Some((last_line, last_end)) = last.take() {
lines.push(last_line);
start = last_end;
line = par.line(fonts, start .. end);
}
}
// 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 no shorter line will be possible.
if mandatory || !regions.first.x.fits(line.size.x) {
lines.push(line);
start = end;
last = None;
} else {
last = Some((line, end));
}
}
if let Some((line, _)) = last {
lines.push(line);
}
// Determine the paragraph's width: Fit to width if we shoudn't expand
// and there's no fractional spacing.
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();
}
// State for final frame building.
let mut regions = regions.clone();
let mut finished = vec![];
let mut first = true;
let mut output = Frame::new(Size::with_x(width));
// Stack the lines into one frame per region.
for line in lines {
while !regions.first.y.fits(line.size.y) && !regions.in_last() {
finished.push(Arc::new(output));
output = Frame::new(Size::with_x(width));
regions.next();
first = true;
}
if !first {
output.size.y += leading;
}
let frame = line.build(fonts, width, align);
let pos = Point::with_y(output.size.y);
output.size.y += frame.size.y;
output.merge_frame(pos, frame);
regions.first.y -= line.size.y + leading;
first = false;
}
finished.push(Arc::new(output));
Ok(finished)
}
}
impl ParNode {
/// Concatenate all text in the paragraph into one string, replacing spacing
/// with a space character and other non-text nodes with the object
@ -212,6 +118,127 @@ impl ParNode {
}
}
impl Layout for ParNode {
fn layout(
&self,
ctx: &mut Context,
regions: &Regions,
styles: StyleChain,
) -> TypResult<Vec<Arc<Frame>>> {
// Collect all text into one string and perform BiDi analysis.
let text = self.collect_text();
let level = Level::from_dir(styles.get(Self::DIR));
let bidi = BidiInfo::new(&text, level);
// Prepare paragraph layout by building a representation on which we can
// do line breaking without layouting each and every line from scratch.
let par = ParLayout::new(ctx, self, bidi, regions, &styles)?;
// Break the paragraph into lines.
let lines = break_lines(&mut ctx.fonts, &par, regions.first.x);
// Stack the lines into one frame per region.
Ok(stack_lines(&ctx.fonts, lines, regions, styles))
}
}
/// Perform line breaking.
fn break_lines<'a>(
fonts: &mut FontStore,
par: &'a ParLayout<'a>,
width: Length,
) -> Vec<LineLayout<'a>> {
// The already determined lines and the current line attempt.
let mut lines = vec![];
let mut start = 0;
let mut last = None;
// Find suitable line breaks.
for (end, mandatory) in LineBreakIterator::new(&par.bidi.text) {
// Compute the line and its size.
let mut line = par.line(fonts, start .. end, mandatory);
// 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(line.size.x) {
if let Some((last_line, last_end)) = last.take() {
lines.push(last_line);
start = last_end;
line = par.line(fonts, start .. end, mandatory);
}
}
// 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(line.size.x) {
lines.push(line);
start = end;
last = None;
} else {
last = Some((line, end));
}
}
if let Some((line, _)) = last {
lines.push(line);
}
lines
}
/// Combine the lines into one frame per region.
fn stack_lines(
fonts: &FontStore,
lines: Vec<LineLayout>,
regions: &Regions,
styles: StyleChain,
) -> Vec<Arc<Frame>> {
let em = styles.get(TextNode::SIZE).abs;
let leading = styles.get(ParNode::LEADING).resolve(em);
let align = styles.get(ParNode::ALIGN);
let justify = styles.get(ParNode::JUSTIFY);
// Determine the paragraph's width: Full width of the region if we
// 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();
}
// State for final frame building.
let mut regions = regions.clone();
let mut finished = vec![];
let mut first = true;
let mut output = Frame::new(Size::with_x(width));
// Stack the lines into one frame per region.
for line in lines {
while !regions.first.y.fits(line.size.y) && !regions.in_last() {
finished.push(Arc::new(output));
output = Frame::new(Size::with_x(width));
regions.next();
first = true;
}
if !first {
output.size.y += leading;
}
let frame = line.build(fonts, width, align, justify);
let pos = Point::with_y(output.size.y);
output.size.y += frame.size.y;
output.merge_frame(pos, frame);
regions.first.y -= line.size.y + leading;
first = false;
}
finished.push(Arc::new(output));
finished
}
impl Debug for ParNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("Par ")?;
@ -261,7 +288,7 @@ impl LinebreakNode {
}
/// A paragraph representation in which children are already layouted and text
/// is separated into shapable runs.
/// is already preshaped.
struct ParLayout<'a> {
/// Bidirectional text embedding levels for the paragraph.
bidi: BidiInfo<'a>,
@ -340,7 +367,12 @@ impl<'a> ParLayout<'a> {
}
/// Create a line which spans the given range.
fn line(&'a self, fonts: &mut FontStore, mut range: Range) -> LineLayout<'a> {
fn line(
&'a self,
fonts: &mut FontStore,
mut range: Range,
mandatory: bool,
) -> LineLayout<'a> {
// Find the items which bound the text range.
let last_idx = self.find(range.end.saturating_sub(1)).unwrap();
let first_idx = if range.is_empty() {
@ -432,6 +464,7 @@ impl<'a> ParLayout<'a> {
size: Size::new(width, top + bottom),
baseline: top,
fr,
mandatory,
}
}
@ -467,18 +500,36 @@ struct LineLayout<'a> {
baseline: Length,
/// The sum of fractional ratios in the line.
fr: Fractional,
/// Whether the line ends at a mandatory break.
mandatory: bool,
}
impl<'a> LineLayout<'a> {
/// Build the line's frame.
fn build(&self, fonts: &FontStore, width: Length, align: Align) -> Frame {
let size = Size::new(self.size.x.max(width), self.size.y);
let remaining = size.x - self.size.x;
fn build(
&self,
fonts: &FontStore,
width: Length,
align: Align,
justify: bool,
) -> Frame {
let size = Size::new(width, self.size.y);
let mut remaining = width - self.size.x;
let mut offset = Length::zero();
let mut output = Frame::new(size);
output.baseline = Some(self.baseline);
let mut justification = Length::zero();
if justify
&& !self.mandatory
&& self.range.end < self.bidi.text.len()
&& self.fr.is_zero()
{
justification = remaining / self.spaces() as f64;
remaining = Length::zero();
}
for item in self.reordered() {
let mut position = |frame: Frame| {
let x = offset + align.resolve(remaining);
@ -490,7 +541,7 @@ impl<'a> LineLayout<'a> {
match item {
ParItem::Absolute(v) => offset += *v,
ParItem::Fractional(v) => offset += v.resolve(self.fr, remaining),
ParItem::Text(shaped) => position(shaped.build(fonts)),
ParItem::Text(shaped) => position(shaped.build(fonts, justification)),
ParItem::Frame(frame) => position(frame.clone()),
}
}
@ -498,6 +549,11 @@ impl<'a> LineLayout<'a> {
output
}
/// The number of spaces in the line.
fn spaces(&self) -> usize {
self.shapeds().map(ShapedText::spaces).sum()
}
/// Iterate through the line's items in visual order.
fn reordered(&self) -> impl Iterator<Item = &ParItem<'a>> {
// The bidi crate doesn't like empty lines.
@ -533,6 +589,19 @@ impl<'a> LineLayout<'a> {
.map(move |idx| self.get(idx).unwrap())
}
/// Iterate over the line's items.
fn items(&self) -> impl Iterator<Item = &ParItem<'a>> {
self.first.iter().chain(self.items).chain(&self.last)
}
/// Iterate through the line's text items.
fn shapeds(&self) -> impl Iterator<Item = &ShapedText<'a>> {
self.items().filter_map(|item| match item {
ParItem::Text(shaped) => Some(shaped),
_ => None,
})
}
/// Find the index of the item whose range contains the `text_offset`.
fn find(&self, text_offset: usize) -> Option<usize> {
self.ranges.binary_search_by(|r| r.locate(text_offset)).ok()
@ -540,7 +609,7 @@ impl<'a> LineLayout<'a> {
/// Get the item at the index.
fn get(&self, index: usize) -> Option<&ParItem<'a>> {
self.first.iter().chain(self.items).chain(&self.last).nth(index)
self.items().nth(index)
}
}

View File

@ -406,6 +406,191 @@ impl Case {
}
}
/// 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.
#[derive(Debug, Clone)]
pub struct ShapedText<'a> {
/// The text that was shaped.
pub text: Cow<'a, str>,
/// The text direction.
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 shaped glyphs.
pub glyphs: Cow<'a, [ShapedGlyph]>,
}
/// 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 index in the face.
pub glyph_id: u16,
/// The advance width of the glyph.
pub x_advance: Em,
/// The horizontal offset of the glyph.
pub x_offset: Em,
/// 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,
/// Whether this glyph represents a space.
pub is_space: bool,
}
/// A visual side.
enum Side {
Left,
Right,
}
impl<'a> ShapedText<'a> {
/// Build the shaped text's frame.
///
/// The `justification` defines how much extra advance width each
/// [space glyph](ShapedGlyph::is_space) will get.
pub fn build(&self, fonts: &FontStore, justification: Length) -> Frame {
let mut offset = Length::zero();
let mut frame = Frame::new(self.size);
frame.baseline = Some(self.baseline);
for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
let pos = Point::new(offset, self.baseline);
let size = self.styles.get(TextNode::SIZE).abs;
let fill = self.styles.get(TextNode::FILL);
let glyphs = group
.iter()
.map(|glyph| Glyph {
id: glyph.glyph_id,
x_advance: glyph.x_advance
+ if glyph.is_space {
frame.size.x += justification;
Em::from_length(justification, size)
} else {
Em::zero()
},
x_offset: glyph.x_offset,
})
.collect();
let text = Text { face_id, size, fill, glyphs };
let text_layer = frame.layer();
let width = text.width();
// Apply line decorations.
for deco in self.styles.get_cloned(TextNode::LINES) {
decorate(&mut frame, &deco, fonts, &text, pos, width);
}
frame.insert(text_layer, pos, Element::Text(text));
offset += width;
}
// Apply link if it exists.
if let Some(url) = self.styles.get_ref(TextNode::LINK) {
frame.link(url);
}
frame
}
/// How many spaces the text contains.
pub fn spaces(&self) -> usize {
self.glyphs.iter().filter(|g| g.is_space).count()
}
/// Reshape a range of the shaped text, reusing information from this
/// shaping process if possible.
pub fn reshape(
&'a self,
fonts: &mut FontStore,
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.clone(),
size,
baseline,
glyphs: Cow::Borrowed(glyphs),
}
} else {
shape(fonts, &self.text[text_range], self.styles.clone(), self.dir)
}
}
/// 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 Range { mut start, mut end } = text_range;
if !self.dir.is_positive() {
std::mem::swap(&mut start, &mut end);
}
let left = self.find_safe_to_break(start, Side::Left)?;
let right = self.find_safe_to_break(end, Side::Right)?;
Some(&self.glyphs[left .. right])
}
/// Find the glyph offset matching the text index that is most towards the
/// given side and safe-to-break.
fn find_safe_to_break(&self, text_index: usize, towards: Side) -> 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 });
}
// Find any glyph with the text index.
let mut idx = self
.glyphs
.binary_search_by(|g| {
let ordering = g.text_index.cmp(&text_index);
if ltr { ordering } else { ordering.reverse() }
})
.ok()?;
let next = match towards {
Side::Left => usize::checked_sub,
Side::Right => usize::checked_add,
};
// Search for the outermost glyph with the text index.
while let Some(next) = next(idx, 1) {
if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) {
break;
}
idx = next;
}
// RTL needs offset one because the left side of the range should be
// exclusive and the right side inclusive, contrary to the normal
// behaviour of ranges.
if !ltr {
idx += 1;
}
self.glyphs[idx].safe_to_break.then(|| idx)
}
}
/// Shape text into [`ShapedText`].
pub fn shape<'a>(
fonts: &mut FontStore,
@ -446,197 +631,6 @@ pub fn shape<'a>(
}
}
/// Shape text with font fallback using the `families` iterator.
fn shape_segment<'a>(
fonts: &mut FontStore,
glyphs: &mut Vec<ShapedGlyph>,
base: usize,
text: &str,
variant: FontVariant,
mut families: impl Iterator<Item = &'a str> + Clone,
mut first_face: Option<FaceId>,
dir: Dir,
tags: &[rustybuzz::Feature],
) {
// No font has newlines.
if text.chars().all(|c| c == '\n') {
return;
}
// Select the font family.
let (face_id, fallback) = loop {
// Try to load the next available font family.
match families.next() {
Some(family) => {
if let Some(id) = fonts.select(family, variant) {
break (id, true);
}
}
// We're out of families, so we don't do any more fallback and just
// shape the tofus with the first face we originally used.
None => match first_face {
Some(id) => break (id, false),
None => return,
},
}
};
// Remember the id if this the first available face since we use that one to
// shape tofus.
first_face.get_or_insert(face_id);
// Fill the buffer with our text.
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
buffer.set_direction(match dir {
Dir::LTR => rustybuzz::Direction::LeftToRight,
Dir::RTL => rustybuzz::Direction::RightToLeft,
_ => unimplemented!(),
});
// Shape!
let mut face = fonts.get(face_id);
let buffer = rustybuzz::shape(face.ttf(), tags, buffer);
let infos = buffer.glyph_infos();
let pos = buffer.glyph_positions();
// Collect the shaped glyphs, doing fallback and shaping parts again with
// the next font if necessary.
let mut i = 0;
while i < infos.len() {
let info = &infos[i];
let cluster = info.cluster as usize;
if info.glyph_id != 0 || !fallback {
// Add the glyph to the shaped output.
// TODO: Don't ignore y_advance and y_offset.
glyphs.push(ShapedGlyph {
face_id,
glyph_id: info.glyph_id as u16,
x_advance: face.to_em(pos[i].x_advance),
x_offset: face.to_em(pos[i].x_offset),
text_index: base + cluster,
safe_to_break: !info.unsafe_to_break(),
});
} else {
// Determine the source text range for the tofu sequence.
let range = {
// First, search for the end of the tofu sequence.
let k = i;
while infos.get(i + 1).map_or(false, |info| info.glyph_id == 0) {
i += 1;
}
// Then, determine the start and end text index.
//
// Examples:
// Everything is shown in visual order. Tofus are written as "_".
// We want to find out that the tofus span the text `2..6`.
// Note that the clusters are longer than 1 char.
//
// Left-to-right:
// Text: h a l i h a l l o
// Glyphs: A _ _ C E
// Clusters: 0 2 4 6 8
// k=1 i=2
//
// Right-to-left:
// Text: O L L A H I L A H
// Glyphs: E C _ _ A
// Clusters: 8 6 4 2 0
// k=2 i=3
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_or(text.len(), |info| info.cluster as usize);
start .. end
};
// Recursively shape the tofu sequence with the next family.
shape_segment(
fonts,
glyphs,
base + range.start,
&text[range],
variant,
families.clone(),
first_face,
dir,
tags,
);
face = fonts.get(face_id);
}
i += 1;
}
}
/// Apply tracking to a slice of shaped glyphs.
fn track(glyphs: &mut [ShapedGlyph], tracking: Em) {
if tracking.is_zero() {
return;
}
let mut glyphs = glyphs.iter_mut().peekable();
while let Some(glyph) = glyphs.next() {
if glyphs
.peek()
.map_or(false, |next| glyph.text_index != next.text_index)
{
glyph.x_advance += tracking;
}
}
}
/// 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).abs;
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| {
top.set_max(face.vertical_metric(top_edge, size));
bottom.set_max(-face.vertical_metric(bottom_edge, size));
};
if glyphs.is_empty() {
// When there are no glyphs, we just use the vertical metrics of the
// first available font.
for family in families(styles) {
if let Some(face_id) = fonts.select(family, variant(styles)) {
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.resolve(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(
@ -762,91 +756,200 @@ fn tags(styles: StyleChain) -> Vec<Feature> {
tags
}
/// 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.
#[derive(Debug, Clone)]
pub struct ShapedText<'a> {
/// The text that was shaped.
pub text: Cow<'a, str>,
/// The text direction.
pub dir: Dir,
/// The text's style properties.
pub styles: StyleChain<'a>,
/// 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]>,
/// Shape text with font fallback using the `families` iterator.
fn shape_segment<'a>(
fonts: &mut FontStore,
glyphs: &mut Vec<ShapedGlyph>,
base: usize,
text: &str,
variant: FontVariant,
mut families: impl Iterator<Item = &'a str> + Clone,
mut first_face: Option<FaceId>,
dir: Dir,
tags: &[rustybuzz::Feature],
) {
// No font has newlines.
if text.chars().all(|c| c == '\n') {
return;
}
/// 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 index in the face.
pub glyph_id: u16,
/// The advance width of the glyph.
pub x_advance: Em,
/// The horizontal offset of the glyph.
pub x_offset: Em,
/// 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,
// Select the font family.
let (face_id, fallback) = loop {
// Try to load the next available font family.
match families.next() {
Some(family) => {
if let Some(id) = fonts.select(family, variant) {
break (id, true);
}
}
// We're out of families, so we don't do any more fallback and just
// shape the tofus with the first face we originally used.
None => match first_face {
Some(id) => break (id, false),
None => return,
},
}
};
// Remember the id if this the first available face since we use that one to
// shape tofus.
first_face.get_or_insert(face_id);
// Fill the buffer with our text.
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
buffer.set_direction(match dir {
Dir::LTR => rustybuzz::Direction::LeftToRight,
Dir::RTL => rustybuzz::Direction::RightToLeft,
_ => unimplemented!(),
});
// Shape!
let mut face = fonts.get(face_id);
let buffer = rustybuzz::shape(face.ttf(), tags, buffer);
let infos = buffer.glyph_infos();
let pos = buffer.glyph_positions();
// Collect the shaped glyphs, doing fallback and shaping parts again with
// the next font if necessary.
let mut i = 0;
while i < infos.len() {
let info = &infos[i];
let cluster = info.cluster as usize;
if info.glyph_id != 0 || !fallback {
// Add the glyph to the shaped output.
// TODO: Don't ignore y_advance and y_offset.
glyphs.push(ShapedGlyph {
face_id,
glyph_id: info.glyph_id as u16,
x_advance: face.to_em(pos[i].x_advance),
x_offset: face.to_em(pos[i].x_offset),
text_index: base + cluster,
safe_to_break: !info.unsafe_to_break(),
is_space: text[cluster ..].chars().next() == Some(' '),
});
} else {
// Determine the source text range for the tofu sequence.
let range = {
// First, search for the end of the tofu sequence.
let k = i;
while infos.get(i + 1).map_or(false, |info| info.glyph_id == 0) {
i += 1;
}
impl<'a> ShapedText<'a> {
/// Build the shaped text's frame.
pub fn build(&self, fonts: &FontStore) -> Frame {
let mut offset = Length::zero();
let mut frame = Frame::new(self.size);
frame.baseline = Some(self.baseline);
// Then, determine the start and end text index.
//
// Examples:
// Everything is shown in visual order. Tofus are written as "_".
// We want to find out that the tofus span the text `2..6`.
// Note that the clusters are longer than 1 char.
//
// Left-to-right:
// Text: h a l i h a l l o
// Glyphs: A _ _ C E
// Clusters: 0 2 4 6 8
// k=1 i=2
//
// Right-to-left:
// Text: O L L A H I L A H
// Glyphs: E C _ _ A
// Clusters: 8 6 4 2 0
// k=2 i=3
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_or(text.len(), |info| info.cluster as usize);
for (face_id, group) in self.glyphs.as_ref().group_by_key(|g| g.face_id) {
let pos = Point::new(offset, self.baseline);
start .. end
};
let size = self.styles.get(TextNode::SIZE).abs;
let fill = self.styles.get(TextNode::FILL);
let glyphs = group
.iter()
.map(|glyph| Glyph {
id: glyph.glyph_id,
x_advance: glyph.x_advance,
x_offset: glyph.x_offset,
})
.collect();
// Recursively shape the tofu sequence with the next family.
shape_segment(
fonts,
glyphs,
base + range.start,
&text[range],
variant,
families.clone(),
first_face,
dir,
tags,
);
let text = Text { face_id, size, fill, glyphs };
let text_layer = frame.layer();
let width = text.width();
// Apply line decorations.
for deco in self.styles.get_cloned(TextNode::LINES) {
self.decorate(&mut frame, &deco, fonts, &text, pos, width);
face = fonts.get(face_id);
}
frame.insert(text_layer, pos, Element::Text(text));
offset += width;
i += 1;
}
}
// Apply link if it exists.
if let Some(url) = self.styles.get_ref(TextNode::LINK) {
frame.link(url);
/// Apply tracking to a slice of shaped glyphs.
fn track(glyphs: &mut [ShapedGlyph], tracking: Em) {
if tracking.is_zero() {
return;
}
frame
let mut glyphs = glyphs.iter_mut().peekable();
while let Some(glyph) = glyphs.next() {
if glyphs
.peek()
.map_or(false, |next| glyph.text_index != next.text_index)
{
glyph.x_advance += tracking;
}
}
}
/// Add line decorations to a run of shaped text of a single font.
/// 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).abs;
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| {
top.set_max(face.vertical_metric(top_edge, size));
bottom.set_max(-face.vertical_metric(bottom_edge, size));
};
if glyphs.is_empty() {
// When there are no glyphs, we just use the vertical metrics of the
// first available font.
for family in families(styles) {
if let Some(face_id) = fonts.select(family, variant(styles)) {
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.resolve(size);
}
}
}
(Size::new(width, top + bottom), top)
}
/// Add line decorations to a single run of shaped text.
fn decorate(
&self,
frame: &mut Frame,
deco: &Decoration,
fonts: &FontStore,
@ -907,8 +1010,7 @@ impl<'a> ShapedText<'a> {
for glyph in text.glyphs.iter() {
let dx = glyph.x_offset.resolve(text.size) + x;
let mut builder =
KurboPathBuilder::new(face.units_per_em, text.size, dx.to_raw());
let mut builder = BezPathBuilder::new(face.units_per_em, text.size, dx.to_raw());
let bbox = face.ttf().outline_glyph(GlyphId(glyph.id), &mut builder);
let path = builder.finish();
@ -958,101 +1060,15 @@ impl<'a> ShapedText<'a> {
}
}
/// Reshape a range of the shaped text, reusing information from this
/// shaping process if possible.
pub fn reshape(
&'a self,
fonts: &mut FontStore,
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.clone(),
size,
baseline,
glyphs: Cow::Borrowed(glyphs),
}
} else {
shape(fonts, &self.text[text_range], self.styles.clone(), self.dir)
}
}
/// 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 Range { mut start, mut end } = text_range;
if !self.dir.is_positive() {
std::mem::swap(&mut start, &mut end);
}
let left = self.find_safe_to_break(start, Side::Left)?;
let right = self.find_safe_to_break(end, Side::Right)?;
Some(&self.glyphs[left .. right])
}
/// Find the glyph offset matching the text index that is most towards the
/// given side and safe-to-break.
fn find_safe_to_break(&self, text_index: usize, towards: Side) -> 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 });
}
// Find any glyph with the text index.
let mut idx = self
.glyphs
.binary_search_by(|g| {
let ordering = g.text_index.cmp(&text_index);
if ltr { ordering } else { ordering.reverse() }
})
.ok()?;
let next = match towards {
Side::Left => usize::checked_sub,
Side::Right => usize::checked_add,
};
// Search for the outermost glyph with the text index.
while let Some(next) = next(idx, 1) {
if self.glyphs.get(next).map_or(true, |g| g.text_index != text_index) {
break;
}
idx = next;
}
// RTL needs offset one because the left side of the range should be
// exclusive and the right side inclusive, contrary to the normal
// behaviour of ranges.
if !ltr {
idx += 1;
}
self.glyphs[idx].safe_to_break.then(|| idx)
}
}
/// A visual side.
enum Side {
Left,
Right,
}
struct KurboPathBuilder {
/// Builds a kurbo [`BezPath`] for a glyph.
struct BezPathBuilder {
path: BezPath,
units_per_em: f64,
font_size: Length,
x_offset: f64,
}
impl KurboPathBuilder {
impl BezPathBuilder {
fn new(units_per_em: f64, font_size: Length, x_offset: f64) -> Self {
Self {
path: BezPath::new(),
@ -1075,7 +1091,7 @@ impl KurboPathBuilder {
}
}
impl OutlineBuilder for KurboPathBuilder {
impl OutlineBuilder for BezPathBuilder {
fn move_to(&mut self, x: f32, y: f32) {
self.path.move_to(self.p(x, y));
}

BIN
tests/ref/text/justify.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,14 @@
---
#set par(indent: 14pt, spacing: 0pt, leading: 5pt, justify: true)
This text is justified, meaning that spaces are stretched so that the text
forms as "block" with flush edges at both sides.
First line indents and hyphenation play nicely with justified text.
---
// Test that lines with hard breaks aren't justified.
#set par(justify: true)
A B C \
D