Merge text runs during paragraph layout

This commit is contained in:
Laurenz 2022-04-12 13:46:34 +02:00
parent 1101a8370f
commit 56968bc0d6
6 changed files with 239 additions and 205 deletions

View File

@ -15,7 +15,7 @@ enum Last {
Supportive, Supportive,
} }
impl<'a, T: Merge> CollapsingBuilder<'a, T> { impl<'a, T> CollapsingBuilder<'a, T> {
/// Create a new style-vec builder. /// Create a new style-vec builder.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@ -82,45 +82,19 @@ impl<'a, T: Merge> CollapsingBuilder<'a, T> {
fn flush(&mut self, supportive: bool) { fn flush(&mut self, supportive: bool) {
for (item, styles, strength) in self.staged.drain(..) { for (item, styles, strength) in self.staged.drain(..) {
if supportive || strength.is_none() { if supportive || strength.is_none() {
push_merging(&mut self.builder, item, styles); self.builder.push(item, styles);
} }
} }
} }
/// Push a new item into the style vector. /// Push a new item into the style vector.
fn push(&mut self, item: T, styles: StyleChain<'a>) { fn push(&mut self, item: T, styles: StyleChain<'a>) {
push_merging(&mut self.builder, item, styles); self.builder.push(item, styles);
} }
} }
/// Push an item into a style-vec builder, trying to merging it with the impl<'a, T> Default for CollapsingBuilder<'a, T> {
/// previous item.
fn push_merging<'a, T: Merge>(
builder: &mut StyleVecBuilder<'a, T>,
item: T,
styles: StyleChain<'a>,
) {
if let Some((prev_item, prev_styles)) = builder.last_mut() {
if styles == prev_styles {
if prev_item.merge(&item) {
return;
}
}
}
builder.push(item, styles);
}
impl<'a, T: Merge> Default for CollapsingBuilder<'a, T> {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
} }
} }
/// Defines if and how to merge two adjacent items in a [`CollapsingBuilder`].
pub trait Merge {
/// Try to merge the items, returning whether they were merged.
///
/// Defaults to not merging.
fn merge(&mut self, next: &Self) -> bool;
}

View File

@ -111,3 +111,9 @@ assign_impl!(Fraction += Fraction);
assign_impl!(Fraction -= Fraction); assign_impl!(Fraction -= Fraction);
assign_impl!(Fraction *= f64); assign_impl!(Fraction *= f64);
assign_impl!(Fraction /= f64); assign_impl!(Fraction /= f64);
impl Sum for Fraction {
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
Self(iter.map(|s| s.0).sum())
}
}

View File

