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> {
/// 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>> {
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;
for (idx, item) in self.items.iter().enumerate() {
for item in &self.items {
let end = cursor + item.len();
if (cursor .. end).contains(&text_offset) {
return Some((idx, cursor));
return Some(item);
}
cursor = end;
}
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
/// paragraph.
fn get_shared<K: Key<'a>>(&self, key: K) -> Option<K::Output> {
@ -307,12 +330,14 @@ impl<'a> Item<'a> {
struct Line<'a> {
/// Bidi information about the paragraph.
bidi: &'a BidiInfo<'a>,
/// The (untrimmed) range the line spans in the paragraph.
range: Range,
/// The trimmed range the line spans in the paragraph.
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.
first: Option<Item<'a>>,
/// Middle items which don't need to be reprocessed.
items: &'a [Item<'a>],
/// Inner items which don't need to be reprocessed.
inner: &'a [Item<'a>],
/// 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`.
last: Option<Item<'a>>,
@ -328,29 +353,31 @@ struct Line<'a> {
impl<'a> Line<'a> {
/// Iterate over the line's items.
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.
fn get(&self, index: usize) -> Option<&Item<'a>> {
self.items().nth(index)
}
/// Return items that intersect the given `text_range`.
fn slice(&self, text_range: Range) -> impl Iterator<Item = &Item<'a>> {
let mut cursor = self.trimmed.start;
let mut start = 0;
let mut end = 0;
/// Find the index of the item whose range contains the `text_offset`.
fn find(&self, text_offset: usize) -> usize {
let mut idx = 0;
let mut cursor = self.range.start;
for item in self.items() {
let end = cursor + item.len();
if (cursor .. end).contains(&text_offset) {
return idx;
for (i, item) in self.items().enumerate() {
if cursor <= text_range.start {
start = i;
}
cursor = end;
idx += 1;
let len = item.len();
if cursor < text_range.end || cursor + len <= text_range.end {
end = i + 1;
} else {
break;
}
cursor += len;
}
idx.saturating_sub(1)
self.items().skip(start).take(end - start)
}
// How many justifiable glyphs the line contains.
@ -664,7 +691,7 @@ fn linebreak_optimized<'a>(
// Find the optimal predecessor.
for (i, pred) in table.iter_mut().enumerate().skip(active) {
// Layout the line.
let start = pred.line.range.end;
let start = pred.line.end;
let attempt = line(p, fonts, start .. end, mandatory, hyphen);
// Determine how much the line's spaces would need to be stretched
@ -848,16 +875,17 @@ impl Breakpoints<'_> {
fn line<'a>(
p: &'a Preparation,
fonts: &mut FontStore,
range: Range,
mut range: Range,
mandatory: bool,
hyphen: bool,
) -> Line<'a> {
if range.is_empty() {
return Line {
bidi: &p.bidi,
range,
end: range.end,
trimmed: range,
first: None,
items: &[],
inner: &[],
last: None,
width: Length::zero(),
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.
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();
// 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 dash = false;
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
// end of the line.
let base = last_offset;
let start = range.start.max(last_offset);
let end = range.end;
let text = &p.bidi.text[start .. end];
let base = expanded.end - shaped.text.len();
let start = range.start.max(base);
let text = &p.bidi.text[start .. range.end];
let trimmed = text.trim_end();
range.end = start + trimmed.len();
// Deal with hyphens, dashes and justification.
let shy = trimmed.ends_with('\u{ad}');
dash = hyphen || shy || trimmed.ends_with(['-', '', '—']);
justify |= text.ends_with('\u{2028}');
@ -905,10 +925,9 @@ fn line<'a>(
// need the shaped empty string to make the line the appropriate
// height. That is the case exactly if the string is empty and there
// are no other items in the line.
if hyphen || trimmed.len() < shaped.text.len() {
if hyphen || !trimmed.is_empty() || before.is_empty() {
let end = start + trimmed.len();
let shifted = start - base .. end - base;
if hyphen || start + shaped.text.len() > range.end {
if hyphen || start < range.end || before.is_empty() {
let shifted = start - base .. range.end - base;
let mut reshaped = shaped.reshape(fonts, shifted);
if hyphen || shy {
reshaped.push_hyphen(fonts);
@ -917,41 +936,41 @@ fn line<'a>(
last = Some(Item::Text(reshaped));
}
items = before;
inner = before;
}
}
// Reshape the start item if it's split in half.
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.
let base = first_offset;
let start = range.start;
let end = range.end.min(first_offset + shaped.text.len());
let base = expanded.start;
let end = range.end.min(base + shaped.text.len());
// Reshape if necessary.
if end - start < shaped.text.len() {
if start < end {
let shifted = start - base .. end - base;
if range.start + shaped.text.len() > end {
if range.start < end {
let shifted = range.start - base .. end - base;
let reshaped = shaped.reshape(fonts, shifted);
width += reshaped.width;
first = Some(Item::Text(reshaped));
}
items = after;
inner = after;
}
}
// Measure the inner items.
for item in items {
for item in inner {
width += item.width();
}
Line {
bidi: &p.bidi,
range,
trimmed: range,
end,
first,
items,
inner,
last,
width,
justify,
@ -1058,10 +1077,7 @@ fn commit(
let fr = line.fr();
let mut justification = Length::zero();
if remaining < Length::zero()
|| (justify
&& line.justify
&& line.range.end < line.bidi.text.len()
&& fr.is_zero())
|| (justify && line.justify && line.end < line.bidi.text.len() && fr.is_zero())
{
let justifiables = line.justifiables();
if justifiables > 0 {
@ -1104,7 +1120,11 @@ fn commit(
let pod = Regions::one(size, regions.base, Spec::new(false, false));
let frame = node.layout(ctx, &pod, styles)?.remove(0);
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() {
for _ in 0 .. (count as usize).min(1000) {
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 mut output = Frame::new(size);
output.baseline = Some(top);
@ -1131,12 +1156,12 @@ fn commit(
}
/// 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![];
// The bidi crate doesn't like empty lines.
if line.range.is_empty() {
return reordered;
if line.trimmed.is_empty() {
return line.slice(line.trimmed.clone()).collect();
}
// Find the paragraph that contains the line.
@ -1144,24 +1169,25 @@ fn reorder<'a>(line: &'a Line<'a>) -> Vec<&'a Item<'a>> {
.bidi
.paragraphs
.iter()
.find(|para| para.range.contains(&line.range.start))
.find(|para| para.range.contains(&line.trimmed.start))
.unwrap();
// 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.
for run in runs {
let first_idx = line.find(run.start);
let last_idx = line.find(run.end - 1);
let range = first_idx ..= last_idx;
// Skip reset L1 runs because handling them would require reshaping
// again in some cases.
if line.bidi.levels[run.start] != levels[run.start] {
continue;
}
// Provide the items forwards or backwards depending on the run's
// direction.
if levels[run.start].is_ltr() {
reordered.extend(range.filter_map(|i| line.get(i)));
} else {
reordered.extend(range.rev().filter_map(|i| line.get(i)));
let prev = reordered.len();
reordered.extend(line.slice(run.clone()));
if levels[run.start].is_rtl() {
reordered[prev ..].reverse();
}
}

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.
#set par(align: right)
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.

View File

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

View File

@ -1,6 +1,7 @@
// Test the `repeat` function.
---
// Test multiple repeats.
#let sections = (
("Introduction", 1),
("Approach", 1),
@ -13,3 +14,26 @@
#for section in sections [
#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))سون