mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
BiDi reordering 🔃
Co-Authored-By: Martin <mhaug@live.de>
This commit is contained in:
parent
47b4dab7ed
commit
79948c0c5e
@ -28,6 +28,7 @@ miniz_oxide = "0.3"
|
|||||||
pdf-writer = { path = "../pdf-writer" }
|
pdf-writer = { path = "../pdf-writer" }
|
||||||
rustybuzz = "0.3"
|
rustybuzz = "0.3"
|
||||||
ttf-parser = "0.9"
|
ttf-parser = "0.9"
|
||||||
|
unicode-bidi = "0.3"
|
||||||
unicode-xid = "0.2"
|
unicode-xid = "0.2"
|
||||||
xi-unicode = "0.3"
|
xi-unicode = "0.3"
|
||||||
anyhow = { version = "1", optional = true }
|
anyhow = { version = "1", optional = true }
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use std::fmt::{self, Debug, Formatter};
|
use std::fmt::{self, Debug, Formatter};
|
||||||
use std::mem;
|
use std::mem;
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use unicode_bidi::{BidiInfo, Level};
|
||||||
use xi_unicode::LineBreakIterator;
|
use xi_unicode::LineBreakIterator;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -29,18 +31,6 @@ pub enum ParChild {
|
|||||||
Any(AnyNode, Align),
|
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.
|
/// A consecutive, styled run of text.
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct TextNode {
|
pub struct TextNode {
|
||||||
@ -50,26 +40,73 @@ pub struct TextNode {
|
|||||||
pub props: FontProps,
|
pub props: FontProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Debug for TextNode {
|
impl Layout for ParNode {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Frame> {
|
||||||
write!(f, "Text({:?})", self.text)
|
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(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(range.clone(), frame, align);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Layout for ParNode {
|
|
||||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Frame> {
|
|
||||||
let mut layouter = ParLayouter::new(self.dir, self.line_spacing, areas.clone());
|
|
||||||
for child in &self.children {
|
|
||||||
match *child {
|
|
||||||
ParChild::Spacing(amount) => layouter.push_spacing(amount),
|
|
||||||
ParChild::Text(ref node, align) => layouter.push_text(ctx, node, align),
|
|
||||||
ParChild::Any(ref node, align) => {
|
|
||||||
for frame in node.layout(ctx, &layouter.areas) {
|
|
||||||
layouter.push_frame(frame, align);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
layouter.finish()
|
layouter.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,65 +117,92 @@ impl From<ParNode> for AnyNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ParLayouter {
|
struct ParLayouter<'a> {
|
||||||
dir: Dir,
|
dir: Dir,
|
||||||
line_spacing: Length,
|
line_spacing: Length,
|
||||||
|
bidi: &'a BidiInfo<'a>,
|
||||||
areas: Areas,
|
areas: Areas,
|
||||||
finished: Vec<Frame>,
|
finished: Vec<Frame>,
|
||||||
stack: Vec<(Length, Frame, Align)>,
|
stack: Vec<(Length, Frame, Align)>,
|
||||||
stack_size: Size,
|
stack_size: Size,
|
||||||
line: Vec<(Length, Frame, Align)>,
|
line: Line,
|
||||||
line_size: Size,
|
|
||||||
line_ruler: Align,
|
|
||||||
hard: bool,
|
hard: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParLayouter {
|
struct Line {
|
||||||
fn new(dir: Dir, line_spacing: Length, areas: Areas) -> Self {
|
items: Vec<LineItem>,
|
||||||
|
size: Size,
|
||||||
|
ruler: Align,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LineItem {
|
||||||
|
range: Range<usize>,
|
||||||
|
frame: Frame,
|
||||||
|
align: Align,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ParLayouter<'a> {
|
||||||
|
fn new(dir: Dir, line_spacing: Length, bidi: &'a BidiInfo<'a>, areas: Areas) -> Self {
|
||||||
Self {
|
Self {
|
||||||
dir,
|
dir,
|
||||||
line_spacing,
|
line_spacing,
|
||||||
|
bidi,
|
||||||
areas,
|
areas,
|
||||||
finished: vec![],
|
finished: vec![],
|
||||||
stack: vec![],
|
stack: vec![],
|
||||||
stack_size: Size::ZERO,
|
stack_size: Size::ZERO,
|
||||||
line: vec![],
|
line: Line {
|
||||||
line_size: Size::ZERO,
|
items: vec![],
|
||||||
line_ruler: Align::Start,
|
size: Size::ZERO,
|
||||||
|
ruler: Align::Start,
|
||||||
|
},
|
||||||
hard: true,
|
hard: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_spacing(&mut self, amount: Length) {
|
fn push_spacing(&mut self, range: Range<usize>, amount: Length) {
|
||||||
let max = self.areas.current.width;
|
let amount = amount.min(self.areas.current.width - self.line.size.width);
|
||||||
self.line_size.width = (self.line_size.width + amount).min(max);
|
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<usize>,
|
||||||
|
text: &str,
|
||||||
|
props: &FontProps,
|
||||||
|
align: Align,
|
||||||
|
) {
|
||||||
// Position in the text at which the current line starts.
|
// 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
|
// The current line attempt: Text shaped up to the previous line break
|
||||||
// opportunity.
|
// opportunity.
|
||||||
let mut last = None;
|
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() {
|
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.
|
// Remove trailing newline and spacing at the end of lines.
|
||||||
let mut line = line.trim_end_matches(is_newline);
|
let mut line = line.trim_end_matches(is_newline);
|
||||||
if pos != node.text.len() {
|
if pos != text.len() {
|
||||||
line = line.trim_end();
|
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) {
|
if self.usable().fits(frame.size) {
|
||||||
// Still fits into the line.
|
// Still fits into the line.
|
||||||
if mandatory {
|
if mandatory {
|
||||||
// We have to break here.
|
// We have to break here.
|
||||||
self.push_frame(frame, align);
|
self.push_frame(start .. pos, frame, align);
|
||||||
self.finish_line(true);
|
self.finish_line(true);
|
||||||
start = pos;
|
start = pos;
|
||||||
last = None;
|
last = None;
|
||||||
@ -149,7 +213,7 @@ impl ParLayouter {
|
|||||||
// The line start..pos doesn't fit. So we write the line up to
|
// The line start..pos doesn't fit. So we write the line up to
|
||||||
// the last position and retry writing just the single piece
|
// the last position and retry writing just the single piece
|
||||||
// behind it.
|
// behind it.
|
||||||
self.push_frame(frame, align);
|
self.push_frame(start .. pos, frame, align);
|
||||||
self.finish_line(false);
|
self.finish_line(false);
|
||||||
start = pos;
|
start = pos;
|
||||||
continue;
|
continue;
|
||||||
@ -157,7 +221,7 @@ impl ParLayouter {
|
|||||||
// Since last is `None`, we are at the first piece behind a line
|
// 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
|
// break and it still doesn't fit. Since we can't break it up
|
||||||
// further, so we just have to push it.
|
// further, so we just have to push it.
|
||||||
self.push_frame(frame, align);
|
self.push_frame(start .. pos, frame, align);
|
||||||
self.finish_line(false);
|
self.finish_line(false);
|
||||||
start = pos;
|
start = pos;
|
||||||
}
|
}
|
||||||
@ -166,12 +230,12 @@ impl ParLayouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Leftovers.
|
// Leftovers.
|
||||||
if let Some((frame, _)) = last {
|
if let Some((frame, pos)) = last {
|
||||||
self.push_frame(frame, align);
|
self.push_frame(start .. pos, frame, align);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_frame(&mut self, frame: Frame, align: Align) {
|
fn push_frame(&mut self, range: Range<usize>, frame: Frame, align: Align) {
|
||||||
// When the alignment of the last pushed frame (stored in the "ruler")
|
// 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.
|
// is further to the end than the new `frame`, we need a line break.
|
||||||
//
|
//
|
||||||
@ -184,12 +248,12 @@ impl ParLayouter {
|
|||||||
// | First |
|
// | First |
|
||||||
// | Second |
|
// | Second |
|
||||||
// +----------------------------+
|
// +----------------------------+
|
||||||
if self.line_ruler > align {
|
if self.line.ruler > align {
|
||||||
self.finish_line(false);
|
self.finish_line(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find out whether the area still has enough space for this frame.
|
// 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);
|
self.finish_line(false);
|
||||||
|
|
||||||
// Here, we can directly check whether the frame fits into
|
// 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
|
// A line can contain frames with different alignments. Their exact
|
||||||
// positions are calculated later depending on the alignments.
|
// positions are calculated later depending on the alignments.
|
||||||
let size = frame.size;
|
let size = frame.size;
|
||||||
self.line.push((self.line_size.width, frame, align));
|
self.line.items.push(LineItem { range, frame, align });
|
||||||
self.line_size.width += size.width;
|
self.line.size.width += size.width;
|
||||||
self.line_size.height = self.line_size.height.max(size.height);
|
self.line.size.height = self.line.size.height.max(size.height);
|
||||||
self.line_ruler = align;
|
self.line.ruler = align;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn usable(&self) -> Size {
|
fn usable(&self) -> Size {
|
||||||
@ -220,37 +284,60 @@ impl ParLayouter {
|
|||||||
// `areas.current`, but the width of the current line needs to be
|
// `areas.current`, but the width of the current line needs to be
|
||||||
// subtracted to make sure the frame fits.
|
// subtracted to make sure the frame fits.
|
||||||
let mut usable = self.areas.current;
|
let mut usable = self.areas.current;
|
||||||
usable.width -= self.line_size.width;
|
usable.width -= self.line.size.width;
|
||||||
usable
|
usable
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_line(&mut self, hard: bool) {
|
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;
|
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 full_size = {
|
||||||
let expand = self.areas.expand.horizontal;
|
let expand = self.areas.expand.horizontal;
|
||||||
let full = self.areas.full.width;
|
let full = self.areas.full.width;
|
||||||
Size::new(
|
Size::new(
|
||||||
expand.resolve(self.line_size.width, full),
|
expand.resolve(self.line.size.width, full),
|
||||||
self.line_size.height,
|
self.line.size.height,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut output = Frame::new(full_size);
|
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.
|
// Align along the x axis.
|
||||||
let x = align.resolve(if self.dir.is_positive() {
|
let x = item.align.resolve(if self.dir.is_positive() {
|
||||||
before .. full_size.width - self.line_size.width + before
|
offset .. full_size.width - self.line.size.width + offset
|
||||||
} else {
|
} else {
|
||||||
let before_with_self = before + frame.size.width;
|
full_size.width - self.line.size.width + offset .. offset
|
||||||
full_size.width - before_with_self
|
|
||||||
.. self.line_size.width - before_with_self
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
offset += item.frame.size.width;
|
||||||
|
|
||||||
let pos = Point::new(x, Length::ZERO);
|
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.
|
// 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.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.height += full_size.height;
|
||||||
self.stack_size.width = self.stack_size.width.max(full_size.width);
|
self.stack_size.width = self.stack_size.width.max(full_size.width);
|
||||||
self.areas.current.height -= full_size.height;
|
self.areas.current.height -= full_size.height;
|
||||||
self.line_size = Size::ZERO;
|
self.line.size = Size::ZERO;
|
||||||
self.line_ruler = Align::Start;
|
self.line.ruler = Align::Start;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_area(&mut self) {
|
fn finish_area(&mut self) {
|
||||||
@ -292,3 +379,21 @@ impl ParLayouter {
|
|||||||
self.finished
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Loading…
x
Reference in New Issue
Block a user