@ -62,12 +62,6 @@ impl Layout for FlowNode {
} }
} }
impl Merge for FlowChild {
fn merge(&mut self, _: &Self) -> bool {
false
}
}
impl Debug for FlowNode { impl Debug for FlowNode {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str("Flow ")?; f.write_str("Flow ")?;

View File

@ -9,8 +9,8 @@ pub use typst_macros::node;
pub use crate::diag::{with_alternative, At, Error, StrResult, TypError, TypResult}; pub use crate::diag::{with_alternative, At, Error, StrResult, TypError, TypResult};
pub use crate::eval::{ pub use crate::eval::{
Arg, Args, Array, Cast, Content, Dict, Fold, Func, Key, Layout, LayoutNode, Merge, Arg, Args, Array, Cast, Content, Dict, Fold, Func, Key, Layout, LayoutNode, Node,
Node, RawAlign, RawLength, RawStroke, Regions, Resolve, Scope, Show, ShowNode, Smart, RawAlign, RawLength, RawStroke, Regions, Resolve, Scope, Show, ShowNode, Smart,
StyleChain, StyleMap, StyleVec, Value, StyleChain, StyleMap, StyleVec, Value,
}; };
pub use crate::frame::*; pub use crate::frame::*;

View File

@ -7,7 +7,7 @@ use super::{shape, Lang, ShapedText, TextNode};
use crate::font::FontStore; use crate::font::FontStore;
use crate::library::layout::Spacing; use crate::library::layout::Spacing;
use crate::library::prelude::*; use crate::library::prelude::*;
use crate::util::{ArcExt, EcoString, RangeExt, SliceExt}; use crate::util::{ArcExt, EcoString, SliceExt};
/// Arrange text, spacing and inline-level nodes into a paragraph. /// Arrange text, spacing and inline-level nodes into a paragraph.
#[derive(Hash)] #[derive(Hash)]
@ -54,38 +54,6 @@ impl ParNode {
} }
} }
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.
fn collect_text(&self) -> String {
let mut text = String::new();
for string in self.strings() {
text.push_str(string);
}
text
}
/// The range of each item in the collected text.
fn ranges(&self) -> impl Iterator<Item = Range> + '_ {
let mut cursor = 0;
self.strings().map(move |string| {
let start = cursor;
cursor += string.len();
start .. cursor
})
}
/// The string representation of each child.
fn strings(&self) -> impl Iterator<Item = &str> {
self.0.items().map(|child| match child {
ParChild::Text(text) => text,
ParChild::Spacing(_) => " ",
ParChild::Node(_) => "\u{FFFC}",
})
}
}
impl Layout for ParNode { impl Layout for ParNode {
fn layout( fn layout(
&self, &self,
@ -93,12 +61,13 @@ impl Layout for ParNode {
regions: &Regions, regions: &Regions,
styles: StyleChain, styles: StyleChain,
) -> TypResult<Vec<Arc<Frame>>> { ) -> TypResult<Vec<Arc<Frame>>> {
// Collect all text into one string and perform BiDi analysis. // Collect all text into one string for BiDi analysis.
let text = self.collect_text(); let (text, segments) = collect(self, &styles);
// Prepare paragraph layout by building a representation on which we can // Perform BiDi analysis and then prepare paragraph layout by building a
// do line breaking without layouting each and every line from scratch. // representation on which we can do line breaking without layouting
let p = prepare(ctx, self, &text, regions, &styles)?; // each and every line from scratch.
let p = prepare(ctx, self, &text, segments, regions, styles)?;
// Break the paragraph into lines. // Break the paragraph into lines.
let lines = linebreak(&p, &mut ctx.fonts, regions.first.x); let lines = linebreak(&p, &mut ctx.fonts, regions.first.x);
@ -125,17 +94,6 @@ impl Debug for ParChild {
} }
} }
impl Merge for ParChild {
fn merge(&mut self, next: &Self) -> bool {
if let (Self::Text(left), Self::Text(right)) = (self, next) {
left.push_str(right);
true
} else {
false
}
}
}
/// A horizontal alignment. /// A horizontal alignment.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct HorizontalAlign(pub RawAlign); pub struct HorizontalAlign(pub RawAlign);
@ -213,6 +171,11 @@ impl LinebreakNode {
/// Range of a substring of text. /// Range of a substring of text.
type Range = std::ops::Range<usize>; type Range = std::ops::Range<usize>;
// The characters by which spacing and nodes are replaced in the paragraph's
// full text.
const SPACING_REPLACE: char = ' ';
const NODE_REPLACE: char = '\u{FFFC}';
/// A paragraph representation in which children are already layouted and text /// A paragraph representation in which children are already layouted and text
/// is already preshaped. /// is already preshaped.
/// ///
@ -222,25 +185,32 @@ type Range = std::ops::Range<usize>;
struct Preparation<'a> { struct Preparation<'a> {
/// Bidirectional text embedding levels for the paragraph. /// Bidirectional text embedding levels for the paragraph.
bidi: BidiInfo<'a>, bidi: BidiInfo<'a>,
/// Text runs, spacing and layouted nodes.
items: Vec<Item<'a>>,
/// The styles shared by all children.
styles: StyleChain<'a>,
/// The paragraph's children. /// The paragraph's children.
children: &'a StyleVec<ParChild>, children: &'a StyleVec<ParChild>,
/// Spacing, separated text runs and layouted nodes.
items: Vec<ParItem<'a>>,
/// The ranges of the items in `bidi.text`.
ranges: Vec<Range>,
/// The shared styles.
styles: StyleChain<'a>,
} }
impl<'a> Preparation<'a> { impl<'a> Preparation<'a> {
/// Find the item whose range contains the `text_offset`. /// Find the item which is at the `text_offset`.
fn find(&self, text_offset: usize) -> Option<&ParItem<'a>> { fn find(&self, text_offset: usize) -> Option<&Item<'a>> {
self.find_idx(text_offset).map(|idx| &self.items[idx]) self.find_idx_and_offset(text_offset).map(|(idx, _)| &self.items[idx])
} }
/// Find the index of the item whose range contains the `text_offset`. /// Find the index and text offset of the item which is at the
fn find_idx(&self, text_offset: usize) -> Option<usize> { /// `text_offset`.
self.ranges.binary_search_by(|r| r.locate(text_offset)).ok() fn find_idx_and_offset(&self, text_offset: usize) -> Option<(usize, usize)> {
let mut cursor = 0;
for (idx, item) in self.items.iter().enumerate() {
let end = cursor + item.len();
if (cursor .. end).contains(&text_offset) {
return Some((idx, cursor));
}
cursor = end;
}
None
} }
/// Get a style property, but only if it is the same for all children of the /// Get a style property, but only if it is the same for all children of the
@ -253,19 +223,43 @@ impl<'a> Preparation<'a> {
} }
} }
/// A segment of one or multiple collapsed children.
#[derive(Debug, Copy, Clone)]
enum Segment<'a> {
/// One or multiple collapsed text or text-equivalent children. Stores how
/// long the segment is (in bytes of the full text string).
Text(usize),
/// Horizontal spacing between other segments.
Spacing(Spacing),
/// An arbitrary inline-level layout node.
Node(&'a LayoutNode),
}
impl Segment<'_> {
/// The text length of the item.
fn len(&self) -> usize {
match *self {
Self::Text(len) => len,
Self::Spacing(_) => SPACING_REPLACE.len_utf8(),
Self::Node(_) => NODE_REPLACE.len_utf8(),
}
}
}
/// A prepared item in a paragraph layout. /// A prepared item in a paragraph layout.
enum ParItem<'a> { #[derive(Debug)]
enum Item<'a> {
/// A shaped text run with consistent direction.
Text(ShapedText<'a>),
/// Absolute spacing between other items. /// Absolute spacing between other items.
Absolute(Length), Absolute(Length),
/// Fractional spacing between other items. /// Fractional spacing between other items.
Fractional(Fraction), Fractional(Fraction),
/// A shaped text run with consistent direction.
Text(ShapedText<'a>),
/// A layouted child node. /// A layouted child node.
Frame(Frame), Frame(Frame),
} }
impl<'a> ParItem<'a> { impl<'a> Item<'a> {
/// If this a text item, return it. /// If this a text item, return it.
fn text(&self) -> Option<&ShapedText<'a>> { fn text(&self) -> Option<&ShapedText<'a>> {
match self { match self {
@ -273,6 +267,25 @@ impl<'a> ParItem<'a> {
_ => None, _ => None,
} }
} }
/// The text length of the item.
fn len(&self) -> usize {
match self {
Self::Text(shaped) => shaped.text.len(),
Self::Absolute(_) | Self::Fractional(_) => SPACING_REPLACE.len_utf8(),
Self::Frame(_) => NODE_REPLACE.len_utf8(),
}
}
/// The natural width of the item.
fn width(&self) -> Length {
match self {
Item::Text(shaped) => shaped.width,
Item::Absolute(v) => *v,
Item::Fractional(_) => Length::zero(),
Item::Frame(frame) => frame.size.x,
}
}
} }
/// A layouted line, consisting of a sequence of layouted paragraph items that /// A layouted line, consisting of a sequence of layouted paragraph items that
@ -287,23 +300,17 @@ impl<'a> ParItem<'a> {
struct Line<'a> { struct Line<'a> {
/// Bidi information about the paragraph. /// Bidi information about the paragraph.
bidi: &'a BidiInfo<'a>, bidi: &'a BidiInfo<'a>,
/// The range the line spans in the paragraph. /// The (untrimmed) range the line spans in the paragraph.
range: Range, range: Range,
/// A reshaped text item if the line sliced up a text item at the start. /// A reshaped text item if the line sliced up a text item at the start.
first: Option<ParItem<'a>>, first: Option<Item<'a>>,
/// Middle items which don't need to be reprocessed. /// Middle items which don't need to be reprocessed.
items: &'a [ParItem<'a>], items: &'a [Item<'a>],
/// A reshaped text item if the line sliced up a text item at the end. If /// A reshaped text item if the line sliced up a text item at the end. If
/// there is only one text item, this takes precedence over `first`. /// there is only one text item, this takes precedence over `first`.
last: Option<ParItem<'a>>, last: Option<Item<'a>>,
/// The ranges, indexed as `[first, ..items, last]`. The ranges for `first`
/// and `last` aren't trimmed to the line, but it doesn't matter because
/// we're just checking which range an index falls into.
ranges: &'a [Range],
/// The width of the line. /// The width of the line.
width: Length, width: Length,
/// The sum of fractions in the line.
fr: Fraction,
/// Whether the line ends at a mandatory break. /// Whether the line ends at a mandatory break.
mandatory: bool, mandatory: bool,
/// Whether the line ends with a hyphen or dash, either naturally or through /// Whether the line ends with a hyphen or dash, either naturally or through
@ -313,24 +320,36 @@ struct Line<'a> {
impl<'a> Line<'a> { impl<'a> Line<'a> {
/// Iterate over the line's items. /// Iterate over the line's items.
fn items(&self) -> impl Iterator<Item = &ParItem<'a>> { fn items(&self) -> impl Iterator<Item = &Item<'a>> {
self.first.iter().chain(self.items).chain(&self.last) self.first.iter().chain(self.items).chain(&self.last)
} }
/// Find the index of the item whose range contains the `text_offset`. /// Get the item at the index.
fn find(&self, text_offset: usize) -> Option<usize> { fn get(&self, index: usize) -> Option<&Item<'a>> {
self.ranges.binary_search_by(|r| r.locate(text_offset)).ok() self.items().nth(index)
} }
/// Get the item at the index. /// Find the index of the item whose range contains the `text_offset`.
fn get(&self, index: usize) -> Option<&ParItem<'a>> { fn find(&self, text_offset: usize) -> usize {
self.items().nth(index) let mut idx = 0;
let mut cursor = self.range.start;
for item in self.items() {
let end = cursor + item.len();
if (cursor .. end).contains(&text_offset) {
return idx;
}
cursor = end;
idx += 1;
}
idx.saturating_sub(1)
} }
// How many justifiable glyphs the line contains. // How many justifiable glyphs the line contains.
fn justifiables(&self) -> usize { fn justifiables(&self) -> usize {
let mut count = 0; let mut count = 0;
for shaped in self.items().filter_map(ParItem::text) { for shaped in self.items().filter_map(Item::text) {
count += shaped.justifiables(); count += shaped.justifiables();
} }
count count
@ -339,11 +358,67 @@ impl<'a> Line<'a> {
/// How much of the line is stretchable spaces. /// How much of the line is stretchable spaces.
fn stretch(&self) -> Length { fn stretch(&self) -> Length {
let mut stretch = Length::zero(); let mut stretch = Length::zero();
for shaped in self.items().filter_map(ParItem::text) { for shaped in self.items().filter_map(Item::text) {
stretch += shaped.stretch(); stretch += shaped.stretch();
} }
stretch stretch
} }
/// The sum of fractions in the line.
fn fr(&self) -> Fraction {
self.items()
.filter_map(|item| match item {
Item::Fractional(fr) => Some(*fr),
_ => None,
})
.sum()
}
}
/// Collect all text of the paragraph into one string. This also performs
/// string-level preprocessing like case transformations.
fn collect<'a>(
par: &'a ParNode,
styles: &'a StyleChain<'a>,
) -> (String, Vec<(Segment<'a>, StyleChain<'a>)>) {
let mut full = String::new();
let mut segments = vec![];
for (child, map) in par.0.iter() {
let styles = map.chain(&styles);
let segment = match child {
ParChild::Text(text) => {
let prev = full.len();
if let Some(case) = styles.get(TextNode::CASE) {
full.push_str(&case.apply(text));
} else {
full.push_str(text);
}
Segment::Text(full.len() - prev)
}
ParChild::Spacing(spacing) => {
full.push(SPACING_REPLACE);
Segment::Spacing(*spacing)
}
ParChild::Node(node) => {
full.push(NODE_REPLACE);
Segment::Node(node)
}
};
if let (Some((Segment::Text(last_len), last_styles)), Segment::Text(len)) =
(segments.last_mut(), segment)
{
if *last_styles == styles {
*last_len += len;
continue;
}
}
segments.push((segment, styles));
}
(full, segments)
} }
/// Prepare paragraph layout by shaping the whole paragraph and layouting all /// Prepare paragraph layout by shaping the whole paragraph and layouting all
@ -352,8 +427,9 @@ fn prepare<'a>(
ctx: &mut Context, ctx: &mut Context,
par: &'a ParNode, par: &'a ParNode,
text: &'a str, text: &'a str,
segments: Vec<(Segment<'a>, StyleChain<'a>)>,
regions: &Regions, regions: &Regions,
styles: &'a StyleChain, styles: StyleChain<'a>,
) -> TypResult<Preparation<'a>> { ) -> TypResult<Preparation<'a>> {
let bidi = BidiInfo::new(&text, match styles.get(TextNode::DIR) { let bidi = BidiInfo::new(&text, match styles.get(TextNode::DIR) {
Dir::LTR => Some(Level::ltr()), Dir::LTR => Some(Level::ltr()),
@ -362,38 +438,33 @@ fn prepare<'a>(
}); });
let mut items = vec![]; let mut items = vec![];
let mut ranges = vec![]; let mut cursor = 0;
// Layout the children and collect them into items. // Layout the children and collect them into items.
for (range, (child, map)) in par.ranges().zip(par.0.iter()) { for (segment, styles) in segments {
let styles = map.chain(styles); match segment {
match child { Segment::Text(len) => {
ParChild::Text(_) => { // TODO: Also split by script.
// TODO: Also split by language. let mut start = cursor;
let mut cursor = range.start; for (level, count) in bidi.levels[cursor .. cursor + len].group() {
for (level, count) in bidi.levels[range].group() { let end = start + count;
let start = cursor; let text = &bidi.text[start .. end];
cursor += count;
let subrange = start .. cursor;
let text = &bidi.text[subrange.clone()];
let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL };
let shaped = shape(&mut ctx.fonts, text, styles, dir); let shaped = shape(&mut ctx.fonts, text, styles, dir);
items.push(ParItem::Text(shaped)); items.push(Item::Text(shaped));
ranges.push(subrange); start = end;
} }
} }
ParChild::Spacing(spacing) => match *spacing { Segment::Spacing(spacing) => match spacing {
Spacing::Relative(v) => { Spacing::Relative(v) => {
let resolved = v.resolve(styles).relative_to(regions.base.x); let resolved = v.resolve(styles).relative_to(regions.base.x);
items.push(ParItem::Absolute(resolved)); items.push(Item::Absolute(resolved));
ranges.push(range);
} }
Spacing::Fractional(v) => { Spacing::Fractional(v) => {
items.push(ParItem::Fractional(v)); items.push(Item::Fractional(v));
ranges.push(range);
} }
}, },
ParChild::Node(node) => { Segment::Node(node) => {
// Prevent margin overhang in the inline node except if there's // Prevent margin overhang in the inline node except if there's
// just this one. // just this one.
let local; let local;
@ -407,19 +478,14 @@ fn prepare<'a>(
let size = Size::new(regions.first.x, regions.base.y); let size = Size::new(regions.first.x, regions.base.y);
let pod = Regions::one(size, regions.base, Spec::splat(false)); let pod = Regions::one(size, regions.base, Spec::splat(false));
let frame = node.layout(ctx, &pod, styles)?.remove(0); let frame = node.layout(ctx, &pod, styles)?.remove(0);
items.push(ParItem::Frame(Arc::take(frame))); items.push(Item::Frame(Arc::take(frame)));
ranges.push(range);
} }
} }
cursor += segment.len();
} }
Ok(Preparation { Ok(Preparation { bidi, items, styles, children: &par.0 })
bidi,
children: &par.0,
items,
ranges,
styles: *styles,
})
} }
/// Find suitable linebreaks. /// Find suitable linebreaks.
@ -428,12 +494,10 @@ fn linebreak<'a>(
fonts: &mut FontStore, fonts: &mut FontStore,
width: Length, width: Length,
) -> Vec<Line<'a>> { ) -> Vec<Line<'a>> {
let breaker = match p.styles.get(ParNode::LINEBREAKS) { match p.styles.get(ParNode::LINEBREAKS) {
Linebreaks::Simple => linebreak_simple, Linebreaks::Simple => linebreak_simple(p, fonts, width),
Linebreaks::Optimized => linebreak_optimized, Linebreaks::Optimized => linebreak_optimized(p, fonts, width),
}; }
breaker(p, fonts, width)
} }
/// Perform line breaking in simple first-fit style. This means that we build /// Perform line breaking in simple first-fit style. This means that we build
@ -578,10 +642,12 @@ fn linebreak_optimized<'a>(
ratio.powi(3).abs() ratio.powi(3).abs()
}; };
// Penalize hyphens and especially two consecutive hyphens. // Penalize hyphens.
if hyphen { if hyphen {
cost += HYPH_COST; cost += HYPH_COST;
} }
// Penalize two consecutive dashes (not necessarily hyphens) extra.
if attempt.dash && pred.line.dash { if attempt.dash && pred.line.dash {
cost += CONSECUTIVE_DASH_COST; cost += CONSECUTIVE_DASH_COST;
} }
@ -617,7 +683,7 @@ fn linebreak_optimized<'a>(
/// Returns for each breakpoint the text index, whether the break is mandatory /// Returns for each breakpoint the text index, whether the break is mandatory
/// (after `\n`) and whether a hyphen is required (when breaking inside of a /// (after `\n`) and whether a hyphen is required (when breaking inside of a
/// word). /// word).
fn breakpoints<'a>(p: &'a Preparation) -> impl Iterator<Item = (usize, bool, bool)> + 'a { fn breakpoints<'a>(p: &'a Preparation) -> Breakpoints<'a> {
Breakpoints { Breakpoints {
p, p,
linebreaks: LineBreakIterator::new(p.bidi.text), linebreaks: LineBreakIterator::new(p.bidi.text),
@ -726,26 +792,31 @@ fn line<'a>(
mandatory: bool, mandatory: bool,
hyphen: bool, hyphen: bool,
) -> Line<'a> { ) -> Line<'a> {
// Find the items which bound the text range. // Find the last item.
let last_idx = p.find_idx(range.end.saturating_sub(1)).unwrap(); let (last_idx, last_offset) =
let first_idx = if range.is_empty() { p.find_idx_and_offset(range.end.saturating_sub(1)).unwrap();
last_idx
// Find the first item.
let (first_idx, first_offset) = if range.is_empty() {
(last_idx, last_offset)
} else { } else {
p.find_idx(range.start).unwrap() p.find_idx_and_offset(range.start).unwrap()
}; };
// Slice out the relevant items. // Slice out the relevant items.
let mut items = &p.items[first_idx ..= last_idx]; let mut items = &p.items[first_idx ..= last_idx];
let mut width = Length::zero();
// Reshape the last item if it's split in half. // Reshape the last item if it's split in half.
let mut last = None; let mut last = None;
let mut dash = false; let mut dash = false;
if let Some((ParItem::Text(shaped), before)) = items.split_last() { if let Some((Item::Text(shaped), before)) = items.split_last() {
// Compute the range we want to shape, trimming whitespace at the // Compute the range we want to shape, trimming whitespace at the
// end of the line. // end of the line.
let base = p.ranges[last_idx].start; let base = last_offset;
let start = range.start.max(base); let start = range.start.max(last_offset);
let trimmed = p.bidi.text[start .. range.end].trim_end(); let end = range.end;
let trimmed = p.bidi.text[start .. end].trim_end();
let shy = trimmed.ends_with('\u{ad}'); let shy = trimmed.ends_with('\u{ad}');
dash = hyphen || shy || trimmed.ends_with(['-', '', '—']); dash = hyphen || shy || trimmed.ends_with(['-', '', '—']);
@ -766,7 +837,8 @@ fn line<'a>(
if hyphen || shy { if hyphen || shy {
reshaped.push_hyphen(fonts); reshaped.push_hyphen(fonts);
} }
last = Some(ParItem::Text(reshaped)); width += reshaped.width;
last = Some(Item::Text(reshaped));
} }
items = before; items = before;
@ -775,35 +847,28 @@ fn line<'a>(
// Reshape the start item if it's split in half. // Reshape the start item if it's split in half.
let mut first = None; let mut first = None;
if let Some((ParItem::Text(shaped), after)) = items.split_first() { if let Some((Item::Text(shaped), after)) = items.split_first() {
// Compute the range we want to shape. // Compute the range we want to shape.
let Range { start: base, end: first_end } = p.ranges[first_idx]; let base = first_offset;
let start = range.start; let start = range.start;
let end = range.end.min(first_end); let end = range.end.min(first_offset + shaped.text.len());
// Reshape if necessary. // Reshape if necessary.
if end - start < shaped.text.len() { if end - start < shaped.text.len() {
if start < end { if start < end {
let shifted = start - base .. end - base; let shifted = start - base .. end - base;
let reshaped = shaped.reshape(fonts, shifted); let reshaped = shaped.reshape(fonts, shifted);
first = Some(ParItem::Text(reshaped)); width += reshaped.width;
first = Some(Item::Text(reshaped));
} }
items = after; items = after;
} }
} }
let mut width = Length::zero(); // Measure the inner items.
let mut fr = Fraction::zero(); for item in items {
width += item.width();
// Measure the size of the line.
for item in first.iter().chain(items).chain(&last) {
match item {
ParItem::Absolute(v) => width += *v,
ParItem::Fractional(v) => fr += *v,
ParItem::Text(shaped) => width += shaped.width,
ParItem::Frame(frame) => width += frame.size.x,
}
} }
Line { Line {
@ -812,9 +877,7 @@ fn line<'a>(
first, first,
items, items,
last, last,
ranges: &p.ranges[first_idx ..= last_idx],
width, width,
fr,
mandatory, mandatory,
dash, dash,
} }
@ -834,7 +897,7 @@ fn stack(
// Determine the paragraph's width: Full width of the region if we // Determine the paragraph's width: Full width of the region if we
// should expand or there's fractional spacing, fit-to-width otherwise. // should expand or there's fractional spacing, fit-to-width otherwise.
let mut width = regions.first.x; let mut width = regions.first.x;
if !regions.expand.x && lines.iter().all(|line| line.fr.is_zero()) { if !regions.expand.x && lines.iter().all(|line| line.fr().is_zero()) {
width = lines.iter().map(|line| line.width).max().unwrap_or_default(); width = lines.iter().map(|line| line.width).max().unwrap_or_default();
} }
@ -887,7 +950,7 @@ fn commit(
let reordered = reorder(line); let reordered = reorder(line);
// Handle hanging punctuation to the left. // Handle hanging punctuation to the left.
if let Some(ParItem::Text(text)) = reordered.first() { if let Some(Item::Text(text)) = reordered.first() {
if let Some(glyph) = text.glyphs.first() { if let Some(glyph) = text.glyphs.first() {
if text.styles.get(TextNode::OVERHANG) { if text.styles.get(TextNode::OVERHANG) {
let start = text.dir.is_positive(); let start = text.dir.is_positive();
@ -899,7 +962,7 @@ fn commit(
} }
// Handle hanging punctuation to the right. // Handle hanging punctuation to the right.
if let Some(ParItem::Text(text)) = reordered.last() { if let Some(Item::Text(text)) = reordered.last() {
if let Some(glyph) = text.glyphs.last() { if let Some(glyph) = text.glyphs.last() {
if text.styles.get(TextNode::OVERHANG) if text.styles.get(TextNode::OVERHANG)
&& (reordered.len() > 1 || text.glyphs.len() > 1) && (reordered.len() > 1 || text.glyphs.len() > 1)
@ -912,12 +975,13 @@ fn commit(
} }
// Determine how much to justify each space. // Determine how much to justify each space.
let fr = line.fr();
let mut justification = Length::zero(); let mut justification = Length::zero();
if remaining < Length::zero() if remaining < Length::zero()
|| (justify || (justify
&& !line.mandatory && !line.mandatory
&& line.range.end < line.bidi.text.len() && line.range.end < line.bidi.text.len()
&& line.fr.is_zero()) && fr.is_zero())
{ {
let justifiables = line.justifiables(); let justifiables = line.justifiables();
if justifiables > 0 { if justifiables > 0 {
@ -933,16 +997,16 @@ fn commit(
let mut frames = vec![]; let mut frames = vec![];
for item in reordered { for item in reordered {
let frame = match item { let frame = match item {
ParItem::Absolute(v) => { Item::Absolute(v) => {
offset += *v; offset += *v;
continue; continue;
} }
ParItem::Fractional(v) => { Item::Fractional(v) => {
offset += v.share(line.fr, remaining); offset += v.share(fr, remaining);
continue; continue;
} }
ParItem::Text(shaped) => shaped.build(fonts, justification), Item::Text(shaped) => shaped.build(fonts, justification),
ParItem::Frame(frame) => frame.clone(), Item::Frame(frame) => frame.clone(),
}; };
let width = frame.size.x; let width = frame.size.x;
@ -967,7 +1031,7 @@ fn commit(
} }
/// Return a line's items in visual order. /// Return a line's items in visual order.
fn reorder<'a>(line: &'a Line<'a>) -> Vec<&'a ParItem<'a>> { fn reorder<'a>(line: &'a Line<'a>) -> Vec<&'a Item<'a>> {
let mut reordered = vec![]; let mut reordered = vec![];
// The bidi crate doesn't like empty lines. // The bidi crate doesn't like empty lines.
@ -988,8 +1052,8 @@ fn reorder<'a>(line: &'a Line<'a>) -> Vec<&'a ParItem<'a>> {
// Collect the reordered items. // Collect the reordered items.
for run in runs { for run in runs {
let first_idx = line.find(run.start).unwrap(); let first_idx = line.find(run.start);
let last_idx = line.find(run.end - 1).unwrap(); let last_idx = line.find(run.end - 1);
let range = first_idx ..= last_idx; let range = first_idx ..= last_idx;
// Provide the items forwards or backwards depending on the run's // Provide the items forwards or backwards depending on the run's

View File

@ -15,7 +15,7 @@ use crate::util::SliceExt;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ShapedText<'a> { pub struct ShapedText<'a> {
/// The text that was shaped. /// The text that was shaped.
pub text: Cow<'a, str>, pub text: &'a str,
/// The text direction. /// The text direction.
pub dir: Dir, pub dir: Dir,
/// The text's style properties. /// The text's style properties.
@ -182,7 +182,7 @@ impl<'a> ShapedText<'a> {
) -> ShapedText<'a> { ) -> ShapedText<'a> {
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
Self { Self {
text: Cow::Borrowed(&self.text[text_range]), text: &self.text[text_range],
dir: self.dir, dir: self.dir,
styles: self.styles, styles: self.styles,
size: self.size, size: self.size,
@ -298,10 +298,6 @@ pub fn shape<'a>(
dir: Dir, dir: Dir,
) -> ShapedText<'a> { ) -> ShapedText<'a> {
let size = styles.get(TextNode::SIZE); let size = styles.get(TextNode::SIZE);
let text = match styles.get(TextNode::CASE) {
Some(case) => Cow::Owned(case.apply(text)),
None => Cow::Borrowed(text),
};
let mut ctx = ShapingContext { let mut ctx = ShapingContext {
fonts, fonts,