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