summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-04-03 17:36:33 +0200
committerLaurenz <laurmaedje@gmail.com>2021-04-03 21:09:17 +0200
commitd74c9378b81618419dc8c6315e391b6012955218 (patch)
treeae7de73aafa96b407bac5ead7bd827556133e34d /src
parent8245b7b73667dcdd32b32f49729d39083d513817 (diff)
New paragraph layout 🚀
The previous paragraph layout algorithm had a couple of flaws: - It always produced line break opportunities between runs although on the textual level there might have been none. - It didn't handle trailing spacing correctly in some cases. - It wouldn't have been easily adaptable to Knuth-Plass style optimal line breaking because it was fundamentally structured first-fit run-by-run. The new paragraph layout algorithm fixes these flaws. It proceeds roughly in the following stages: 1. Collect all text in the paragraph. 2. Compute BiDi embedding levels. 3. Shape all runs, layout all children and store the resulting items in a reusable (possibly even cacheable) `ParLayout`. 3. Iterate over all line breaks in the concatenated text. 4. Construct lightweight `LineLayout` objects for full lines instead of runs. These mostly borrow from the `ParLayout` and only reshape the first and last run if necessary. The design allows to use Harfbuzz's UNSAFE_TO_BREAK mechanism to make reshaping more efficient. The size of a `LineLayout` can be measured without building the line's frame. 5. Build only the selected line's frames and stack them.
Diffstat (limited to 'src')
-rw-r--r--src/exec/context.rs14
-rw-r--r--src/geom/align.rs4
-rw-r--r--src/geom/length.rs5
-rw-r--r--src/geom/size.rs3
-rw-r--r--src/layout/pad.rs2
-rw-r--r--src/layout/par.rs690
-rw-r--r--src/layout/shaping.rs64
-rw-r--r--src/layout/stack.rs43
8 files changed, 478 insertions, 347 deletions
diff --git a/src/exec/context.rs b/src/exec/context.rs
index 987f9f7f..d33d62ef 100644
--- a/src/exec/context.rs
+++ b/src/exec/context.rs
@@ -6,7 +6,7 @@ use crate::env::Env;
use crate::eval::TemplateValue;
use crate::geom::{Align, Dir, Gen, GenAxis, Length, Linear, Sides, Size};
use crate::layout::{
- AnyNode, PadNode, PageRun, ParChild, ParNode, StackChild, StackNode, TextNode, Tree,
+ AnyNode, PadNode, PageRun, ParChild, ParNode, StackChild, StackNode, Tree,
};
use crate::syntax::Span;
@@ -129,7 +129,7 @@ impl<'a> ExecContext<'a> {
fn make_text_node(&self, text: impl Into<String>) -> ParChild {
let align = self.state.aligns.cross;
let props = self.state.font.resolve_props();
- ParChild::Text(TextNode { text: text.into(), props }, align)
+ ParChild::Text(text.into(), props, align)
}
}
@@ -238,10 +238,12 @@ impl ParBuilder {
}
fn push_inner(&mut self, child: ParChild) {
- if let ParChild::Text(curr, curr_align) = &child {
- if let Some(ParChild::Text(prev, prev_align)) = self.children.last_mut() {
- if prev_align == curr_align && prev.props == curr.props {
- prev.text.push_str(&curr.text);
+ if let ParChild::Text(curr_text, curr_props, curr_align) = &child {
+ if let Some(ParChild::Text(prev_text, prev_props, prev_align)) =
+ self.children.last_mut()
+ {
+ if prev_align == curr_align && prev_props == curr_props {
+ prev_text.push_str(&curr_text);
return;
}
}
diff --git a/src/geom/align.rs b/src/geom/align.rs
index 422624d8..515efdf2 100644
--- a/src/geom/align.rs
+++ b/src/geom/align.rs
@@ -13,8 +13,8 @@ pub enum Align {
impl Align {
/// Returns the position of this alignment in the given range.
- pub fn resolve(self, range: Range<Length>) -> Length {
- match self {
+ pub fn resolve(self, dir: Dir, range: Range<Length>) -> Length {
+ match if dir.is_positive() { self } else { self.inv() } {
Self::Start => range.start,
Self::Center => (range.start + range.end) / 2.0,
Self::End => range.end,
diff --git a/src/geom/length.rs b/src/geom/length.rs
index 1175876c..1c2a5f86 100644
--- a/src/geom/length.rs
+++ b/src/geom/length.rs
@@ -81,6 +81,11 @@ impl Length {
Self { raw: self.raw.max(other.raw) }
}
+ /// Whether the other length fits into this one (i.e. is smaller).
+ pub fn fits(self, other: Self) -> bool {
+ self.raw + 1e-6 >= other.raw
+ }
+
/// Whether the length is zero.
pub fn is_zero(self) -> bool {
self.raw == 0.0
diff --git a/src/geom/size.rs b/src/geom/size.rs
index 1ba2f04b..2dd34a87 100644
--- a/src/geom/size.rs
+++ b/src/geom/size.rs
@@ -28,8 +28,7 @@ impl Size {
/// Whether the other size fits into this one (smaller width and height).
pub fn fits(self, other: Self) -> bool {
- const EPS: Length = Length::raw(1e-6);
- self.width + EPS >= other.width && self.height + EPS >= other.height
+ self.width.fits(other.width) && self.height.fits(other.height)
}
/// Whether both components are finite.
diff --git a/src/layout/pad.rs b/src/layout/pad.rs
index 2c8712af..d24ca654 100644
--- a/src/layout/pad.rs
+++ b/src/layout/pad.rs
@@ -38,6 +38,8 @@ fn pad(frame: &mut Frame, padding: Sides<Linear>) {
let origin = Point::new(padding.left, padding.top);
frame.size = padded;
+ frame.baseline += origin.y;
+
for (point, _) in &mut frame.elements {
*point += origin;
}
diff --git a/src/layout/par.rs b/src/layout/par.rs
index ffa36fd5..3e35333d 100644
--- a/src/layout/par.rs
+++ b/src/layout/par.rs
@@ -1,13 +1,14 @@
+use std::cmp::Ordering;
use std::fmt::{self, Debug, Formatter};
use std::mem;
-use std::ops::Range;
use unicode_bidi::{BidiInfo, Level};
use xi_unicode::LineBreakIterator;
use super::*;
use crate::exec::FontProps;
-use crate::parse::is_newline;
+
+type Range = std::ops::Range<usize>;
/// A node that arranges its children into a paragraph.
#[derive(Debug, Clone, PartialEq)]
@@ -26,407 +27,470 @@ pub enum ParChild {
/// Spacing between other nodes.
Spacing(Length),
/// A run of text and how to align it in its line.
- Text(TextNode, Align),
+ Text(String, FontProps, Align),
/// Any child node and how to align it in its line.
Any(AnyNode, Align),
}
-/// A consecutive, styled run of text.
-#[derive(Clone, PartialEq)]
-pub struct TextNode {
- /// The text.
- pub text: String,
- /// Properties used for font selection and layout.
- pub props: FontProps,
-}
-
impl Layout for ParNode {
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Frame> {
+ // Collect all text into one string used for BiDi analysis.
+ let (text, ranges) = self.collect_text();
+
+ // Find out the BiDi embedding levels.
+ let bidi = BidiInfo::new(&text, Level::from_dir(self.dir));
+
+ // Build a representation of the paragraph on which we can do
+ // linebreaking without layouting each and every line from scratch.
+ let layout = ParLayout::new(ctx, areas, self, bidi, ranges);
+
+ // Find suitable linebreaks.
+ layout.build(ctx, areas.clone(), self)
+ }
+}
+
+impl ParNode {
+ /// Concatenate all text in the paragraph into one string, replacing spacing
+ /// with a space character and other non-text nodes with the object
+ /// replacement character. Returns the full text alongside the range each
+ /// child spans in the text.
+ fn collect_text(&self) -> (String, Vec<Range>) {
let mut text = String::new();
let mut ranges = vec![];
- // Collect all text into one string used for BiDi analysis.
for child in &self.children {
let start = text.len();
- match child {
+ match *child {
ParChild::Spacing(_) => text.push(' '),
- ParChild::Text(node, _) => text.push_str(&node.text),
+ ParChild::Text(ref piece, _, _) => text.push_str(piece),
ParChild::Any(_, _) => text.push('\u{FFFC}'),
}
ranges.push(start .. text.len());
}
- // Find out the BiDi embedding levels.
- let bidi = BidiInfo::new(&text, Level::from_dir(self.dir));
-
- let mut layouter =
- ParLayouter::new(self.dir, self.line_spacing, &bidi, areas.clone());
-
- // Layout the children.
- 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) => {
- layouter.push_text(ctx, range, &node.props, align);
- }
- ParChild::Any(ref node, align) => {
- for frame in node.layout(ctx, &layouter.areas) {
- layouter.push_frame(range.clone(), frame, align);
- }
- }
- }
- }
-
- layouter.finish()
+ (text, ranges)
}
}
-impl From<ParNode> for AnyNode {
- fn from(par: ParNode) -> Self {
- Self::new(par)
- }
-}
-
-struct ParLayouter<'a> {
+/// A paragraph representation in which children are already layouted and text
+/// is separated into shapable runs.
+#[derive(Debug)]
+struct ParLayout<'a> {
+ /// The top-level direction.
dir: Dir,
- line_spacing: Length,
- bidi: &'a BidiInfo<'a>,
- areas: Areas,
- finished: Vec<Frame>,
- stack: Vec<(Length, Frame, Align)>,
- stack_size: Size,
- line: Line,
+ /// Bidirectional text embedding levels for the paragraph.
+ bidi: BidiInfo<'a>,
+ /// Layouted children and separated text runs.
+ items: Vec<ParItem<'a>>,
+ /// The ranges of the items in `bidi.text`.
+ ranges: Vec<Range>,
}
-struct Line {
- items: Vec<LineItem>,
- width: Length,
- top: Length,
- bottom: Length,
- ruler: Align,
- hard: bool,
-}
-
-struct LineItem {
- range: Range<usize>,
- frame: Frame,
- align: Align,
+/// A prepared item in a paragraph layout.
+#[derive(Debug)]
+enum ParItem<'a> {
+ /// Spacing between other items.
+ Spacing(Length),
+ /// A shaped text run with consistent direction.
+ Text(ShapeResult<'a>, Align),
+ /// A layouted child node.
+ Frame(Frame, 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: Line::new(true),
- }
- }
-
- /// Push horizontal spacing.
- fn push_spacing(&mut self, range: Range<usize>, amount: Length) {
- let amount = amount.min(self.areas.current.width - self.line.width);
- self.line.width += amount;
- self.line.items.push(LineItem {
- range,
- frame: Frame::new(Size::new(amount, Length::ZERO), Length::ZERO),
- align: Align::default(),
- })
- }
-
- /// Push text with equal font properties, but possibly containing runs of
- /// different directions.
- fn push_text(
- &mut self,
+impl<'a> ParLayout<'a> {
+ /// Build a paragraph layout for the given node.
+ fn new(
ctx: &mut LayoutContext,
- range: Range<usize>,
- 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,
- };
+ areas: &Areas,
+ par: &'a ParNode,
+ bidi: BidiInfo<'a>,
+ ranges: Vec<Range>,
+ ) -> Self {
+ // Prepare an iterator over each child an the range it spans.
+ let iter = ranges.into_iter().zip(&par.children);
+
+ let mut items = vec![];
+ let mut ranges = vec![];
- // 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;
+ // Layout the children and collect them into items.
+ for (range, child) in iter {
+ match *child {
+ ParChild::Spacing(amount) => {
+ items.push(ParItem::Spacing(amount));
+ ranges.push(range);
+ }
+ ParChild::Text(_, ref props, align) => {
+ split_runs(&bidi, range, |sub, dir| {
+ let text = &bidi.text[sub.clone()];
+ let shaped = shape(text, dir, &mut ctx.env.fonts, props);
+ items.push(ParItem::Text(shaped, align));
+ ranges.push(sub);
+ });
+ }
+ ParChild::Any(ref node, align) => {
+ for frame in node.layout(ctx, areas) {
+ items.push(ParItem::Frame(frame, align));
+ ranges.push(range.clone());
+ }
+ }
}
- last = level;
}
- self.push_run(ctx, start .. range.end, last.dir(), props, align);
+ Self { dir: par.dir, bidi, items, ranges }
}
- /// Push a text run with fixed direction.
- fn push_run(
- &mut self,
- ctx: &mut LayoutContext,
- range: Range<usize>,
- dir: Dir,
- props: &FontProps,
- align: Align,
- ) {
- // Position in the text at which the current line starts.
- let mut start = range.start;
-
- // The current line attempt: Text shaped up to the previous line break
- // opportunity.
+ /// Find first-fit line breaks and build the paragraph.
+ fn build(self, ctx: &mut LayoutContext, areas: Areas, par: &ParNode) -> Vec<Frame> {
+ let mut start = 0;
let mut last = None;
+ let mut stack = LineStack::new(par.line_spacing, areas);
- // Create an iterator over the line break opportunities.
- let text = &self.bidi.text[range.clone()];
- let mut iter = LineBreakIterator::new(text).peekable();
+ // TODO: Provide line break opportunities on alignment changes.
+ let mut iter = LineBreakIterator::new(self.bidi.text).peekable();
+ // Find suitable line breaks.
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.
- let mut line = line.trim_end_matches(is_newline);
- if end != range.end {
- line = line.trim_end();
- }
+ assert!(start <= end);
- // Shape the line.
- let frame = shape(line, dir, &mut ctx.env.fonts, props);
+ let line = LineLayout::new(&self, start .. end, ctx);
+ let size = line.measure().0;
- // Find out whether the runs still fits into the line.
- if self.usable().fits(frame.size) {
+ // Find out whether the line fits.
+ if stack.fits(size) {
if mandatory {
- // We have to break here because the text contained a hard
- // line break like "\n".
- self.push_frame(start .. end, frame, align);
- self.finish_line(true);
+ stack.push(line);
start = end;
last = None;
+ if end == self.bidi.text.len() {
+ stack.push(LineLayout::new(&self, end .. end, ctx));
+ }
} else {
- // Still fits, so we remember it and try making the line
- // even longer.
- last = Some((frame, end));
+ last = Some((line, end));
}
- } else if let Some((frame, pos)) = last.take() {
- // The line we just tried doesn't fit. So we write the line up
- // to the last position.
- self.push_frame(start .. pos, frame, align);
- self.finish_line(false);
- start = pos;
-
- // Retry writing just the single piece.
+ } else if let Some((line, end)) = last.take() {
+ stack.push(line);
+ stack.prepare(size.height);
+ start = end;
continue;
} else {
- // 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, we just have to push it.
- self.push_frame(start .. end, frame, align);
- self.finish_line(false);
+ stack.push(line);
start = end;
}
iter.next();
}
- // Leftovers.
- if let Some((frame, pos)) = last {
- self.push_frame(start .. pos, frame, align);
+ if let Some((line, _)) = last {
+ stack.push(line);
}
+
+ stack.finish()
}
- fn push_frame(&mut self, range: Range<usize>, 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.
- //
- // For example
- // ```
- // #align(right)[First] #align(center)[Second]
- // ```
- // would be laid out as:
- // +----------------------------+
- // | First |
- // | Second |
- // +----------------------------+
- if self.line.ruler > align {
- self.finish_line(false);
- }
+ /// Find the index of the item whose range contains the `text_offset`.
+ #[track_caller]
+ fn find(&self, text_offset: usize) -> usize {
+ find_range(&self.ranges, text_offset).unwrap()
+ }
+}
- // Find out whether the area still has enough space for this frame.
- if !self.usable().fits(frame.size) && self.line.width > Length::ZERO {
- self.finish_line(false);
-
- // Here, we can directly check whether the frame fits into
- // `areas.current` since we just called `finish_line`.
- while !self.areas.current.fits(frame.size) {
- if self.areas.in_full_last() {
- // The frame fits nowhere.
- // TODO: Should this be placed into the first area or the last?
- // TODO: Produce diagnostic once the necessary spans exist.
- break;
- } else {
- self.finish_area();
- }
- }
+impl ParItem<'_> {
+ /// The size and baseline of the item.
+ pub fn measure(&self) -> (Size, Length) {
+ match self {
+ Self::Spacing(amount) => (Size::new(*amount, Length::ZERO), Length::ZERO),
+ Self::Text(shaped, _) => shaped.measure(),
+ Self::Frame(frame, _) => (frame.size, frame.baseline),
}
-
- // A line can contain frames with different alignments. Their exact
- // positions are calculated later depending on the alignments.
- let Frame { size, baseline, .. } = frame;
- self.line.items.push(LineItem { range, frame, align });
- self.line.width += size.width;
- self.line.top = self.line.top.max(baseline);
- self.line.bottom = self.line.bottom.max(size.height - baseline);
- self.line.ruler = align;
}
+}
- fn usable(&self) -> Size {
- // Space occupied by previous lines is already removed from
- // `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.width;
- usable
+/// Split a range of text into runs of consistent direction.
+fn split_runs(bidi: &BidiInfo, range: Range, mut f: impl FnMut(Range, Dir)) {
+ let levels = &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 {
+ f(start .. end, last.dir());
+ start = end;
+ }
+ last = level;
}
- fn finish_line(&mut self, hard: bool) {
- let mut line = mem::replace(&mut self.line, Line::new(hard));
- if !line.hard && line.items.is_empty() {
- return;
- }
+ f(start .. range.end, last.dir());
+}
- // BiDi reordering.
- line.reorder(&self.bidi);
+/// A lightweight representation of a line that spans a specific range in a
+/// paragraph's text. This type enables you to cheaply measure the size of a
+/// line in a range before comitting to building the line's frame.
+struct LineLayout<'a> {
+ par: &'a ParLayout<'a>,
+ line: Range,
+ first: Option<ParItem<'a>>,
+ items: &'a [ParItem<'a>],
+ last: Option<ParItem<'a>>,
+ ranges: &'a [Range],
+}
- let full_size = {
- let expand = self.areas.expand.horizontal;
- let full = self.areas.full.width;
- Size::new(expand.resolve(line.width, full), line.top + line.bottom)
+impl<'a> LineLayout<'a> {
+ /// Create a line which spans the given range.
+ fn new(par: &'a ParLayout<'a>, mut line: Range, ctx: &mut LayoutContext) -> Self {
+ // Find the items which bound the text range.
+ let last_idx = par.find(line.end - 1);
+ let first_idx = if line.is_empty() {
+ last_idx
+ } else {
+ par.find(line.start)
};
- let mut output = Frame::new(full_size, line.top + line.bottom);
- let mut offset = Length::ZERO;
+ // Slice out the relevant items and ranges.
+ let mut items = &par.items[first_idx ..= last_idx];
+ let ranges = &par.ranges[first_idx ..= last_idx];
- for item in line.items {
- // Align along the x axis.
- let x = item.align.resolve(if self.dir.is_positive() {
- offset .. full_size.width - line.width + offset
- } else {
- full_size.width - line.width + offset .. offset
- });
+ // Reshape the last item if it's split in half.
+ let mut last = None;
+ if let Some((ParItem::Text(shaped, align), rest)) = items.split_last() {
+ // Compute the string slice indices local to the shaped result.
+ let range = &par.ranges[last_idx];
+ let start = line.start.max(range.start) - range.start;
+ let end = line.end - range.start;
+
+ // Trim whitespace at the end of the line.
+ let end = start + shaped.text()[start .. end].trim_end().len();
+ line.end = range.start + end;
+
+ if start != end || rest.is_empty() {
+ // Reshape that part (if the indices span the full range reshaping
+ // is fast and does nothing).
+ let reshaped = shaped.reshape(start .. end, &mut ctx.env.fonts);
+ last = Some(ParItem::Text(reshaped, *align));
+ }
- offset += item.frame.size.width;
+ items = rest;
+ }
- let pos = Point::new(x, line.top - item.frame.baseline);
- output.push_frame(pos, item.frame);
+ // Reshape the start item if it's split in half.
+ let mut first = None;
+ if let Some((ParItem::Text(shaped, align), rest)) = items.split_first() {
+ let range = &par.ranges[first_idx];
+ let start = line.start - range.start;
+ let end = line.end.min(range.end) - range.start;
+ if start != end {
+ let reshaped = shaped.reshape(start .. end, &mut ctx.env.fonts);
+ first = Some(ParItem::Text(reshaped, *align));
+ }
+ items = rest;
}
- // Add line spacing, but only between lines, not after the last line.
- if !self.stack.is_empty() {
- self.stack_size.height += self.line_spacing;
- self.areas.current.height -= self.line_spacing;
+ Self { par, line, first, items, last, ranges }
+ }
+
+ /// Measure the size of the line without actually building its frame.
+ fn measure(&self) -> (Size, Length) {
+ let mut width = Length::ZERO;
+ let mut top = Length::ZERO;
+ let mut bottom = Length::ZERO;
+
+ for item in self.iter() {
+ let (size, baseline) = item.measure();
+ width += size.width;
+ top = top.max(baseline);
+ bottom = bottom.max(size.height - baseline);
}
- self.stack.push((self.stack_size.height, output, 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;
+ (Size::new(width, top + bottom), top)
}
- fn finish_area(&mut self) {
- let mut output = Frame::new(self.stack_size, Length::ZERO);
- let mut baseline = None;
+ /// Build the line's frame.
+ fn build(&self, width: Length) -> Frame {
+ let (size, baseline) = self.measure();
+ let full_size = Size::new(size.width.max(width), size.height);
- for (before, line, align) in mem::take(&mut self.stack) {
- // Align along the x axis.
- let x = align.resolve(if self.dir.is_positive() {
- Length::ZERO .. self.stack_size.width - line.size.width
- } else {
- self.stack_size.width - line.size.width .. Length::ZERO
- });
+ let mut output = Frame::new(full_size, baseline);
+ let mut offset = Length::ZERO;
+
+ let mut ruler = Align::Start;
+ self.reordered(|item| {
+ let (frame, align) = match *item {
+ ParItem::Spacing(amount) => {
+ offset += amount;
+ return;
+ }
+ ParItem::Text(ref shaped, align) => (shaped.build(), align),
+ ParItem::Frame(ref frame, align) => (frame.clone(), align),
+ };
+
+ ruler = ruler.max(align);
+
+ let range = offset .. full_size.width - size.width + offset;
+ let x = ruler.resolve(self.par.dir, range);
+ let y = baseline - frame.baseline;
+
+ offset += frame.size.width;
+ output.push_frame(Point::new(x, y), frame);
+ });
+
+ output
+ }
- let pos = Point::new(x, before);
- baseline.get_or_insert(pos.y + line.baseline);
- output.push_frame(pos, line);
+ /// Iterate through the line's items in visual order.
+ fn reordered(&self, mut f: impl FnMut(&ParItem<'a>)) {
+ if self.line.is_empty() {
+ return;
}
- if let Some(baseline) = baseline {
- output.baseline = baseline;
+ // Find the paragraph that contains the frame.
+ let para = self
+ .par
+ .bidi
+ .paragraphs
+ .iter()
+ .find(|para| para.range.contains(&self.line.start))
+ .unwrap();
+
+ // Compute the reordered ranges in visual order (left to right).
+ let (levels, runs) = self.par.bidi.visual_runs(para, self.line.clone());
+
+ // Find the items for each run.
+ for run in runs {
+ let first_idx = self.find(run.start);
+ let last_idx = self.find(run.end - 1);
+ let range = first_idx ..= last_idx;
+
+ // Provide the items forwards or backwards depending on the run's
+ // direction.
+ if levels[run.start].is_ltr() {
+ for item in range {
+ f(self.get(item));
+ }
+ } else {
+ for item in range.rev() {
+ f(self.get(item));
+ }
+ }
}
+ }
- self.finished.push(output);
- self.areas.next();
- self.stack_size = Size::ZERO;
+ /// Find the index of the item whose range contains the `text_offset`.
+ #[track_caller]
+ fn find(&self, text_offset: usize) -> usize {
+ find_range(self.ranges, text_offset).unwrap()
}
- fn finish(mut self) -> Vec<Frame> {
- self.finish_line(false);
- self.finish_area();
- self.finished
+ /// Get the item at the index.
+ #[track_caller]
+ fn get(&self, index: usize) -> &ParItem<'a> {
+ self.iter().nth(index).unwrap()
+ }
+
+ /// Iterate over the items of the line.
+ fn iter(&self) -> impl Iterator<Item = &ParItem<'a>> {
+ self.first.iter().chain(self.items).chain(&self.last)
}
}
-impl Line {
- fn new(hard: bool) -> Self {
+/// Find the range that contains the position.
+fn find_range(ranges: &[Range], pos: usize) -> Option<usize> {
+ ranges.binary_search_by(|r| cmp(r, pos)).ok()
+}
+
+/// Comparison function for a range and a position used in binary search.
+fn cmp(range: &Range, pos: usize) -> Ordering {
+ if pos < range.start {
+ Ordering::Greater
+ } else if pos < range.end {
+ Ordering::Equal
+ } else {
+ Ordering::Less
+ }
+}
+
+/// Stacks lines into paragraph frames.
+struct LineStack<'a> {
+ line_spacing: Length,
+ areas: Areas,
+ finished: Vec<Frame>,
+ lines: Vec<LineLayout<'a>>,
+ size: Size,
+}
+
+impl<'a> LineStack<'a> {
+ fn new(line_spacing: Length, areas: Areas) -> Self {
Self {
- items: vec![],
- width: Length::ZERO,
- top: Length::ZERO,
- bottom: Length::ZERO,
- ruler: Align::Start,
- hard,
+ line_spacing,
+ areas,
+ finished: vec![],
+ lines: vec![],
+ size: Size::ZERO,
}
}
- fn reorder(&mut self, bidi: &BidiInfo) {
- let items = &mut self.items;
- let line_range = match (items.first(), items.last()) {
- (Some(first), Some(last)) => first.range.start .. last.range.end,
- _ => return,
- };
+ fn fits(&self, size: Size) -> bool {
+ self.areas.current.fits(size)
+ }
- // Find the paragraph that contains the frame.
- let para = bidi
- .paragraphs
- .iter()
- .find(|para| para.range.contains(&line_range.start))
- .unwrap();
+ fn prepare(&mut self, height: Length) {
+ if !self.areas.current.height.fits(height) && !self.areas.in_full_last() {
+ self.finish_area();
+ }
+ }
- // Compute the reordered ranges in visual order (left to right).
- let (levels, ranges) = bidi.visual_runs(para, line_range);
+ fn push(&mut self, line: LineLayout<'a>) {
+ let size = line.measure().0;
- // Reorder the items.
- items.sort_by_key(|item| {
- let Range { start, end } = item.range;
+ if !self.lines.is_empty() {
+ self.size.height += self.line_spacing;
+ self.areas.current.height -= self.line_spacing;
+ }
- // Determine the index in visual order.
- let idx = ranges.iter().position(|r| r.contains(&start)).unwrap();
+ self.size.width = self.size.width.max(size.width);
+ self.size.height += size.height;
+ self.areas.current.height -= size.height;
+ self.lines.push(line);
+ }
- // 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
- };
+ fn finish_area(&mut self) {
+ let expand = self.areas.expand.horizontal;
+ let full = self.areas.full.width;
+ self.size.width = expand.resolve(self.size.width, full);
- (idx, dist)
- });
+ let mut output = Frame::new(self.size, self.size.height);
+ let mut y = Length::ZERO;
+ let mut first = true;
+
+ for line in mem::take(&mut self.lines) {
+ let frame = line.build(self.size.width);
+ let height = frame.size.height;
+
+ if first {
+ output.baseline = y + frame.baseline;
+ first = false;
+ }
+
+ output.push_frame(Point::new(Length::ZERO, y), frame);
+ y += height + self.line_spacing;
+ }
+
+ self.finished.push(output);
+ self.areas.next();
+ self.size = Size::ZERO;
+ }
+
+ fn finish(mut self) -> Vec<Frame> {
+ self.finish_area();
+ self.finished
}
}
+/// Helper methods for BiDi levels.
trait LevelExt: Sized {
fn from_dir(dir: Dir) -> Option<Self>;
fn dir(self) -> Dir;
@@ -446,20 +510,20 @@ impl LevelExt for Level {
}
}
+impl From<ParNode> for AnyNode {
+ fn from(par: ParNode) -> Self {
+ Self::new(par)
+ }
+}
+
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::Text(text, _, align) => write!(f, "Text({:?}, {:?})", 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/src/layout/shaping.rs b/src/layout/shaping.rs
index ca30ce3e..7ead0dff 100644
--- a/src/layout/shaping.rs
+++ b/src/layout/shaping.rs
@@ -1,3 +1,6 @@
+use std::fmt::{self, Debug, Formatter};
+use std::ops::Range;
+
use fontdock::FaceId;
use rustybuzz::UnicodeBuffer;
use ttf_parser::GlyphId;
@@ -8,7 +11,12 @@ use crate::exec::FontProps;
use crate::geom::{Dir, Length, Point, Size};
/// Shape text into a frame containing [`ShapedText`] runs.
-pub fn shape(text: &str, dir: Dir, loader: &mut FontLoader, props: &FontProps) -> Frame {
+pub fn shape<'a>(
+ text: &'a str,
+ dir: Dir,
+ loader: &mut FontLoader,
+ props: &'a FontProps,
+) -> ShapeResult<'a> {
let iter = props.families.iter();
let mut results = vec![];
shape_segment(&mut results, text, dir, loader, props, iter, None);
@@ -21,13 +29,57 @@ pub fn shape(text: &str, dir: Dir, loader: &mut FontLoader, props: &FontProps) -
}
let mut frame = Frame::new(Size::new(Length::ZERO, top + bottom), top);
+
for shaped in results {
let offset = frame.size.width;
frame.size.width += shaped.width;
- frame.push(Point::new(offset, top), Element::Text(shaped));
+
+ if !shaped.glyphs.is_empty() {
+ frame.push(Point::new(offset, top), Element::Text(shaped));
+ }
+ }
+
+ ShapeResult { frame, text, dir, props }
+}
+
+#[derive(Clone)]
+pub struct ShapeResult<'a> {
+ frame: Frame,
+ text: &'a str,
+ dir: Dir,
+ props: &'a FontProps,
+}
+
+impl<'a> ShapeResult<'a> {
+ pub fn reshape(
+ &self,
+ range: Range<usize>,
+ loader: &mut FontLoader,
+ ) -> ShapeResult<'_> {
+ if range.start == 0 && range.end == self.text.len() {
+ self.clone()
+ } else {
+ shape(&self.text[range], self.dir, loader, self.props)
+ }
+ }
+
+ pub fn text(&self) -> &'a str {
+ self.text
}
- frame
+ pub fn measure(&self) -> (Size, Length) {
+ (self.frame.size, self.frame.baseline)
+ }
+
+ pub fn build(&self) -> Frame {
+ self.frame.clone()
+ }
+}
+
+impl Debug for ShapeResult<'_> {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "Shaped({:?})", self.text)
+ }
}
/// Shape text into a frame with font fallback using the `families` iterator.
@@ -71,6 +123,12 @@ fn shape_segment<'a>(
let bottom = convert(i32::from(-props.bottom_edge.lookup(ttf)));
let mut shaped = ShapedText::new(id, props.size, top, bottom, props.color);
+ // For empty text, we want a zero-width box with the correct height.
+ if text.is_empty() {
+ results.push(shaped);
+ return;
+ }
+
// Fill the buffer with our text.
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
diff --git a/src/layout/stack.rs b/src/layout/stack.rs
index b9de1bbc..8e2249d2 100644
--- a/src/layout/stack.rs
+++ b/src/layout/stack.rs
@@ -116,39 +116,40 @@ impl StackLayouter {
size = Size::new(width, width / aspect);
}
- size.switch(self.main)
+ size
};
- let mut output = Frame::new(full_size.switch(self.main).to_size(), Length::ZERO);
- let mut baseline = None;
+ let mut output = Frame::new(full_size, full_size.height);
+ let mut first = true;
+ let full_size = full_size.switch(self.main);
for (before, frame, aligns) in std::mem::take(&mut self.frames) {
let child_size = frame.size.switch(self.main);
// Align along the main axis.
- let main = aligns.main.resolve(if self.dirs.main.is_positive() {
- let after_with_self = self.size.main - before;
- before .. full_size.main - after_with_self
- } else {
- let before_with_self = before + child_size.main;
- let after = self.size.main - (before + child_size.main);
- full_size.main - before_with_self .. after
- });
+ let main = aligns.main.resolve(
+ self.dirs.main,
+ if self.dirs.main.is_positive() {
+ before .. before + full_size.main - self.size.main
+ } else {
+ self.size.main - (before + child_size.main)
+ .. full_size.main - (before + child_size.main)
+ },
+ );
// Align along the cross axis.
- let cross = aligns.cross.resolve(if self.dirs.cross.is_positive() {
- Length::ZERO .. full_size.cross - child_size.cross
- } else {
- full_size.cross - child_size.cross .. Length::ZERO
- });
+ let cross = aligns.cross.resolve(
+ self.dirs.cross,
+ Length::ZERO .. full_size.cross - child_size.cross,
+ );
let pos = Gen::new(main, cross).switch(self.main).to_point();
- baseline.get_or_insert(pos.y + frame.baseline);
- output.push_frame(pos, frame);
- }
+ if first {
+ output.baseline = pos.y + frame.baseline;
+ first = false;
+ }
- if let Some(baseline) = baseline {
- output.baseline = baseline;
+ output.push_frame(pos, frame);
}
self.finished.push(output);