mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
Refactor ♻
This commit is contained in:
parent
12a604fec8
commit
c00cca3677
@ -45,6 +45,7 @@ impl Layout for ParNode {
|
|||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
let mut ranges = vec![];
|
let mut ranges = vec![];
|
||||||
|
|
||||||
|
// Collect all text into one string used for BiDi analysis.
|
||||||
for child in &self.children {
|
for child in &self.children {
|
||||||
let start = text.len();
|
let start = text.len();
|
||||||
match child {
|
match child {
|
||||||
@ -55,49 +56,20 @@ impl Layout for ParNode {
|
|||||||
ranges.push(start .. text.len());
|
ranges.push(start .. text.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
let level = match self.dir {
|
// Find out the BiDi embedding levels.
|
||||||
Dir::LTR => Level::ltr(),
|
let bidi = BidiInfo::new(&text, Level::from_dir(self.dir));
|
||||||
Dir::RTL => Level::rtl(),
|
|
||||||
_ => panic!("invalid paragraph direction"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let bidi = BidiInfo::new(&text, Some(level));
|
|
||||||
let mut layouter =
|
let mut layouter =
|
||||||
ParLayouter::new(self.dir, self.line_spacing, &bidi, areas.clone());
|
ParLayouter::new(self.dir, self.line_spacing, &bidi, areas.clone());
|
||||||
|
|
||||||
|
// Layout the children.
|
||||||
for (range, child) in ranges.into_iter().zip(&self.children) {
|
for (range, child) in ranges.into_iter().zip(&self.children) {
|
||||||
match *child {
|
match *child {
|
||||||
ParChild::Spacing(amount) => {
|
ParChild::Spacing(amount) => {
|
||||||
layouter.push_spacing(range, amount);
|
layouter.push_spacing(range, amount);
|
||||||
}
|
}
|
||||||
ParChild::Text(ref node, align) => {
|
ParChild::Text(ref node, align) => {
|
||||||
let mut start = range.start;
|
layouter.push_text(ctx, range, &node.props, align);
|
||||||
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) => {
|
ParChild::Any(ref node, align) => {
|
||||||
for frame in node.layout(ctx, &layouter.areas) {
|
for frame in node.layout(ctx, &layouter.areas) {
|
||||||
@ -126,13 +98,13 @@ struct ParLayouter<'a> {
|
|||||||
stack: Vec<(Length, Frame, Align)>,
|
stack: Vec<(Length, Frame, Align)>,
|
||||||
stack_size: Size,
|
stack_size: Size,
|
||||||
line: Line,
|
line: Line,
|
||||||
hard: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Line {
|
struct Line {
|
||||||
items: Vec<LineItem>,
|
items: Vec<LineItem>,
|
||||||
size: Size,
|
size: Size,
|
||||||
ruler: Align,
|
ruler: Align,
|
||||||
|
hard: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LineItem {
|
struct LineItem {
|
||||||
@ -155,11 +127,12 @@ impl<'a> ParLayouter<'a> {
|
|||||||
items: vec![],
|
items: vec![],
|
||||||
size: Size::ZERO,
|
size: Size::ZERO,
|
||||||
ruler: Align::Start,
|
ruler: Align::Start,
|
||||||
|
hard: true,
|
||||||
},
|
},
|
||||||
hard: true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Push horizontal spacing.
|
||||||
fn push_spacing(&mut self, range: Range<usize>, amount: Length) {
|
fn push_spacing(&mut self, range: Range<usize>, amount: Length) {
|
||||||
let amount = amount.min(self.areas.current.width - self.line.size.width);
|
let amount = amount.min(self.areas.current.width - self.line.size.width);
|
||||||
self.line.size.width += amount;
|
self.line.size.width += amount;
|
||||||
@ -170,11 +143,42 @@ impl<'a> ParLayouter<'a> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Push text with equal font properties, but possibly containing runs of
|
||||||
|
/// different directions.
|
||||||
fn push_text(
|
fn push_text(
|
||||||
&mut self,
|
&mut self,
|
||||||
ctx: &mut LayoutContext,
|
ctx: &mut LayoutContext,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
text: &str,
|
props: &FontProps,
|
||||||
|
align: Align,
|
||||||
|
) {
|
||||||
|
let levels = &self.bidi.levels[range.clone()];
|
||||||
|
|
||||||
|
let mut start = range.start;
|
||||||
|
let mut last = match levels.first() {
|
||||||
|
Some(&level) => level,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Split into runs with the same embedding level.
|
||||||
|
for (idx, &level) in levels.iter().enumerate() {
|
||||||
|
let end = range.start + idx;
|
||||||
|
if last != level {
|
||||||
|
self.push_run(ctx, start .. end, last.dir(), props, align);
|
||||||
|
start = end;
|
||||||
|
}
|
||||||
|
last = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.push_run(ctx, start .. range.end, last.dir(), props, align);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push a text run with fixed direction.
|
||||||
|
fn push_run(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut LayoutContext,
|
||||||
|
range: Range<usize>,
|
||||||
|
dir: Dir,
|
||||||
props: &FontProps,
|
props: &FontProps,
|
||||||
align: Align,
|
align: Align,
|
||||||
) {
|
) {
|
||||||
@ -185,45 +189,54 @@ impl<'a> ParLayouter<'a> {
|
|||||||
// opportunity.
|
// opportunity.
|
||||||
let mut last = None;
|
let mut last = None;
|
||||||
|
|
||||||
|
// Create an iterator over the line break opportunities.
|
||||||
|
let text = &self.bidi.text[range.clone()];
|
||||||
let mut iter = LineBreakIterator::new(text).peekable();
|
let mut iter = LineBreakIterator::new(text).peekable();
|
||||||
while let Some(&(pos, mandatory)) = iter.peek() {
|
|
||||||
let line = &text[start - range.start .. pos];
|
while let Some(&(end, mandatory)) = iter.peek() {
|
||||||
|
// Slice the line of text.
|
||||||
|
let end = range.start + end;
|
||||||
|
let line = &self.bidi.text[start .. end];
|
||||||
|
|
||||||
// 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 != text.len() {
|
if end != range.end {
|
||||||
line = line.trim_end();
|
line = line.trim_end();
|
||||||
}
|
}
|
||||||
|
|
||||||
let pos = range.start + pos;
|
// Shape the line.
|
||||||
let frame = shape(line, &mut ctx.env.fonts, props);
|
let frame = shape(line, dir, &mut ctx.env.fonts, props);
|
||||||
|
|
||||||
|
// Find out whether the runs still fits into the line.
|
||||||
if self.usable().fits(frame.size) {
|
if self.usable().fits(frame.size) {
|
||||||
// Still fits into the line.
|
|
||||||
if mandatory {
|
if mandatory {
|
||||||
// We have to break here.
|
// We have to break here because the text contained a hard
|
||||||
self.push_frame(start .. pos, frame, align);
|
// line break like "\n".
|
||||||
|
self.push_frame(start .. end, frame, align);
|
||||||
self.finish_line(true);
|
self.finish_line(true);
|
||||||
start = pos;
|
start = end;
|
||||||
last = None;
|
last = None;
|
||||||
} else {
|
} else {
|
||||||
last = Some((frame, pos));
|
// Still fits, so we remember it and try making the line
|
||||||
|
// even longer.
|
||||||
|
last = Some((frame, end));
|
||||||
}
|
}
|
||||||
} else if let Some((frame, pos)) = last.take() {
|
} else if let Some((frame, pos)) = last.take() {
|
||||||
// The line start..pos doesn't fit. So we write the line up to
|
// The line we just tried doesn't fit. So we write the line up
|
||||||
// the last position and retry writing just the single piece
|
// to the last position.
|
||||||
// behind it.
|
|
||||||
self.push_frame(start .. pos, frame, align);
|
self.push_frame(start .. pos, frame, align);
|
||||||
self.finish_line(false);
|
self.finish_line(false);
|
||||||
start = pos;
|
start = pos;
|
||||||
|
|
||||||
|
// Retry writing just the single piece.
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
// 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
|
||||||
// break and it still doesn't fit. Since we can't break it up
|
// line break and it still doesn't fit. Since we can't break it
|
||||||
// further, so we just have to push it.
|
// up further, we just have to push it.
|
||||||
self.push_frame(start .. pos, frame, align);
|
self.push_frame(start .. end, frame, align);
|
||||||
self.finish_line(false);
|
self.finish_line(false);
|
||||||
start = pos;
|
start = end;
|
||||||
}
|
}
|
||||||
|
|
||||||
iter.next();
|
iter.next();
|
||||||
@ -289,30 +302,12 @@ impl<'a> ParLayouter<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn finish_line(&mut self, hard: bool) {
|
fn finish_line(&mut self, hard: bool) {
|
||||||
if !mem::replace(&mut self.hard, hard) && self.line.items.is_empty() {
|
if !mem::replace(&mut self.line.hard, hard) && self.line.items.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut items = mem::take(&mut self.line.items);
|
// BiDi reordering.
|
||||||
if let (Some(first), Some(last)) = (items.first(), items.last()) {
|
self.reorder_line();
|
||||||
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;
|
||||||
@ -326,7 +321,7 @@ impl<'a> ParLayouter<'a> {
|
|||||||
let mut output = Frame::new(full_size);
|
let mut output = Frame::new(full_size);
|
||||||
let mut offset = Length::ZERO;
|
let mut offset = Length::ZERO;
|
||||||
|
|
||||||
for item in items {
|
for item in mem::take(&mut self.line.items) {
|
||||||
// Align along the x axis.
|
// Align along the x axis.
|
||||||
let x = item.align.resolve(if self.dir.is_positive() {
|
let x = item.align.resolve(if self.dir.is_positive() {
|
||||||
offset .. full_size.width - self.line.size.width + offset
|
offset .. full_size.width - self.line.size.width + offset
|
||||||
@ -354,6 +349,45 @@ impl<'a> ParLayouter<'a> {
|
|||||||
self.line.ruler = Align::Start;
|
self.line.ruler = Align::Start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reorder_line(&mut self) {
|
||||||
|
let items = &mut self.line.items;
|
||||||
|
let line_range = match (items.first(), items.last()) {
|
||||||
|
(Some(first), Some(last)) => first.range.start .. last.range.end,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find the paragraph that contains the frame.
|
||||||
|
let para = self
|
||||||
|
.bidi
|
||||||
|
.paragraphs
|
||||||
|
.iter()
|
||||||
|
.find(|para| para.range.contains(&line_range.start))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Compute the reordered ranges in visual order (left to right).
|
||||||
|
let (levels, ranges) = self.bidi.visual_runs(para, line_range);
|
||||||
|
|
||||||
|
// Reorder the items.
|
||||||
|
items.sort_by_key(|item| {
|
||||||
|
let Range { start, end } = item.range;
|
||||||
|
|
||||||
|
// Determine the index in visual order.
|
||||||
|
let idx = ranges.iter().position(|r| r.contains(&start)).unwrap();
|
||||||
|
|
||||||
|
// A run might span more than one frame. To sort frames inside a run
|
||||||
|
// based on the run's direction, we compute the distance from
|
||||||
|
// the "start" of the run.
|
||||||
|
let run = &ranges[idx];
|
||||||
|
let dist = if levels[start].is_ltr() {
|
||||||
|
start - run.start
|
||||||
|
} else {
|
||||||
|
run.end - end
|
||||||
|
};
|
||||||
|
|
||||||
|
(idx, dist)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn finish_area(&mut self) {
|
fn finish_area(&mut self) {
|
||||||
let mut output = Frame::new(self.stack_size);
|
let mut output = Frame::new(self.stack_size);
|
||||||
for (before, line, align) in mem::take(&mut self.stack) {
|
for (before, line, align) in mem::take(&mut self.stack) {
|
||||||
@ -380,6 +414,25 @@ impl<'a> ParLayouter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait LevelExt: Sized {
|
||||||
|
fn from_dir(dir: Dir) -> Option<Self>;
|
||||||
|
fn dir(self) -> Dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LevelExt for Level {
|
||||||
|
fn from_dir(dir: Dir) -> Option<Self> {
|
||||||
|
match dir {
|
||||||
|
Dir::LTR => Some(Level::ltr()),
|
||||||
|
Dir::RTL => Some(Level::rtl()),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dir(self) -> Dir {
|
||||||
|
if self.is_ltr() { Dir::LTR } else { Dir::RTL }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Debug for ParChild {
|
impl Debug for ParChild {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
@ -5,12 +5,13 @@ use ttf_parser::GlyphId;
|
|||||||
use super::{Element, Frame, ShapedText};
|
use super::{Element, Frame, ShapedText};
|
||||||
use crate::env::FontLoader;
|
use crate::env::FontLoader;
|
||||||
use crate::exec::FontProps;
|
use crate::exec::FontProps;
|
||||||
use crate::geom::{Point, Size};
|
use crate::geom::{Dir, Point, Size};
|
||||||
|
|
||||||
/// Shape text into a frame containing [`ShapedText`] runs.
|
/// Shape text into a frame containing [`ShapedText`] runs.
|
||||||
pub fn shape(text: &str, loader: &mut FontLoader, props: &FontProps) -> Frame {
|
pub fn shape(text: &str, dir: Dir, loader: &mut FontLoader, props: &FontProps) -> Frame {
|
||||||
let mut frame = Frame::new(Size::ZERO);
|
let mut frame = Frame::new(Size::ZERO);
|
||||||
shape_segment(&mut frame, text, loader, props, props.families.iter(), None);
|
let iter = props.families.iter();
|
||||||
|
shape_segment(&mut frame, text, dir, loader, props, iter, None);
|
||||||
frame
|
frame
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,6 +19,7 @@ pub fn shape(text: &str, loader: &mut FontLoader, props: &FontProps) -> Frame {
|
|||||||
fn shape_segment<'a>(
|
fn shape_segment<'a>(
|
||||||
frame: &mut Frame,
|
frame: &mut Frame,
|
||||||
text: &str,
|
text: &str,
|
||||||
|
dir: Dir,
|
||||||
loader: &mut FontLoader,
|
loader: &mut FontLoader,
|
||||||
props: &FontProps,
|
props: &FontProps,
|
||||||
mut families: impl Iterator<Item = &'a str> + Clone,
|
mut families: impl Iterator<Item = &'a str> + Clone,
|
||||||
@ -57,11 +59,11 @@ fn shape_segment<'a>(
|
|||||||
// Fill the buffer with our text.
|
// Fill the buffer with our text.
|
||||||
let mut buffer = UnicodeBuffer::new();
|
let mut buffer = UnicodeBuffer::new();
|
||||||
buffer.push_str(text);
|
buffer.push_str(text);
|
||||||
buffer.guess_segment_properties();
|
buffer.set_direction(match dir {
|
||||||
|
Dir::LTR => rustybuzz::Direction::LeftToRight,
|
||||||
// Find out the text direction.
|
Dir::RTL => rustybuzz::Direction::RightToLeft,
|
||||||
// TODO: Replace this once we do BiDi.
|
_ => unimplemented!(),
|
||||||
let rtl = matches!(buffer.direction(), rustybuzz::Direction::RightToLeft);
|
});
|
||||||
|
|
||||||
// Shape!
|
// Shape!
|
||||||
let glyphs = rustybuzz::shape(face.buzz(), &[], buffer);
|
let glyphs = rustybuzz::shape(face.buzz(), &[], buffer);
|
||||||
@ -92,7 +94,7 @@ fn shape_segment<'a>(
|
|||||||
// Because Harfbuzz outputs glyphs in visual order, the start
|
// Because Harfbuzz outputs glyphs in visual order, the start
|
||||||
// cluster actually corresponds to the last codepoint in
|
// cluster actually corresponds to the last codepoint in
|
||||||
// right-to-left text.
|
// right-to-left text.
|
||||||
if rtl {
|
if !dir.is_positive() {
|
||||||
assert!(end <= start);
|
assert!(end <= start);
|
||||||
std::mem::swap(&mut start, &mut end);
|
std::mem::swap(&mut start, &mut end);
|
||||||
}
|
}
|
||||||
@ -102,9 +104,10 @@ fn shape_segment<'a>(
|
|||||||
// char.
|
// char.
|
||||||
let offset = text[end ..].chars().next().unwrap().len_utf8();
|
let offset = text[end ..].chars().next().unwrap().len_utf8();
|
||||||
let range = start .. end + offset;
|
let range = start .. end + offset;
|
||||||
|
let part = &text[range];
|
||||||
|
|
||||||
// Recursively shape the tofu sequence with the next family.
|
// Recursively shape the tofu sequence with the next family.
|
||||||
shape_segment(frame, &text[range], loader, props, families.clone(), first);
|
shape_segment(frame, part, dir, loader, props, families.clone(), first);
|
||||||
} else {
|
} else {
|
||||||
// Add the glyph to the shaped output.
|
// Add the glyph to the shaped output.
|
||||||
// TODO: Don't ignore y_advance and y_offset.
|
// TODO: Don't ignore y_advance and y_offset.
|
||||||
|
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