diff --git a/Cargo.toml b/Cargo.toml
index e0d0d77d2..4eef9f8cb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,6 +28,7 @@ miniz_oxide = "0.3"
pdf-writer = { path = "../pdf-writer" }
rustybuzz = "0.3"
ttf-parser = "0.9"
+unicode-bidi = "0.3"
unicode-xid = "0.2"
xi-unicode = "0.3"
anyhow = { version = "1", optional = true }
diff --git a/src/layout/par.rs b/src/layout/par.rs
index 6a226fb4d..de3b1bab7 100644
--- a/src/layout/par.rs
+++ b/src/layout/par.rs
@@ -1,6 +1,8 @@
use std::fmt::{self, Debug, Formatter};
use std::mem;
+use std::ops::Range;
+use unicode_bidi::{BidiInfo, Level};
use xi_unicode::LineBreakIterator;
use super::*;
@@ -29,18 +31,6 @@ pub enum ParChild {
Any(AnyNode, Align),
}
-impl Debug for ParChild {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- match self {
- Self::Spacing(amount) => write!(f, "Spacing({:?})", amount),
- Self::Text(node, align) => write!(f, "Text({:?}, {:?})", node.text, align),
- Self::Any(any, align) => {
- f.debug_tuple("Any").field(any).field(align).finish()
- }
- }
- }
-}
-
/// A consecutive, styled run of text.
#[derive(Clone, PartialEq)]
pub struct TextNode {
@@ -50,26 +40,73 @@ pub struct TextNode {
pub props: FontProps,
}
-impl Debug for TextNode {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- write!(f, "Text({:?})", self.text)
- }
-}
-
impl Layout for ParNode {
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec {
- let mut layouter = ParLayouter::new(self.dir, self.line_spacing, areas.clone());
+ let mut text = String::new();
+ let mut ranges = vec![];
+
for child in &self.children {
+ let start = text.len();
+ match child {
+ ParChild::Spacing(_) => text.push(' '),
+ ParChild::Text(node, _) => text.push_str(&node.text),
+ ParChild::Any(_, _) => text.push('\u{FFFC}'),
+ }
+ ranges.push(start .. text.len());
+ }
+
+ let level = match self.dir {
+ Dir::LTR => Level::ltr(),
+ Dir::RTL => Level::rtl(),
+ _ => panic!("invalid paragraph direction"),
+ };
+
+ let bidi = BidiInfo::new(&text, Some(level));
+ let mut layouter =
+ ParLayouter::new(self.dir, self.line_spacing, &bidi, areas.clone());
+
+ for (range, child) in ranges.into_iter().zip(&self.children) {
match *child {
- ParChild::Spacing(amount) => layouter.push_spacing(amount),
- ParChild::Text(ref node, align) => layouter.push_text(ctx, node, align),
+ ParChild::Spacing(amount) => {
+ layouter.push_spacing(range, amount);
+ }
+ ParChild::Text(ref node, align) => {
+ let mut start = range.start;
+ let mut last = None;
+ for (idx, level) in bidi.levels[range.clone()].iter().enumerate() {
+ let idx = range.start + idx;
+
+ if last.map_or(false, |last| last != level) {
+ // Push the text up until `idx` (exclusively).
+ layouter.push_text(
+ ctx,
+ start .. idx,
+ &text[start .. idx],
+ &node.props,
+ align,
+ );
+ start = idx;
+ }
+
+ last = Some(level);
+ }
+
+ layouter.push_text(
+ ctx,
+ start .. range.end,
+ &text[start .. range.end],
+ &node.props,
+ align,
+ );
+ }
ParChild::Any(ref node, align) => {
for frame in node.layout(ctx, &layouter.areas) {
- layouter.push_frame(frame, align);
+ layouter.push_frame(range.clone(), frame, align);
}
}
}
}
+
layouter.finish()
}
}
@@ -80,65 +117,92 @@ impl From for AnyNode {
}
}
-struct ParLayouter {
+struct ParLayouter<'a> {
dir: Dir,
line_spacing: Length,
+ bidi: &'a BidiInfo<'a>,
areas: Areas,
finished: Vec,
stack: Vec<(Length, Frame, Align)>,
stack_size: Size,
- line: Vec<(Length, Frame, Align)>,
- line_size: Size,
- line_ruler: Align,
+ line: Line,
hard: bool,
}
-impl ParLayouter {
- fn new(dir: Dir, line_spacing: Length, areas: Areas) -> Self {
+struct Line {
+ items: Vec,
+ size: Size,
+ ruler: Align,
+}
+
+struct LineItem {
+ range: Range,
+ frame: Frame,
+ align: Align,
+}
+
+impl<'a> ParLayouter<'a> {
+ fn new(dir: Dir, line_spacing: Length, bidi: &'a BidiInfo<'a>, areas: Areas) -> Self {
Self {
dir,
line_spacing,
+ bidi,
areas,
finished: vec![],
stack: vec![],
stack_size: Size::ZERO,
- line: vec![],
- line_size: Size::ZERO,
- line_ruler: Align::Start,
+ line: Line {
+ items: vec![],
+ size: Size::ZERO,
+ ruler: Align::Start,
+ },
hard: true,
}
}
- fn push_spacing(&mut self, amount: Length) {
- let max = self.areas.current.width;
- self.line_size.width = (self.line_size.width + amount).min(max);
+ fn push_spacing(&mut self, range: Range, amount: Length) {
+ let amount = amount.min(self.areas.current.width - self.line.size.width);
+ self.line.size.width += amount;
+ self.line.items.push(LineItem {
+ range,
+ frame: Frame::new(Size::new(amount, Length::ZERO)),
+ align: Align::default(),
+ })
}
- fn push_text(&mut self, ctx: &mut LayoutContext, node: &TextNode, align: Align) {
+ fn push_text(
+ &mut self,
+ ctx: &mut LayoutContext,
+ range: Range,
+ text: &str,
+ props: &FontProps,
+ align: Align,
+ ) {
// Position in the text at which the current line starts.
- let mut start = 0;
+ let mut start = range.start;
// The current line attempt: Text shaped up to the previous line break
// opportunity.
let mut last = None;
- let mut iter = LineBreakIterator::new(&node.text).peekable();
+ let mut iter = LineBreakIterator::new(text).peekable();
while let Some(&(pos, mandatory)) = iter.peek() {
- let line = &node.text[start .. pos];
+ let line = &text[start - range.start .. pos];
// Remove trailing newline and spacing at the end of lines.
let mut line = line.trim_end_matches(is_newline);
- if pos != node.text.len() {
+ if pos != text.len() {
line = line.trim_end();
}
- let frame = shape(line, &mut ctx.env.fonts, &node.props);
+ let pos = range.start + pos;
+ let frame = shape(line, &mut ctx.env.fonts, props);
if self.usable().fits(frame.size) {
// Still fits into the line.
if mandatory {
// We have to break here.
- self.push_frame(frame, align);
+ self.push_frame(start .. pos, frame, align);
self.finish_line(true);
start = pos;
last = None;
@@ -149,7 +213,7 @@ impl ParLayouter {
// The line start..pos doesn't fit. So we write the line up to
// the last position and retry writing just the single piece
// behind it.
- self.push_frame(frame, align);
+ self.push_frame(start .. pos, frame, align);
self.finish_line(false);
start = pos;
continue;
@@ -157,7 +221,7 @@ impl ParLayouter {
// Since last is `None`, we are at the first piece behind a line
// break and it still doesn't fit. Since we can't break it up
// further, so we just have to push it.
- self.push_frame(frame, align);
+ self.push_frame(start .. pos, frame, align);
self.finish_line(false);
start = pos;
}
@@ -166,12 +230,12 @@ impl ParLayouter {
}
// Leftovers.
- if let Some((frame, _)) = last {
- self.push_frame(frame, align);
+ if let Some((frame, pos)) = last {
+ self.push_frame(start .. pos, frame, align);
}
}
- fn push_frame(&mut self, frame: Frame, align: Align) {
+ fn push_frame(&mut self, range: Range, frame: Frame, align: Align) {
// When the alignment of the last pushed frame (stored in the "ruler")
// is further to the end than the new `frame`, we need a line break.
//
@@ -184,12 +248,12 @@ impl ParLayouter {
// | First |
// | Second |
// +----------------------------+
- if self.line_ruler > align {
+ if self.line.ruler > align {
self.finish_line(false);
}
// Find out whether the area still has enough space for this frame.
- if !self.usable().fits(frame.size) && self.line_size.width > Length::ZERO {
+ if !self.usable().fits(frame.size) && self.line.size.width > Length::ZERO {
self.finish_line(false);
// Here, we can directly check whether the frame fits into
@@ -209,10 +273,10 @@ impl ParLayouter {
// A line can contain frames with different alignments. Their exact
// positions are calculated later depending on the alignments.
let size = frame.size;
- self.line.push((self.line_size.width, frame, align));
- self.line_size.width += size.width;
- self.line_size.height = self.line_size.height.max(size.height);
- self.line_ruler = align;
+ self.line.items.push(LineItem { range, frame, align });
+ self.line.size.width += size.width;
+ self.line.size.height = self.line.size.height.max(size.height);
+ self.line.ruler = align;
}
fn usable(&self) -> Size {
@@ -220,37 +284,60 @@ impl ParLayouter {
// `areas.current`, but the width of the current line needs to be
// subtracted to make sure the frame fits.
let mut usable = self.areas.current;
- usable.width -= self.line_size.width;
+ usable.width -= self.line.size.width;
usable
}
fn finish_line(&mut self, hard: bool) {
- if !mem::replace(&mut self.hard, hard) && self.line.is_empty() {
+ if !mem::replace(&mut self.hard, hard) && self.line.items.is_empty() {
return;
}
+ let mut items = mem::take(&mut self.line.items);
+ if let (Some(first), Some(last)) = (items.first(), items.last()) {
+ let range = first.range.start .. last.range.end;
+ let para = self
+ .bidi
+ .paragraphs
+ .iter()
+ .find(|para| para.range.contains(&range.start))
+ .unwrap();
+
+ let (levels, ranges) = self.bidi.visual_runs(¶, range);
+
+ items.sort_by_key(|item| {
+ let start = item.range.start;
+ let idx = ranges.iter().position(|r| r.contains(&start)).unwrap();
+ let ltr = levels[start].is_ltr();
+ let sec = start as isize * if ltr { 1 } else { -1 };
+ (idx, sec)
+ });
+ }
+
let full_size = {
let expand = self.areas.expand.horizontal;
let full = self.areas.full.width;
Size::new(
- expand.resolve(self.line_size.width, full),
- self.line_size.height,
+ expand.resolve(self.line.size.width, full),
+ self.line.size.height,
)
};
let mut output = Frame::new(full_size);
- for (before, frame, align) in mem::take(&mut self.line) {
+ let mut offset = Length::ZERO;
+
+ for item in items {
// Align along the x axis.
- let x = align.resolve(if self.dir.is_positive() {
- before .. full_size.width - self.line_size.width + before
+ let x = item.align.resolve(if self.dir.is_positive() {
+ offset .. full_size.width - self.line.size.width + offset
} else {
- let before_with_self = before + frame.size.width;
- full_size.width - before_with_self
- .. self.line_size.width - before_with_self
+ full_size.width - self.line.size.width + offset .. offset
});
+ offset += item.frame.size.width;
+
let pos = Point::new(x, Length::ZERO);
- output.push_frame(pos, frame);
+ output.push_frame(pos, item.frame);
}
// Add line spacing, but only between lines, not after the last line.
@@ -259,12 +346,12 @@ impl ParLayouter {
self.areas.current.height -= self.line_spacing;
}
- self.stack.push((self.stack_size.height, output, self.line_ruler));
+ self.stack.push((self.stack_size.height, output, self.line.ruler));
self.stack_size.height += full_size.height;
self.stack_size.width = self.stack_size.width.max(full_size.width);
self.areas.current.height -= full_size.height;
- self.line_size = Size::ZERO;
- self.line_ruler = Align::Start;
+ self.line.size = Size::ZERO;
+ self.line.ruler = Align::Start;
}
fn finish_area(&mut self) {
@@ -292,3 +379,21 @@ impl ParLayouter {
self.finished
}
}
+
+impl Debug for ParChild {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ match self {
+ Self::Spacing(amount) => write!(f, "Spacing({:?})", amount),
+ Self::Text(node, align) => write!(f, "Text({:?}, {:?})", node.text, align),
+ Self::Any(any, align) => {
+ f.debug_tuple("Any").field(any).field(align).finish()
+ }
+ }
+ }
+}
+
+impl Debug for TextNode {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "Text({:?})", self.text)
+ }
+}
diff --git a/tests/ref/text/shaping.png b/tests/ref/text/shaping.png
index 9af49f160..88e7b0ad3 100644
Binary files a/tests/ref/text/shaping.png and b/tests/ref/text/shaping.png differ