Fix duplicate RTL text and alignment + fr bugs

This commit is contained in:
Laurenz 2022-04-18 12:25:24 +02:00
parent db820ae9aa
commit a302105b9f
7 changed files with 149 additions and 88 deletions

View File

@ -199,25 +199,48 @@ struct Preparation<'a> {
} }
impl<'a> Preparation<'a> { impl<'a> Preparation<'a> {
/// Find the item which is at the `text_offset`. /// Find the item that contains the given `text_offset`.
fn find(&self, text_offset: usize) -> Option<&Item<'a>> { fn find(&self, text_offset: usize) -> Option<&Item<'a>> {
self.find_idx_and_offset(text_offset).map(|(idx, _)| &self.items[idx])
}
/// Find the index and text offset of the item which is at the
/// `text_offset`.
fn find_idx_and_offset(&self, text_offset: usize) -> Option<(usize, usize)> {
let mut cursor = 0; let mut cursor = 0;
for (idx, item) in self.items.iter().enumerate() { for item in &self.items {
let end = cursor + item.len(); let end = cursor + item.len();
if (cursor .. end).contains(&text_offset) { if (cursor .. end).contains(&text_offset) {
return Some((idx, cursor)); return Some(item);
} }
cursor = end; cursor = end;
} }
None None
} }
/// Return the items that intersect the given `text_range`.
///
/// Returns the expanded range around the items and the items.
fn slice(&self, text_range: Range) -> (Range, &[Item<'a>]) {
let mut cursor = 0;
let mut start = 0;
let mut end = 0;
let mut expanded = text_range.clone();
for (i, item) in self.items.iter().enumerate() {
if cursor <= text_range.start {
start = i;
expanded.start = cursor;
}
let len = item.len();
if cursor < text_range.end || cursor + len <= text_range.end {
end = i + 1;
expanded.end = cursor + len;
} else {
break;
}
cursor += len;
}
(expanded, &self.items[start .. end])
}
/// Get a style property, but only if it is the same for all children of the /// Get a style property, but only if it is the same for all children of the
/// paragraph. /// paragraph.
fn get_shared<K: Key<'a>>(&self, key: K) -> Option<K::Output> { fn get_shared<K: Key<'a>>(&self, key: K) -> Option<K::Output> {
@ -307,12 +330,14 @@ impl<'a> Item<'a> {
struct Line<'a> { struct Line<'a> {
/// Bidi information about the paragraph. /// Bidi information about the paragraph.
bidi: &'a BidiInfo<'a>, bidi: &'a BidiInfo<'a>,
/// The (untrimmed) range the line spans in the paragraph. /// The trimmed range the line spans in the paragraph.
range: Range, trimmed: Range,
/// The untrimmed end where the line ends.
end: usize,
/// A reshaped text item if the line sliced up a text item at the start. /// A reshaped text item if the line sliced up a text item at the start.
first: Option<Item<'a>>, first: Option<Item<'a>>,
/// Middle items which don't need to be reprocessed. /// Inner items which don't need to be reprocessed.
items: &'a [Item<'a>], inner: &'a [Item<'a>],
/// A reshaped text item if the line sliced up a text item at the end. If /// A reshaped text item if the line sliced up a text item at the end. If
/// there is only one text item, this takes precedence over `first`. /// there is only one text item, this takes precedence over `first`.
last: Option<Item<'a>>, last: Option<Item<'a>>,
@ -328,29 +353,31 @@ struct Line<'a> {
impl<'a> Line<'a> { impl<'a> Line<'a> {
/// Iterate over the line's items. /// Iterate over the line's items.
fn items(&self) -> impl Iterator<Item = &Item<'a>> { fn items(&self) -> impl Iterator<Item = &Item<'a>> {
self.first.iter().chain(self.items).chain(&self.last) self.first.iter().chain(self.inner).chain(&self.last)
} }
/// Get the item at the index. /// Return items that intersect the given `text_range`.
fn get(&self, index: usize) -> Option<&Item<'a>> { fn slice(&self, text_range: Range) -> impl Iterator<Item = &Item<'a>> {
self.items().nth(index) let mut cursor = self.trimmed.start;
let mut start = 0;
let mut end = 0;
for (i, item) in self.items().enumerate() {
if cursor <= text_range.start {
start = i;
} }
/// Find the index of the item whose range contains the `text_offset`. let len = item.len();
fn find(&self, text_offset: usize) -> usize { if cursor < text_range.end || cursor + len <= text_range.end {
let mut idx = 0; end = i + 1;
let mut cursor = self.range.start; } else {
break;
for item in self.items() {
let end = cursor + item.len();
if (cursor .. end).contains(&text_offset) {
return idx;
}
cursor = end;
idx += 1;
} }
idx.saturating_sub(1) cursor += len;
}
self.items().skip(start).take(end - start)
} }
// How many justifiable glyphs the line contains. // How many justifiable glyphs the line contains.
@ -664,7 +691,7 @@ fn linebreak_optimized<'a>(
// Find the optimal predecessor. // Find the optimal predecessor.
for (i, pred) in table.iter_mut().enumerate().skip(active) { for (i, pred) in table.iter_mut().enumerate().skip(active) {
// Layout the line. // Layout the line.
let start = pred.line.range.end; let start = pred.line.end;
let attempt = line(p, fonts, start .. end, mandatory, hyphen); let attempt = line(p, fonts, start .. end, mandatory, hyphen);
// Determine how much the line's spaces would need to be stretched // Determine how much the line's spaces would need to be stretched
@ -848,16 +875,17 @@ impl Breakpoints<'_> {
fn line<'a>( fn line<'a>(
p: &'a Preparation, p: &'a Preparation,
fonts: &mut FontStore, fonts: &mut FontStore,
range: Range, mut range: Range,
mandatory: bool, mandatory: bool,
hyphen: bool, hyphen: bool,
) -> Line<'a> { ) -> Line<'a> {
if range.is_empty() { if range.is_empty() {
return Line { return Line {
bidi: &p.bidi, bidi: &p.bidi,
range, end: range.end,
trimmed: range,
first: None, first: None,
items: &[], inner: &[],
last: None, last: None,
width: Length::zero(), width: Length::zero(),
justify: !mandatory, justify: !mandatory,
@ -865,33 +893,25 @@ fn line<'a>(
}; };
} }
// Find the last item.
let (last_idx, last_offset) =
p.find_idx_and_offset(range.end.saturating_sub(1)).unwrap();
// Find the first item.
let (first_idx, first_offset) = if range.is_empty() {
(last_idx, last_offset)
} else {
p.find_idx_and_offset(range.start).unwrap()
};
// Slice out the relevant items. // Slice out the relevant items.
let mut items = &p.items[first_idx ..= last_idx]; let end = range.end;
let (expanded, mut inner) = p.slice(range.clone());
let mut width = Length::zero(); let mut width = Length::zero();
// Reshape the last item if it's split in half. // Reshape the last item if it's split in half or hyphenated.
let mut last = None; let mut last = None;
let mut dash = false; let mut dash = false;
let mut justify = !mandatory; let mut justify = !mandatory;
if let Some((Item::Text(shaped), before)) = items.split_last() { if let Some((Item::Text(shaped), before)) = inner.split_last() {
// Compute the range we want to shape, trimming whitespace at the // Compute the range we want to shape, trimming whitespace at the
// end of the line. // end of the line.
let base = last_offset; let base = expanded.end - shaped.text.len();
let start = range.start.max(last_offset); let start = range.start.max(base);
let end = range.end; let text = &p.bidi.text[start .. range.end];
let text = &p.bidi.text[start .. end];
let trimmed = text.trim_end(); let trimmed = text.trim_end();
range.end = start + trimmed.len();
// Deal with hyphens, dashes and justification.
let shy = trimmed.ends_with('\u{ad}'); let shy = trimmed.ends_with('\u{ad}');
dash = hyphen || shy || trimmed.ends_with(['-', '', '—']); dash = hyphen || shy || trimmed.ends_with(['-', '', '—']);
justify |= text.ends_with('\u{2028}'); justify |= text.ends_with('\u{2028}');
@ -905,10 +925,9 @@ fn line<'a>(
// need the shaped empty string to make the line the appropriate // need the shaped empty string to make the line the appropriate
// height. That is the case exactly if the string is empty and there // height. That is the case exactly if the string is empty and there
// are no other items in the line. // are no other items in the line.
if hyphen || trimmed.len() < shaped.text.len() { if hyphen || start + shaped.text.len() > range.end {
if hyphen || !trimmed.is_empty() || before.is_empty() { if hyphen || start < range.end || before.is_empty() {
let end = start + trimmed.len(); let shifted = start - base .. range.end - base;
let shifted = start - base .. end - base;
let mut reshaped = shaped.reshape(fonts, shifted); let mut reshaped = shaped.reshape(fonts, shifted);
if hyphen || shy { if hyphen || shy {
reshaped.push_hyphen(fonts); reshaped.push_hyphen(fonts);
@ -917,41 +936,41 @@ fn line<'a>(
last = Some(Item::Text(reshaped)); last = Some(Item::Text(reshaped));
} }
items = before; inner = before;
} }
} }
// Reshape the start item if it's split in half. // Reshape the start item if it's split in half.
let mut first = None; let mut first = None;
if let Some((Item::Text(shaped), after)) = items.split_first() { if let Some((Item::Text(shaped), after)) = inner.split_first() {
// Compute the range we want to shape. // Compute the range we want to shape.
let base = first_offset; let base = expanded.start;
let start = range.start; let end = range.end.min(base + shaped.text.len());
let end = range.end.min(first_offset + shaped.text.len());
// Reshape if necessary. // Reshape if necessary.
if end - start < shaped.text.len() { if range.start + shaped.text.len() > end {
if start < end { if range.start < end {
let shifted = start - base .. end - base; let shifted = range.start - base .. end - base;
let reshaped = shaped.reshape(fonts, shifted); let reshaped = shaped.reshape(fonts, shifted);
width += reshaped.width; width += reshaped.width;
first = Some(Item::Text(reshaped)); first = Some(Item::Text(reshaped));
} }
items = after; inner = after;
} }
} }
// Measure the inner items. // Measure the inner items.
for item in items { for item in inner {
width += item.width(); width += item.width();
} }
Line { Line {
bidi: &p.bidi, bidi: &p.bidi,
range, trimmed: range,
end,
first, first,
items, inner,
last, last,
width, width,
justify, justify,
@ -1058,10 +1077,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 || (justify && line.justify && line.end < line.bidi.text.len() && fr.is_zero())
&& line.justify
&& line.range.end < line.bidi.text.len()
&& fr.is_zero())
{ {
let justifiables = line.justifiables(); let justifiables = line.justifiables();
if justifiables > 0 { if justifiables > 0 {
@ -1104,7 +1120,11 @@ fn commit(
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 apart = (width % frame.size.x) / (count - 1.0); let remaining = width % frame.size.x;
let apart = remaining / (count - 1.0);
if count == 1.0 {
offset += 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) {
push(&mut offset, frame.as_ref().clone()); push(&mut offset, frame.as_ref().clone());
@ -1116,6 +1136,11 @@ fn commit(
} }
} }
// Remaining space is distributed now.
if !fr.is_zero() {
remaining = Length::zero();
}
let size = Size::new(width, top + bottom); let size = Size::new(width, top + bottom);
let mut output = Frame::new(size); let mut output = Frame::new(size);
output.baseline = Some(top); output.baseline = Some(top);
@ -1131,12 +1156,12 @@ fn commit(
} }
/// Return a line's items in visual order. /// Return a line's items in visual order.
fn reorder<'a>(line: &'a Line<'a>) -> Vec<&'a Item<'a>> { fn reorder<'a>(line: &'a Line<'a>) -> Vec<&Item<'a>> {
let mut reordered = vec![]; let mut reordered = vec![];
// The bidi crate doesn't like empty lines. // The bidi crate doesn't like empty lines.
if line.range.is_empty() { if line.trimmed.is_empty() {
return reordered; return line.slice(line.trimmed.clone()).collect();
} }
// Find the paragraph that contains the line. // Find the paragraph that contains the line.
@ -1144,24 +1169,25 @@ fn reorder<'a>(line: &'a Line<'a>) -> Vec<&'a Item<'a>> {
.bidi .bidi
.paragraphs .paragraphs
.iter() .iter()
.find(|para| para.range.contains(&line.range.start)) .find(|para| para.range.contains(&line.trimmed.start))
.unwrap(); .unwrap();
// Compute the reordered ranges in visual order (left to right). // Compute the reordered ranges in visual order (left to right).
let (levels, runs) = line.bidi.visual_runs(para, line.range.clone()); let (levels, runs) = line.bidi.visual_runs(para, line.trimmed.clone());
// Collect the reordered items. // Collect the reordered items.
for run in runs { for run in runs {
let first_idx = line.find(run.start); // Skip reset L1 runs because handling them would require reshaping
let last_idx = line.find(run.end - 1); // again in some cases.
let range = first_idx ..= last_idx; if line.bidi.levels[run.start] != levels[run.start] {
continue;
}
// Provide the items forwards or backwards depending on the run's let prev = reordered.len();
// direction. reordered.extend(line.slice(run.clone()));
if levels[run.start].is_ltr() {
reordered.extend(range.filter_map(|i| line.get(i))); if levels[run.start].is_rtl() {
} else { reordered[prev ..].reverse();
reordered.extend(range.rev().filter_map(|i| line.get(i)));
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -21,7 +21,14 @@ Add #h(10pt) #h(10pt) up
// Test spacing collapsing before spacing. // Test spacing collapsing before spacing.
#set par(align: right) #set par(align: right)
A #h(0pt) B #h(0pt) \ A #h(0pt) B #h(0pt) \
A B A B \
A #h(-1fr) B
---
// Test RTL spacing.
#set text(dir: rtl)
A #h(10pt) B \
A #h(1fr) B
--- ---
// Missing spacing. // Missing spacing.

View File

@ -45,6 +45,10 @@ Lריווח #h(1cm) R
#set text(lang: "he", "IBM Plex Serif") #set text(lang: "he", "IBM Plex Serif")
קרנפיםRh#image("../../res/rhino.png", height: 11pt)inoחיים קרנפיםRh#image("../../res/rhino.png", height: 11pt)inoחיים
---
// Test whether L1 whitespace resetting destroys stuff.
الغالب #h(70pt) ن{" "}ة
--- ---
// Test setting a vertical direction. // Test setting a vertical direction.
// Ref: false // Ref: false

View File

@ -1,6 +1,7 @@
// Test the `repeat` function. // Test the `repeat` function.
--- ---
// Test multiple repeats.
#let sections = ( #let sections = (
("Introduction", 1), ("Introduction", 1),
("Approach", 1), ("Approach", 1),
@ -13,3 +14,26 @@
#for section in sections [ #for section in sections [
#section(0) #repeat[.] #section(1) \ #section(0) #repeat[.] #section(1) \
] ]
---
// Test dots with RTL.
#set text(lang: "ar")
مقدمة #repeat[.] 15
---
// Test empty repeat.
A #repeat[] B
---
// Test spaceless repeat.
A#repeat(rect(width: 2.5em, height: 1em))B
---
// Test single repeat in both directions.
A#repeat(rect(width: 6em, height: 0.7em))B
#set par(align: center)
A#repeat(rect(width: 6em, height: 0.7em))B
#set text(dir: rtl)
ريجين#repeat(rect(width: 4em, height: 0.7em))سون