mirror of
https://github.com/typst/typst
synced 2025-05-20 12:05:27 +08:00
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.
This commit is contained in:
parent
8245b7b736
commit
d74c9378b8
@ -6,7 +6,7 @@ use crate::env::Env;
|
|||||||
use crate::eval::TemplateValue;
|
use crate::eval::TemplateValue;
|
||||||
use crate::geom::{Align, Dir, Gen, GenAxis, Length, Linear, Sides, Size};
|
use crate::geom::{Align, Dir, Gen, GenAxis, Length, Linear, Sides, Size};
|
||||||
use crate::layout::{
|
use crate::layout::{
|
||||||
AnyNode, PadNode, PageRun, ParChild, ParNode, StackChild, StackNode, TextNode, Tree,
|
AnyNode, PadNode, PageRun, ParChild, ParNode, StackChild, StackNode, Tree,
|
||||||
};
|
};
|
||||||
use crate::syntax::Span;
|
use crate::syntax::Span;
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ impl<'a> ExecContext<'a> {
|
|||||||
fn make_text_node(&self, text: impl Into<String>) -> ParChild {
|
fn make_text_node(&self, text: impl Into<String>) -> ParChild {
|
||||||
let align = self.state.aligns.cross;
|
let align = self.state.aligns.cross;
|
||||||
let props = self.state.font.resolve_props();
|
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) {
|
fn push_inner(&mut self, child: ParChild) {
|
||||||
if let ParChild::Text(curr, curr_align) = &child {
|
if let ParChild::Text(curr_text, curr_props, curr_align) = &child {
|
||||||
if let Some(ParChild::Text(prev, prev_align)) = self.children.last_mut() {
|
if let Some(ParChild::Text(prev_text, prev_props, prev_align)) =
|
||||||
if prev_align == curr_align && prev.props == curr.props {
|
self.children.last_mut()
|
||||||
prev.text.push_str(&curr.text);
|
{
|
||||||
|
if prev_align == curr_align && prev_props == curr_props {
|
||||||
|
prev_text.push_str(&curr_text);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@ pub enum Align {
|
|||||||
|
|
||||||
impl Align {
|
impl Align {
|
||||||
/// Returns the position of this alignment in the given range.
|
/// Returns the position of this alignment in the given range.
|
||||||
pub fn resolve(self, range: Range<Length>) -> Length {
|
pub fn resolve(self, dir: Dir, range: Range<Length>) -> Length {
|
||||||
match self {
|
match if dir.is_positive() { self } else { self.inv() } {
|
||||||
Self::Start => range.start,
|
Self::Start => range.start,
|
||||||
Self::Center => (range.start + range.end) / 2.0,
|
Self::Center => (range.start + range.end) / 2.0,
|
||||||
Self::End => range.end,
|
Self::End => range.end,
|
||||||
|
@ -81,6 +81,11 @@ impl Length {
|
|||||||
Self { raw: self.raw.max(other.raw) }
|
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.
|
/// Whether the length is zero.
|
||||||
pub fn is_zero(self) -> bool {
|
pub fn is_zero(self) -> bool {
|
||||||
self.raw == 0.0
|
self.raw == 0.0
|
||||||
|
@ -28,8 +28,7 @@ impl Size {
|
|||||||
|
|
||||||
/// Whether the other size fits into this one (smaller width and height).
|
/// Whether the other size fits into this one (smaller width and height).
|
||||||
pub fn fits(self, other: Self) -> bool {
|
pub fn fits(self, other: Self) -> bool {
|
||||||
const EPS: Length = Length::raw(1e-6);
|
self.width.fits(other.width) && self.height.fits(other.height)
|
||||||
self.width + EPS >= other.width && self.height + EPS >= other.height
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether both components are finite.
|
/// Whether both components are finite.
|
||||||
|
@ -38,6 +38,8 @@ fn pad(frame: &mut Frame, padding: Sides<Linear>) {
|
|||||||
let origin = Point::new(padding.left, padding.top);
|
let origin = Point::new(padding.left, padding.top);
|
||||||
|
|
||||||
frame.size = padded;
|
frame.size = padded;
|
||||||
|
frame.baseline += origin.y;
|
||||||
|
|
||||||
for (point, _) in &mut frame.elements {
|
for (point, _) in &mut frame.elements {
|
||||||
*point += origin;
|
*point += origin;
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
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 unicode_bidi::{BidiInfo, Level};
|
||||||
use xi_unicode::LineBreakIterator;
|
use xi_unicode::LineBreakIterator;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::exec::FontProps;
|
use crate::exec::FontProps;
|
||||||
use crate::parse::is_newline;
|
|
||||||
|
type Range = std::ops::Range<usize>;
|
||||||
|
|
||||||
/// A node that arranges its children into a paragraph.
|
/// A node that arranges its children into a paragraph.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@ -26,407 +27,470 @@ pub enum ParChild {
|
|||||||
/// Spacing between other nodes.
|
/// Spacing between other nodes.
|
||||||
Spacing(Length),
|
Spacing(Length),
|
||||||
/// A run of text and how to align it in its line.
|
/// 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 child node and how to align it in its line.
|
||||||
Any(AnyNode, Align),
|
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 {
|
impl Layout for ParNode {
|
||||||
fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Vec<Frame> {
|
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 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 {
|
||||||
ParChild::Spacing(_) => text.push(' '),
|
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}'),
|
ParChild::Any(_, _) => text.push('\u{FFFC}'),
|
||||||
}
|
}
|
||||||
ranges.push(start .. text.len());
|
ranges.push(start .. text.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find out the BiDi embedding levels.
|
(text, ranges)
|
||||||
let bidi = BidiInfo::new(&text, Level::from_dir(self.dir));
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut layouter =
|
/// A paragraph representation in which children are already layouted and text
|
||||||
ParLayouter::new(self.dir, self.line_spacing, &bidi, areas.clone());
|
/// is separated into shapable runs.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ParLayout<'a> {
|
||||||
|
/// The top-level direction.
|
||||||
|
dir: Dir,
|
||||||
|
/// 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>,
|
||||||
|
}
|
||||||
|
|
||||||
// Layout the children.
|
/// A prepared item in a paragraph layout.
|
||||||
for (range, child) in ranges.into_iter().zip(&self.children) {
|
#[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> ParLayout<'a> {
|
||||||
|
/// Build a paragraph layout for the given node.
|
||||||
|
fn new(
|
||||||
|
ctx: &mut LayoutContext,
|
||||||
|
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![];
|
||||||
|
|
||||||
|
// Layout the children and collect them into items.
|
||||||
|
for (range, child) in iter {
|
||||||
match *child {
|
match *child {
|
||||||
ParChild::Spacing(amount) => {
|
ParChild::Spacing(amount) => {
|
||||||
layouter.push_spacing(range, amount);
|
items.push(ParItem::Spacing(amount));
|
||||||
|
ranges.push(range);
|
||||||
}
|
}
|
||||||
ParChild::Text(ref node, align) => {
|
ParChild::Text(_, ref props, align) => {
|
||||||
layouter.push_text(ctx, range, &node.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) => {
|
ParChild::Any(ref node, align) => {
|
||||||
for frame in node.layout(ctx, &layouter.areas) {
|
for frame in node.layout(ctx, areas) {
|
||||||
layouter.push_frame(range.clone(), frame, align);
|
items.push(ParItem::Frame(frame, align));
|
||||||
|
ranges.push(range.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
layouter.finish()
|
Self { dir: par.dir, bidi, items, ranges }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ParNode> for AnyNode {
|
|
||||||
fn from(par: ParNode) -> Self {
|
|
||||||
Self::new(par)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ParLayouter<'a> {
|
|
||||||
dir: Dir,
|
|
||||||
line_spacing: Length,
|
|
||||||
bidi: &'a BidiInfo<'a>,
|
|
||||||
areas: Areas,
|
|
||||||
finished: Vec<Frame>,
|
|
||||||
stack: Vec<(Length, Frame, Align)>,
|
|
||||||
stack_size: Size,
|
|
||||||
line: Line,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Line {
|
|
||||||
items: Vec<LineItem>,
|
|
||||||
width: Length,
|
|
||||||
top: Length,
|
|
||||||
bottom: Length,
|
|
||||||
ruler: Align,
|
|
||||||
hard: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
dir,
|
|
||||||
line_spacing,
|
|
||||||
bidi,
|
|
||||||
areas,
|
|
||||||
finished: vec![],
|
|
||||||
stack: vec![],
|
|
||||||
stack_size: Size::ZERO,
|
|
||||||
line: Line::new(true),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push horizontal spacing.
|
/// Find first-fit line breaks and build the paragraph.
|
||||||
fn push_spacing(&mut self, range: Range<usize>, amount: Length) {
|
fn build(self, ctx: &mut LayoutContext, areas: Areas, par: &ParNode) -> Vec<Frame> {
|
||||||
let amount = amount.min(self.areas.current.width - self.line.width);
|
let mut start = 0;
|
||||||
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,
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
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.
|
|
||||||
let mut last = None;
|
let mut last = None;
|
||||||
|
let mut stack = LineStack::new(par.line_spacing, areas);
|
||||||
|
|
||||||
// Create an iterator over the line break opportunities.
|
// TODO: Provide line break opportunities on alignment changes.
|
||||||
let text = &self.bidi.text[range.clone()];
|
let mut iter = LineBreakIterator::new(self.bidi.text).peekable();
|
||||||
let mut iter = LineBreakIterator::new(text).peekable();
|
|
||||||
|
|
||||||
|
// Find suitable line breaks.
|
||||||
while let Some(&(end, mandatory)) = iter.peek() {
|
while let Some(&(end, mandatory)) = iter.peek() {
|
||||||
// Slice the line of text.
|
assert!(start <= end);
|
||||||
let end = range.start + end;
|
|
||||||
let line = &self.bidi.text[start .. end];
|
|
||||||
|
|
||||||
// Remove trailing newline and spacing at the end of lines.
|
let line = LineLayout::new(&self, start .. end, ctx);
|
||||||
let mut line = line.trim_end_matches(is_newline);
|
let size = line.measure().0;
|
||||||
if end != range.end {
|
|
||||||
line = line.trim_end();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shape the line.
|
// Find out whether the line fits.
|
||||||
let frame = shape(line, dir, &mut ctx.env.fonts, props);
|
if stack.fits(size) {
|
||||||
|
|
||||||
// Find out whether the runs still fits into the line.
|
|
||||||
if self.usable().fits(frame.size) {
|
|
||||||
if mandatory {
|
if mandatory {
|
||||||
// We have to break here because the text contained a hard
|
stack.push(line);
|
||||||
// line break like "\n".
|
|
||||||
self.push_frame(start .. end, frame, align);
|
|
||||||
self.finish_line(true);
|
|
||||||
start = end;
|
start = end;
|
||||||
last = None;
|
last = None;
|
||||||
|
if end == self.bidi.text.len() {
|
||||||
|
stack.push(LineLayout::new(&self, end .. end, ctx));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Still fits, so we remember it and try making the line
|
last = Some((line, end));
|
||||||
// even longer.
|
|
||||||
last = Some((frame, end));
|
|
||||||
}
|
}
|
||||||
} else if let Some((frame, pos)) = last.take() {
|
} else if let Some((line, end)) = last.take() {
|
||||||
// The line we just tried doesn't fit. So we write the line up
|
stack.push(line);
|
||||||
// to the last position.
|
stack.prepare(size.height);
|
||||||
self.push_frame(start .. pos, frame, align);
|
start = end;
|
||||||
self.finish_line(false);
|
|
||||||
start = pos;
|
|
||||||
|
|
||||||
// Retry writing just the single piece.
|
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
// Since `last` is `None`, we are at the first piece behind a
|
stack.push(line);
|
||||||
// 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);
|
|
||||||
start = end;
|
start = end;
|
||||||
}
|
}
|
||||||
|
|
||||||
iter.next();
|
iter.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leftovers.
|
if let Some((line, _)) = last {
|
||||||
if let Some((frame, pos)) = last {
|
stack.push(line);
|
||||||
self.push_frame(start .. pos, frame, align);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stack.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_frame(&mut self, range: Range<usize>, frame: Frame, align: Align) {
|
/// Find the index of the item whose range contains the `text_offset`.
|
||||||
// When the alignment of the last pushed frame (stored in the "ruler")
|
#[track_caller]
|
||||||
// is further to the end than the new `frame`, we need a line break.
|
fn find(&self, text_offset: usize) -> usize {
|
||||||
//
|
find_range(&self.ranges, text_offset).unwrap()
|
||||||
// For example
|
}
|
||||||
// ```
|
}
|
||||||
// #align(right)[First] #align(center)[Second]
|
|
||||||
// ```
|
impl ParItem<'_> {
|
||||||
// would be laid out as:
|
/// The size and baseline of the item.
|
||||||
// +----------------------------+
|
pub fn measure(&self) -> (Size, Length) {
|
||||||
// | First |
|
match self {
|
||||||
// | Second |
|
Self::Spacing(amount) => (Size::new(*amount, Length::ZERO), Length::ZERO),
|
||||||
// +----------------------------+
|
Self::Text(shaped, _) => shaped.measure(),
|
||||||
if self.line.ruler > align {
|
Self::Frame(frame, _) => (frame.size, frame.baseline),
|
||||||
self.finish_line(false);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Find out whether the area still has enough space for this frame.
|
/// Split a range of text into runs of consistent direction.
|
||||||
if !self.usable().fits(frame.size) && self.line.width > Length::ZERO {
|
fn split_runs(bidi: &BidiInfo, range: Range, mut f: impl FnMut(Range, Dir)) {
|
||||||
self.finish_line(false);
|
let levels = &bidi.levels[range.clone()];
|
||||||
|
|
||||||
// Here, we can directly check whether the frame fits into
|
let mut start = range.start;
|
||||||
// `areas.current` since we just called `finish_line`.
|
let mut last = match levels.first() {
|
||||||
while !self.areas.current.fits(frame.size) {
|
Some(&level) => level,
|
||||||
if self.areas.in_full_last() {
|
None => return,
|
||||||
// The frame fits nowhere.
|
};
|
||||||
// TODO: Should this be placed into the first area or the last?
|
|
||||||
// TODO: Produce diagnostic once the necessary spans exist.
|
// Split into runs with the same embedding level.
|
||||||
break;
|
for (idx, &level) in levels.iter().enumerate() {
|
||||||
} else {
|
let end = range.start + idx;
|
||||||
self.finish_area();
|
if last != level {
|
||||||
}
|
f(start .. end, last.dir());
|
||||||
|
start = end;
|
||||||
|
}
|
||||||
|
last = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
f(start .. range.end, last.dir());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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],
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Slice out the relevant items and ranges.
|
||||||
|
let mut items = &par.items[first_idx ..= last_idx];
|
||||||
|
let ranges = &par.ranges[first_idx ..= last_idx];
|
||||||
|
|
||||||
|
// 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
items = rest;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A line can contain frames with different alignments. Their exact
|
// Reshape the start item if it's split in half.
|
||||||
// positions are calculated later depending on the alignments.
|
let mut first = None;
|
||||||
let Frame { size, baseline, .. } = frame;
|
if let Some((ParItem::Text(shaped, align), rest)) = items.split_first() {
|
||||||
self.line.items.push(LineItem { range, frame, align });
|
let range = &par.ranges[first_idx];
|
||||||
self.line.width += size.width;
|
let start = line.start - range.start;
|
||||||
self.line.top = self.line.top.max(baseline);
|
let end = line.end.min(range.end) - range.start;
|
||||||
self.line.bottom = self.line.bottom.max(size.height - baseline);
|
if start != end {
|
||||||
self.line.ruler = align;
|
let reshaped = shaped.reshape(start .. end, &mut ctx.env.fonts);
|
||||||
|
first = Some(ParItem::Text(reshaped, *align));
|
||||||
|
}
|
||||||
|
items = rest;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { par, line, first, items, last, ranges }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn usable(&self) -> Size {
|
/// Measure the size of the line without actually building its frame.
|
||||||
// Space occupied by previous lines is already removed from
|
fn measure(&self) -> (Size, Length) {
|
||||||
// `areas.current`, but the width of the current line needs to be
|
let mut width = Length::ZERO;
|
||||||
// subtracted to make sure the frame fits.
|
let mut top = Length::ZERO;
|
||||||
let mut usable = self.areas.current;
|
let mut bottom = Length::ZERO;
|
||||||
usable.width -= self.line.width;
|
|
||||||
usable
|
for item in self.iter() {
|
||||||
|
let (size, baseline) = item.measure();
|
||||||
|
width += size.width;
|
||||||
|
top = top.max(baseline);
|
||||||
|
bottom = bottom.max(size.height - baseline);
|
||||||
|
}
|
||||||
|
|
||||||
|
(Size::new(width, top + bottom), top)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_line(&mut self, hard: bool) {
|
/// Build the line's frame.
|
||||||
let mut line = mem::replace(&mut self.line, Line::new(hard));
|
fn build(&self, width: Length) -> Frame {
|
||||||
if !line.hard && line.items.is_empty() {
|
let (size, baseline) = self.measure();
|
||||||
|
let full_size = Size::new(size.width.max(width), size.height);
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate through the line's items in visual order.
|
||||||
|
fn reordered(&self, mut f: impl FnMut(&ParItem<'a>)) {
|
||||||
|
if self.line.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BiDi reordering.
|
// Find the paragraph that contains the frame.
|
||||||
line.reorder(&self.bidi);
|
let para = self
|
||||||
|
.par
|
||||||
|
.bidi
|
||||||
|
.paragraphs
|
||||||
|
.iter()
|
||||||
|
.find(|para| para.range.contains(&self.line.start))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let full_size = {
|
// Compute the reordered ranges in visual order (left to right).
|
||||||
let expand = self.areas.expand.horizontal;
|
let (levels, runs) = self.par.bidi.visual_runs(para, self.line.clone());
|
||||||
let full = self.areas.full.width;
|
|
||||||
Size::new(expand.resolve(line.width, full), line.top + line.bottom)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut output = Frame::new(full_size, line.top + line.bottom);
|
// Find the items for each run.
|
||||||
let mut offset = Length::ZERO;
|
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;
|
||||||
|
|
||||||
for item in line.items {
|
// Provide the items forwards or backwards depending on the run's
|
||||||
// Align along the x axis.
|
// direction.
|
||||||
let x = item.align.resolve(if self.dir.is_positive() {
|
if levels[run.start].is_ltr() {
|
||||||
offset .. full_size.width - line.width + offset
|
for item in range {
|
||||||
|
f(self.get(item));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
full_size.width - line.width + offset .. offset
|
for item in range.rev() {
|
||||||
});
|
f(self.get(item));
|
||||||
|
}
|
||||||
offset += item.frame.size.width;
|
}
|
||||||
|
|
||||||
let pos = Point::new(x, line.top - item.frame.baseline);
|
|
||||||
output.push_frame(pos, item.frame);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add line spacing, but only between lines, not after the last line.
|
/// Find the index of the item whose range contains the `text_offset`.
|
||||||
if !self.stack.is_empty() {
|
#[track_caller]
|
||||||
self.stack_size.height += self.line_spacing;
|
fn find(&self, text_offset: usize) -> usize {
|
||||||
|
find_range(self.ranges, text_offset).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
line_spacing,
|
||||||
|
areas,
|
||||||
|
finished: vec![],
|
||||||
|
lines: vec![],
|
||||||
|
size: Size::ZERO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fits(&self, size: Size) -> bool {
|
||||||
|
self.areas.current.fits(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare(&mut self, height: Length) {
|
||||||
|
if !self.areas.current.height.fits(height) && !self.areas.in_full_last() {
|
||||||
|
self.finish_area();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&mut self, line: LineLayout<'a>) {
|
||||||
|
let size = line.measure().0;
|
||||||
|
|
||||||
|
if !self.lines.is_empty() {
|
||||||
|
self.size.height += self.line_spacing;
|
||||||
self.areas.current.height -= self.line_spacing;
|
self.areas.current.height -= self.line_spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.stack.push((self.stack_size.height, output, line.ruler));
|
self.size.width = self.size.width.max(size.width);
|
||||||
self.stack_size.height += full_size.height;
|
self.size.height += size.height;
|
||||||
self.stack_size.width = self.stack_size.width.max(full_size.width);
|
self.areas.current.height -= size.height;
|
||||||
self.areas.current.height -= full_size.height;
|
self.lines.push(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_area(&mut self) {
|
fn finish_area(&mut self) {
|
||||||
let mut output = Frame::new(self.stack_size, Length::ZERO);
|
let expand = self.areas.expand.horizontal;
|
||||||
let mut baseline = None;
|
let full = self.areas.full.width;
|
||||||
|
self.size.width = expand.resolve(self.size.width, full);
|
||||||
|
|
||||||
for (before, line, align) in mem::take(&mut self.stack) {
|
let mut output = Frame::new(self.size, self.size.height);
|
||||||
// Align along the x axis.
|
let mut y = Length::ZERO;
|
||||||
let x = align.resolve(if self.dir.is_positive() {
|
let mut first = true;
|
||||||
Length::ZERO .. self.stack_size.width - line.size.width
|
|
||||||
} else {
|
|
||||||
self.stack_size.width - line.size.width .. Length::ZERO
|
|
||||||
});
|
|
||||||
|
|
||||||
let pos = Point::new(x, before);
|
for line in mem::take(&mut self.lines) {
|
||||||
baseline.get_or_insert(pos.y + line.baseline);
|
let frame = line.build(self.size.width);
|
||||||
output.push_frame(pos, line);
|
let height = frame.size.height;
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(baseline) = baseline {
|
if first {
|
||||||
output.baseline = baseline;
|
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.finished.push(output);
|
||||||
self.areas.next();
|
self.areas.next();
|
||||||
self.stack_size = Size::ZERO;
|
self.size = Size::ZERO;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(mut self) -> Vec<Frame> {
|
fn finish(mut self) -> Vec<Frame> {
|
||||||
self.finish_line(false);
|
|
||||||
self.finish_area();
|
self.finish_area();
|
||||||
self.finished
|
self.finished
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Line {
|
/// Helper methods for BiDi levels.
|
||||||
fn new(hard: bool) -> Self {
|
|
||||||
Self {
|
|
||||||
items: vec![],
|
|
||||||
width: Length::ZERO,
|
|
||||||
top: Length::ZERO,
|
|
||||||
bottom: Length::ZERO,
|
|
||||||
ruler: Align::Start,
|
|
||||||
hard,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find the paragraph that contains the frame.
|
|
||||||
let para = 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) = 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)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trait LevelExt: Sized {
|
trait LevelExt: Sized {
|
||||||
fn from_dir(dir: Dir) -> Option<Self>;
|
fn from_dir(dir: Dir) -> Option<Self>;
|
||||||
fn dir(self) -> Dir;
|
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 {
|
impl Debug for ParChild {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Spacing(amount) => write!(f, "Spacing({:?})", amount),
|
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) => {
|
Self::Any(any, align) => {
|
||||||
f.debug_tuple("Any").field(any).field(align).finish()
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
use std::fmt::{self, Debug, Formatter};
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
use fontdock::FaceId;
|
use fontdock::FaceId;
|
||||||
use rustybuzz::UnicodeBuffer;
|
use rustybuzz::UnicodeBuffer;
|
||||||
use ttf_parser::GlyphId;
|
use ttf_parser::GlyphId;
|
||||||
@ -8,7 +11,12 @@ use crate::exec::FontProps;
|
|||||||
use crate::geom::{Dir, Length, Point, Size};
|
use crate::geom::{Dir, Length, Point, Size};
|
||||||
|
|
||||||
/// Shape text into a frame containing [`ShapedText`] runs.
|
/// 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 iter = props.families.iter();
|
||||||
let mut results = vec![];
|
let mut results = vec![];
|
||||||
shape_segment(&mut results, text, dir, loader, props, iter, None);
|
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);
|
let mut frame = Frame::new(Size::new(Length::ZERO, top + bottom), top);
|
||||||
|
|
||||||
for shaped in results {
|
for shaped in results {
|
||||||
let offset = frame.size.width;
|
let offset = frame.size.width;
|
||||||
frame.size.width += shaped.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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
frame
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
/// 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 bottom = convert(i32::from(-props.bottom_edge.lookup(ttf)));
|
||||||
let mut shaped = ShapedText::new(id, props.size, top, bottom, props.color);
|
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.
|
// Fill the buffer with our text.
|
||||||
let mut buffer = UnicodeBuffer::new();
|
let mut buffer = UnicodeBuffer::new();
|
||||||
buffer.push_str(text);
|
buffer.push_str(text);
|
||||||
|
@ -116,39 +116,40 @@ impl StackLayouter {
|
|||||||
size = Size::new(width, width / aspect);
|
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 output = Frame::new(full_size, full_size.height);
|
||||||
let mut baseline = None;
|
let mut first = true;
|
||||||
|
|
||||||
|
let full_size = full_size.switch(self.main);
|
||||||
for (before, frame, aligns) in std::mem::take(&mut self.frames) {
|
for (before, frame, aligns) in std::mem::take(&mut self.frames) {
|
||||||
let child_size = frame.size.switch(self.main);
|
let child_size = frame.size.switch(self.main);
|
||||||
|
|
||||||
// Align along the main axis.
|
// Align along the main axis.
|
||||||
let main = aligns.main.resolve(if self.dirs.main.is_positive() {
|
let main = aligns.main.resolve(
|
||||||
let after_with_self = self.size.main - before;
|
self.dirs.main,
|
||||||
before .. full_size.main - after_with_self
|
if self.dirs.main.is_positive() {
|
||||||
} else {
|
before .. before + full_size.main - self.size.main
|
||||||
let before_with_self = before + child_size.main;
|
} else {
|
||||||
let after = self.size.main - (before + child_size.main);
|
self.size.main - (before + child_size.main)
|
||||||
full_size.main - before_with_self .. after
|
.. full_size.main - (before + child_size.main)
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Align along the cross axis.
|
// Align along the cross axis.
|
||||||
let cross = aligns.cross.resolve(if self.dirs.cross.is_positive() {
|
let cross = aligns.cross.resolve(
|
||||||
Length::ZERO .. full_size.cross - child_size.cross
|
self.dirs.cross,
|
||||||
} else {
|
Length::ZERO .. full_size.cross - child_size.cross,
|
||||||
full_size.cross - child_size.cross .. Length::ZERO
|
);
|
||||||
});
|
|
||||||
|
|
||||||
let pos = Gen::new(main, cross).switch(self.main).to_point();
|
let pos = Gen::new(main, cross).switch(self.main).to_point();
|
||||||
baseline.get_or_insert(pos.y + frame.baseline);
|
if first {
|
||||||
output.push_frame(pos, frame);
|
output.baseline = pos.y + frame.baseline;
|
||||||
}
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(baseline) = baseline {
|
output.push_frame(pos, frame);
|
||||||
output.baseline = baseline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.finished.push(output);
|
self.finished.push(output);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user