summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-04-18 13:08:00 +0200
committerLaurenz <laurmaedje@gmail.com>2022-04-18 13:10:40 +0200
commitf27f7a05ab24e57da99af9b4b96bab82bb31276a (patch)
tree22176e133929196d50dd2a7df5941ef1a376f9f7 /src
parenta302105b9fda249755f6b1b27e010d32587f4946 (diff)
Tidy paragraph layout
Diffstat (limited to 'src')
-rw-r--r--src/library/text/par.rs185
1 files changed, 102 insertions, 83 deletions
diff --git a/src/library/text/par.rs b/src/library/text/par.rs
index 6e8b5917..9f45a411 100644
--- a/src/library/text/par.rs
+++ b/src/library/text/par.rs
@@ -76,7 +76,7 @@ impl Layout for ParNode {
let lines = linebreak(&p, &mut ctx.fonts, regions.first.x);
// 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>>,
/// The styles shared by all children.
styles: StyleChain<'a>,
- /// The paragraph's children.
- children: &'a StyleVec<ParChild>,
+ /// 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>,
+ /// 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> {
@@ -240,15 +248,6 @@ impl<'a> Preparation<'a> {
(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.
@@ -286,7 +285,7 @@ enum Item<'a> {
/// A layouted child node.
Frame(Frame),
/// A repeating node.
- Repeat(&'a RepeatNode),
+ Repeat(&'a RepeatNode, StyleChain<'a>),
}
impl<'a> Item<'a> {
@@ -303,7 +302,7 @@ impl<'a> Item<'a> {
match self {
Self::Text(shaped) => shaped.text.len(),
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 {
Item::Text(shaped) => shaped.width,
Item::Absolute(v) => *v,
- Item::Fractional(_) | Self::Repeat(_) => Length::zero(),
+ Item::Fractional(_) | Self::Repeat(_, _) => Length::zero(),
Item::Frame(frame) => frame.size.x,
}
}
@@ -403,7 +402,7 @@ impl<'a> Line<'a> {
self.items()
.filter_map(|item| match item {
Item::Fractional(fr) => Some(*fr),
- Item::Repeat(_) => Some(Fraction::one()),
+ Item::Repeat(_, _) => Some(Fraction::one()),
_ => None,
})
.sum()
@@ -505,38 +504,7 @@ fn prepare<'a>(
let end = cursor + segment.len();
match segment {
Segment::Text(_) => {
- let mut process = |text, level: Level| {
- 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);
+ shape_range(&mut items, &mut ctx.fonts, &bidi, cursor .. end, styles);
}
Segment::Spacing(spacing) => match spacing {
Spacing::Relative(v) => {
@@ -549,7 +517,7 @@ fn prepare<'a>(
},
Segment::Node(node) => {
if let Some(repeat) = node.downcast() {
- items.push(Item::Repeat(repeat));
+ items.push(Item::Repeat(repeat, styles));
} else {
let size = Size::new(regions.first.x, regions.base.y);
let pod = Regions::one(size, regions.base, Spec::splat(false));
@@ -562,7 +530,60 @@ fn prepare<'a>(
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.
@@ -575,6 +596,16 @@ fn is_compatible(a: Script, b: Script) -> bool {
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.
fn linebreak<'a>(
p: &'a Preparation<'a>,
@@ -672,9 +703,6 @@ fn linebreak_optimized<'a>(
const MIN_COST: Cost = -MAX_COST;
const MIN_RATIO: f64 = -0.15;
- let em = p.styles.get(TextNode::SIZE);
- let justify = p.styles.get(ParNode::JUSTIFY);
-
// Dynamic programming table.
let mut active = 0;
let mut table = vec![Entry {
@@ -683,6 +711,8 @@ fn linebreak_optimized<'a>(
line: line(p, fonts, 0 .. 0, false, false),
}];
+ let em = p.styles.get(TextNode::SIZE);
+
for (end, mandatory, hyphen) in breakpoints(p) {
let k = table.len();
let eof = end == p.bidi.text.len();
@@ -706,7 +736,7 @@ fn linebreak_optimized<'a>(
ratio = ratio.min(10.0);
// 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
// - justification is on, but we'd need to shrink to much
// - justification is off and the line just doesn't fit
@@ -779,8 +809,6 @@ fn breakpoints<'a>(p: &'a Preparation) -> Breakpoints<'a> {
suffix: 0,
end: 0,
mandatory: false,
- hyphenate: p.get_shared(TextNode::HYPHENATE),
- lang: p.get_shared(TextNode::LANG),
}
}
@@ -800,10 +828,6 @@ struct Breakpoints<'a> {
end: usize,
/// Whether the break after the current word is mandatory.
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<'_> {
@@ -820,7 +844,7 @@ impl Iterator for Breakpoints<'_> {
// Filter out hyphenation opportunities where hyphenation was
// actually disabled.
let hyphen = self.offset < self.end;
- if hyphen && !self.hyphenate_at(self.offset) {
+ if hyphen && !self.hyphenate(self.offset) {
return self.next();
}
@@ -831,8 +855,8 @@ impl Iterator for Breakpoints<'_> {
(self.end, self.mandatory) = self.linebreaks.next()?;
// Hyphenate the next word.
- if self.hyphenate != Some(false) {
- if let Some(lang) = self.lang_at(self.offset) {
+ if self.p.hyphenate != Some(false) {
+ if let Some(lang) = self.lang(self.offset) {
let word = &self.p.bidi.text[self.offset .. self.end];
let trimmed = word.trim_end_matches(|c: char| !c.is_alphabetic());
if !trimmed.is_empty() {
@@ -850,8 +874,9 @@ impl Iterator for Breakpoints<'_> {
impl Breakpoints<'_> {
/// Whether hyphenation is enabled at the given offset.
- fn hyphenate_at(&self, offset: usize) -> bool {
- self.hyphenate
+ fn hyphenate(&self, offset: usize) -> bool {
+ self.p
+ .hyphenate
.or_else(|| {
let shaped = self.p.find(offset)?.text()?;
Some(shaped.styles.get(TextNode::HYPHENATE))
@@ -860,8 +885,8 @@ impl Breakpoints<'_> {
}
/// The text language at the given offset.
- fn lang_at(&self, offset: usize) -> Option<hypher::Lang> {
- let lang = self.lang.or_else(|| {
+ fn lang(&self, offset: usize) -> Option<hypher::Lang> {
+ let lang = self.p.lang.or_else(|| {
let shaped = self.p.find(offset)?.text()?;
Some(shaped.styles.get(TextNode::LANG))
})?;
@@ -980,15 +1005,11 @@ fn line<'a>(
/// Combine layouted lines into one frame per region.
fn stack(
+ p: &Preparation,
ctx: &mut Context,
lines: &[Line],
regions: &Regions,
- styles: StyleChain,
) -> 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
// should expand or there's fractional spacing, fit-to-width otherwise.
let mut width = regions.first.x;
@@ -1004,7 +1025,7 @@ fn stack(
// Stack the lines into one frame per region.
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;
while !regions.first.y.fits(height) && !regions.in_last() {
@@ -1015,14 +1036,14 @@ fn stack(
}
if !first {
- output.size.y += leading;
+ output.size.y += p.leading;
}
let pos = Point::with_y(output.size.y);
output.size.y += height;
output.merge_frame(pos, frame);
- regions.first.y -= height + leading;
+ regions.first.y -= height + p.leading;
first = false;
}
@@ -1032,13 +1053,11 @@ fn stack(
/// Commit to a line and build its frame.
fn commit(
+ p: &Preparation,
ctx: &mut Context,
line: &Line,
regions: &Regions,
width: Length,
- styles: StyleChain,
- align: Align,
- justify: bool,
) -> TypResult<Frame> {
let mut remaining = width - line.width;
let mut offset = Length::zero();
@@ -1077,7 +1096,7 @@ fn commit(
let fr = line.fr();
let mut justification = 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();
if justifiables > 0 {
@@ -1113,17 +1132,17 @@ fn commit(
Item::Frame(frame) => {
push(&mut offset, frame.clone());
}
- Item::Repeat(node) => {
+ Item::Repeat(node, styles) => {
let before = offset;
let width = Fraction::one().share(fr, remaining);
let size = Size::new(width, regions.base.y);
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 remaining = width % frame.size.x;
let apart = remaining / (count - 1.0);
if count == 1.0 {
- offset += align.position(remaining);
+ offset += p.align.position(remaining);
}
if frame.size.x > Length::zero() {
for _ in 0 .. (count as usize).min(1000) {
@@ -1147,7 +1166,7 @@ fn commit(
// Construct the line's frame.
for (offset, frame) in frames {
- let x = offset + align.position(remaining);
+ let x = offset + p.align.position(remaining);
let y = top - frame.baseline();
output.merge_frame(Point::new(x, y), frame);
}