Tidy paragraph layout

This commit is contained in:
Laurenz 2022-04-18 13:08:00 +02:00
parent a302105b9f
commit f27f7a05ab

View File

@ -76,7 +76,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.
stack(ctx, &lines, regions, styles) stack(&p, ctx, &lines, regions)
} }
} }
@ -194,8 +194,16 @@ struct Preparation<'a> {
items: Vec<Item<'a>>, items: Vec<Item<'a>>,
/// The styles shared by all children. /// The styles shared by all children.
styles: StyleChain<'a>, styles: StyleChain<'a>,
/// The paragraph's children. /// Whether to hyphenate if it's the same for all children.
children: &'a StyleVec<ParChild>, hyphenate: Option<bool>,
/// The text language if it's the same for all children.
lang: Option<Lang>,
/// The resolved leading between lines.
leading: Length,
/// The paragraph's resolved alignment.
align: Align,
/// Whether to justify the paragraph.
justify: bool,
} }
impl<'a> Preparation<'a> { impl<'a> Preparation<'a> {
@ -240,15 +248,6 @@ impl<'a> Preparation<'a> {
(expanded, &self.items[start .. end]) (expanded, &self.items[start .. end])
} }
/// Get a style property, but only if it is the same for all children of the
/// paragraph.
fn get_shared<K: Key<'a>>(&self, key: K) -> Option<K::Output> {
self.children
.maps()
.all(|map| !map.contains(key))
.then(|| self.styles.get(key))
}
} }
/// A segment of one or multiple collapsed children. /// A segment of one or multiple collapsed children.
@ -286,7 +285,7 @@ enum Item<'a> {
/// A layouted child node. /// A layouted child node.
Frame(Frame), Frame(Frame),
/// A repeating node. /// A repeating node.
Repeat(&'a RepeatNode), Repeat(&'a RepeatNode, StyleChain<'a>),
} }
impl<'a> Item<'a> { impl<'a> Item<'a> {
@ -303,7 +302,7 @@ impl<'a> Item<'a> {
match self { match self {
Self::Text(shaped) => shaped.text.len(), Self::Text(shaped) => shaped.text.len(),
Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(), Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(),
Self::Frame(_) | Self::Repeat(_) => NODE_REPLACE.len_utf8(), Self::Frame(_) | Self::Repeat(_, _) => NODE_REPLACE.len_utf8(),
} }
} }
@ -312,7 +311,7 @@ impl<'a> Item<'a> {
match self { match self {
Item::Text(shaped) => shaped.width, Item::Text(shaped) => shaped.width,
Item::Absolute(v) => *v, Item::Absolute(v) => *v,
Item::Fractional(_) | Self::Repeat(_) => Length::zero(), Item::Fractional(_) | Self::Repeat(_, _) => Length::zero(),
Item::Frame(frame) => frame.size.x, Item::Frame(frame) => frame.size.x,
} }
} }
@ -403,7 +402,7 @@ impl<'a> Line<'a> {
self.items() self.items()
.filter_map(|item| match item { .filter_map(|item| match item {
Item::Fractional(fr) => Some(*fr), Item::Fractional(fr) => Some(*fr),
Item::Repeat(_) => Some(Fraction::one()), Item::Repeat(_, _) => Some(Fraction::one()),
_ => None, _ => None,
}) })
.sum() .sum()
@ -505,38 +504,7 @@ fn prepare<'a>(
let end = cursor + segment.len(); let end = cursor + segment.len();
match segment { match segment {
Segment::Text(_) => { Segment::Text(_) => {
let mut process = |text, level: Level| { shape_range(&mut items, &mut ctx.fonts, &bidi, cursor .. end, styles);
let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL };
let shaped = shape(&mut ctx.fonts, text, styles, dir);
items.push(Item::Text(shaped));
};
let mut prev_level = Level::ltr();
let mut prev_script = Script::Unknown;
// Group by embedding level and script.
for i in cursor .. end {
if !text.is_char_boundary(i) {
continue;
}
let level = bidi.levels[i];
let script =
text[i ..].chars().next().map_or(Script::Unknown, |c| c.script());
if level != prev_level || !is_compatible(script, prev_script) {
if cursor < i {
process(&text[cursor .. i], prev_level);
}
cursor = i;
prev_level = level;
prev_script = script;
} else if is_generic_script(prev_script) {
prev_script = script;
}
}
process(&text[cursor .. end], prev_level);
} }
Segment::Spacing(spacing) => match spacing { Segment::Spacing(spacing) => match spacing {
Spacing::Relative(v) => { Spacing::Relative(v) => {
@ -549,7 +517,7 @@ fn prepare<'a>(
}, },
Segment::Node(node) => { Segment::Node(node) => {
if let Some(repeat) = node.downcast() { if let Some(repeat) = node.downcast() {
items.push(Item::Repeat(repeat)); items.push(Item::Repeat(repeat, styles));
} else { } else {
let size = Size::new(regions.first.x, regions.base.y); let size = Size::new(regions.first.x, regions.base.y);
let pod = Regions::one(size, regions.base, Spec::splat(false)); let pod = Regions::one(size, regions.base, Spec::splat(false));
@ -562,7 +530,60 @@ fn prepare<'a>(
cursor = end; cursor = end;
} }
Ok(Preparation { bidi, items, styles, children: &par.0 }) Ok(Preparation {
bidi,
items,
styles,
hyphenate: shared_get(styles, &par.0, TextNode::HYPHENATE),
lang: shared_get(styles, &par.0, TextNode::LANG),
leading: styles.get(ParNode::LEADING),
align: styles.get(ParNode::ALIGN),
justify: styles.get(ParNode::JUSTIFY),
})
}
/// Group a range of text by BiDi level and script, shape the runs and generate
/// items for them.
fn shape_range<'a>(
items: &mut Vec<Item<'a>>,
fonts: &mut FontStore,
bidi: &BidiInfo<'a>,
range: Range,
styles: StyleChain<'a>,
) {
let mut process = |text, level: Level| {
let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL };
let shaped = shape(fonts, text, styles, dir);
items.push(Item::Text(shaped));
};
let mut prev_level = Level::ltr();
let mut prev_script = Script::Unknown;
let mut cursor = range.start;
// Group by embedding level and script.
for i in cursor .. range.end {
if !bidi.text.is_char_boundary(i) {
continue;
}
let level = bidi.levels[i];
let script =
bidi.text[i ..].chars().next().map_or(Script::Unknown, |c| c.script());
if level != prev_level || !is_compatible(script, prev_script) {
if cursor < i {
process(&bidi.text[cursor .. i], prev_level);
}
cursor = i;
prev_level = level;
prev_script = script;
} else if is_generic_script(prev_script) {
prev_script = script;
}
}
process(&bidi.text[cursor .. range.end], prev_level);
} }
/// Whether this is not a specific script. /// Whether this is not a specific script.
@ -575,6 +596,16 @@ fn is_compatible(a: Script, b: Script) -> bool {
is_generic_script(a) || is_generic_script(b) || a == b is_generic_script(a) || is_generic_script(b) || a == b
} }
/// Get a style property, but only if it is the same for all children of the
/// paragraph.
fn shared_get<'a, K: Key<'a>>(
styles: StyleChain<'a>,
children: &StyleVec<ParChild>,
key: K,
) -> Option<K::Output> {
children.maps().all(|map| !map.contains(key)).then(|| styles.get(key))
}
/// Find suitable linebreaks. /// Find suitable linebreaks.
fn linebreak<'a>( fn linebreak<'a>(
p: &'a Preparation<'a>, p: &'a Preparation<'a>,
@ -672,9 +703,6 @@ fn linebreak_optimized<'a>(
const MIN_COST: Cost = -MAX_COST; const MIN_COST: Cost = -MAX_COST;
const MIN_RATIO: f64 = -0.15; const MIN_RATIO: f64 = -0.15;
let em = p.styles.get(TextNode::SIZE);
let justify = p.styles.get(ParNode::JUSTIFY);
// Dynamic programming table. // Dynamic programming table.
let mut active = 0; let mut active = 0;
let mut table = vec![Entry { let mut table = vec![Entry {
@ -683,6 +711,8 @@ fn linebreak_optimized<'a>(
line: line(p, fonts, 0 .. 0, false, false), line: line(p, fonts, 0 .. 0, false, false),
}]; }];
let em = p.styles.get(TextNode::SIZE);
for (end, mandatory, hyphen) in breakpoints(p) { for (end, mandatory, hyphen) in breakpoints(p) {
let k = table.len(); let k = table.len();
let eof = end == p.bidi.text.len(); let eof = end == p.bidi.text.len();
@ -706,7 +736,7 @@ fn linebreak_optimized<'a>(
ratio = ratio.min(10.0); ratio = ratio.min(10.0);
// Determine the cost of the line. // Determine the cost of the line.
let mut cost = if ratio < if justify { MIN_RATIO } else { 0.0 } { let mut cost = if ratio < if p.justify { MIN_RATIO } else { 0.0 } {
// The line is overfull. This is the case if // The line is overfull. This is the case if
// - justification is on, but we'd need to shrink to much // - justification is on, but we'd need to shrink to much
// - justification is off and the line just doesn't fit // - justification is off and the line just doesn't fit
@ -779,8 +809,6 @@ fn breakpoints<'a>(p: &'a Preparation) -> Breakpoints<'a> {
suffix: 0, suffix: 0,
end: 0, end: 0,
mandatory: false, mandatory: false,
hyphenate: p.get_shared(TextNode::HYPHENATE),
lang: p.get_shared(TextNode::LANG),
} }
} }
@ -800,10 +828,6 @@ struct Breakpoints<'a> {
end: usize, end: usize,
/// Whether the break after the current word is mandatory. /// Whether the break after the current word is mandatory.
mandatory: bool, mandatory: bool,
/// Whether to hyphenate if it's the same for all children.
hyphenate: Option<bool>,
/// The text language if it's the same for all children.
lang: Option<Lang>,
} }
impl Iterator for Breakpoints<'_> { impl Iterator for Breakpoints<'_> {
@ -820,7 +844,7 @@ impl Iterator for Breakpoints<'_> {
// Filter out hyphenation opportunities where hyphenation was // Filter out hyphenation opportunities where hyphenation was
// actually disabled. // actually disabled.
let hyphen = self.offset < self.end; let hyphen = self.offset < self.end;
if hyphen && !self.hyphenate_at(self.offset) { if hyphen && !self.hyphenate(self.offset) {
return self.next(); return self.next();
} }
@ -831,8 +855,8 @@ impl Iterator for Breakpoints<'_> {
(self.end, self.mandatory) = self.linebreaks.next()?; (self.end, self.mandatory) = self.linebreaks.next()?;
// Hyphenate the next word. // Hyphenate the next word.
if self.hyphenate != Some(false) { if self.p.hyphenate != Some(false) {
if let Some(lang) = self.lang_at(self.offset) { if let Some(lang) = self.lang(self.offset) {
let word = &self.p.bidi.text[self.offset .. self.end]; let word = &self.p.bidi.text[self.offset .. self.end];
let trimmed = word.trim_end_matches(|c: char| !c.is_alphabetic()); let trimmed = word.trim_end_matches(|c: char| !c.is_alphabetic());
if !trimmed.is_empty() { if !trimmed.is_empty() {
@ -850,8 +874,9 @@ impl Iterator for Breakpoints<'_> {
impl Breakpoints<'_> { impl Breakpoints<'_> {
/// Whether hyphenation is enabled at the given offset. /// Whether hyphenation is enabled at the given offset.
fn hyphenate_at(&self, offset: usize) -> bool { fn hyphenate(&self, offset: usize) -> bool {
self.hyphenate self.p
.hyphenate
.or_else(|| { .or_else(|| {
let shaped = self.p.find(offset)?.text()?; let shaped = self.p.find(offset)?.text()?;
Some(shaped.styles.get(TextNode::HYPHENATE)) Some(shaped.styles.get(TextNode::HYPHENATE))
@ -860,8 +885,8 @@ impl Breakpoints<'_> {
} }
/// The text language at the given offset. /// The text language at the given offset.
fn lang_at(&self, offset: usize) -> Option<hypher::Lang> { fn lang(&self, offset: usize) -> Option<hypher::Lang> {
let lang = self.lang.or_else(|| { let lang = self.p.lang.or_else(|| {
let shaped = self.p.find(offset)?.text()?; let shaped = self.p.find(offset)?.text()?;
Some(shaped.styles.get(TextNode::LANG)) Some(shaped.styles.get(TextNode::LANG))
})?; })?;
@ -980,15 +1005,11 @@ fn line<'a>(
/// Combine layouted lines into one frame per region. /// Combine layouted lines into one frame per region.
fn stack( fn stack(
p: &Preparation,
ctx: &mut Context, ctx: &mut Context,
lines: &[Line], lines: &[Line],
regions: &Regions, regions: &Regions,
styles: StyleChain,
) -> TypResult<Vec<Arc<Frame>>> { ) -> TypResult<Vec<Arc<Frame>>> {
let leading = styles.get(ParNode::LEADING);
let align = styles.get(ParNode::ALIGN);
let justify = styles.get(ParNode::JUSTIFY);
// Determine the paragraph's width: Full width of the region if we // Determine the paragraph's width: Full width of the region if we
// 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;
@ -1004,7 +1025,7 @@ 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 {
let frame = commit(ctx, line, &regions, width, styles, align, justify)?; let frame = commit(p, ctx, line, &regions, width)?;
let height = frame.size.y; let height = frame.size.y;
while !regions.first.y.fits(height) && !regions.in_last() { while !regions.first.y.fits(height) && !regions.in_last() {
@ -1015,14 +1036,14 @@ fn stack(
} }
if !first { if !first {
output.size.y += leading; output.size.y += p.leading;
} }
let pos = Point::with_y(output.size.y); let pos = Point::with_y(output.size.y);
output.size.y += height; output.size.y += height;
output.merge_frame(pos, frame); output.merge_frame(pos, frame);
regions.first.y -= height + leading; regions.first.y -= height + p.leading;
first = false; first = false;
} }
@ -1032,13 +1053,11 @@ fn stack(
/// Commit to a line and build its frame. /// Commit to a line and build its frame.
fn commit( fn commit(
p: &Preparation,
ctx: &mut Context, ctx: &mut Context,
line: &Line, line: &Line,
regions: &Regions, regions: &Regions,
width: Length, width: Length,
styles: StyleChain,
align: Align,
justify: bool,
) -> TypResult<Frame> { ) -> TypResult<Frame> {
let mut remaining = width - line.width; let mut remaining = width - line.width;
let mut offset = Length::zero(); let mut offset = Length::zero();
@ -1077,7 +1096,7 @@ fn commit(
let fr = line.fr(); let fr = line.fr();
let mut justification = Length::zero(); let mut justification = Length::zero();
if remaining < Length::zero() if remaining < Length::zero()
|| (justify && line.justify && line.end < line.bidi.text.len() && fr.is_zero()) || (p.justify && line.justify && line.end < line.bidi.text.len() && fr.is_zero())
{ {
let justifiables = line.justifiables(); let justifiables = line.justifiables();
if justifiables > 0 { if justifiables > 0 {
@ -1113,17 +1132,17 @@ fn commit(
Item::Frame(frame) => { Item::Frame(frame) => {
push(&mut offset, frame.clone()); push(&mut offset, frame.clone());
} }
Item::Repeat(node) => { Item::Repeat(node, styles) => {
let before = offset; let before = offset;
let width = Fraction::one().share(fr, remaining); let width = Fraction::one().share(fr, remaining);
let size = Size::new(width, regions.base.y); let size = Size::new(width, regions.base.y);
let pod = Regions::one(size, regions.base, Spec::new(false, false)); let pod = Regions::one(size, regions.base, Spec::new(false, false));
let frame = node.layout(ctx, &pod, styles)?.remove(0); let frame = node.layout(ctx, &pod, *styles)?.remove(0);
let count = (width / frame.size.x).floor(); let count = (width / frame.size.x).floor();
let remaining = width % frame.size.x; let remaining = width % frame.size.x;
let apart = remaining / (count - 1.0); let apart = remaining / (count - 1.0);
if count == 1.0 { if count == 1.0 {
offset += align.position(remaining); offset += p.align.position(remaining);
} }
if frame.size.x > Length::zero() { if frame.size.x > Length::zero() {
for _ in 0 .. (count as usize).min(1000) { for _ in 0 .. (count as usize).min(1000) {
@ -1147,7 +1166,7 @@ fn commit(
// Construct the line's frame. // Construct the line's frame.
for (offset, frame) in frames { for (offset, frame) in frames {
let x = offset + align.position(remaining); let x = offset + p.align.position(remaining);
let y = top - frame.baseline(); let y = top - frame.baseline();
output.merge_frame(Point::new(x, y), frame); output.merge_frame(Point::new(x, y), frame);
} }