Merge pull request #7087 from dotCooCoo/feat/every-line-composer-shaped-area

Type: the Every-line Composer sets mixed sizes in shaped and wrapped area type
This commit is contained in:
BFlat
2026-11-13 23:03:29 +07:00
committed by GitHub
9 changed files with 834 additions and 49 deletions
+82 -17
View File
@@ -61,10 +61,51 @@ pub(crate) struct Params {
pub hyphen_limit: usize,
}
struct Node {
/// The search gave up: it ran past its budget ([`Measure::line`]).
#[derive(Debug)]
pub(crate) struct Exhausted;
/// Where each line of a paragraph goes and how wide it is: the measure a line gets may depend on
/// the lines set above it (their heights move it down a shaped frame, or past a wrap object).
pub(crate) trait Measure {
/// What the measure of the next line depends on; breaks reached with equal states are
/// interchangeable from there on.
type State: Clone + PartialEq;
/// The state of the paragraph's first line.
fn first(&mut self) -> Self::State;
/// The width of the line of glyphs `pos..end` set from `state`, and the state of the line
/// after it; `Ok(None)` when the line can't be set (the frame is full), and [`Exhausted`] when
/// the search is to stop.
fn line(&mut self, state: &Self::State, pos: usize, end: usize) -> Result<Option<(f64, Self::State)>, Exhausted>;
/// Whether a line from `state` longer than `pos..end`, which [`Measure::line`] just measured,
/// may get more room than it. While it may, a line from `state` that is too long, or that
/// can't be set, does not end the reach of `state`. Lines from a state are all as wide by
/// default.
fn may_widen(&mut self, _state: &Self::State, _pos: usize, _end: usize) -> bool {
false
}
}
/// Line widths by line number: `width(k)` for line `k`, lines from `uniform_from` on all as wide.
struct ByLine<'a> {
width: &'a dyn Fn(usize) -> f64,
uniform_from: usize,
}
impl Measure for ByLine<'_> {
type State = usize;
fn first(&mut self) -> usize {
0
}
fn line(&mut self, &line: &usize, _: usize, _: usize) -> Result<Option<(f64, usize)>, Exhausted> {
Ok(Some(((self.width)(line), (line + 1).min(self.uniform_from))))
}
}
struct Node<S> {
pos: usize,
/// Line number of the next line, clamped to `uniform_from`.
line: usize,
/// The measure of the next line (its line number, clamped to `uniform_from`, for [`compose`]).
state: S,
fitness: u8,
demerits: f64,
/// Lines in a row ending with a hyphen up to this break (at most `hyphen_limit`, or 1 with no
@@ -131,6 +172,19 @@ pub(crate) fn compose(
cands: &[Breakpoint],
flex: &[Flex],
p: &Params,
) -> Option<Vec<(usize, bool)>> {
compose_with(g, &mut ByLine { width, uniform_from: p.uniform_from }, start_credit, end_credit, cands, flex, p)
}
/// [`compose`] with each line's width from `measure`, which follows where the lines above it went.
pub(crate) fn compose_with<M: Measure>(
g: &[SGlyph],
measure: &mut M,
start_credit: &dyn Fn(usize) -> f64,
end_credit: &dyn Fn(usize) -> f64,
cands: &[Breakpoint],
flex: &[Flex],
p: &Params,
) -> Option<Vec<(usize, bool)>> {
let n = g.len();
if n == 0 {
@@ -165,14 +219,14 @@ pub(crate) fn compose(
all.push(Breakpoint { end: n, hyphen: 0.0 });
let (adj_demerits, hyphen_penalty) =
if p.ragged { (ADJ_DEMERITS_RAGGED, HYPHEN_PENALTY_RAGGED) } else { (ADJ_DEMERITS_JUSTIFIED, HYPHEN_PENALTY) };
let mut nodes = vec![Node { pos: 0, line: 0, fitness: 1, demerits: 0.0, hyphens: 0, prev: None }];
let mut nodes = vec![Node { pos: 0, state: measure.first(), fitness: 1, demerits: 0.0, hyphens: 0, prev: None }];
let mut active: Vec<usize> = vec![0];
for bp in &all {
let last = bp.end == n;
let hyphen = bp.hyphen > 0.0;
// `automatic` is true for a hyphenation point and false for a soft hyphen typed in the text.
let automatic = hyphen && !bp.end.checked_sub(1).and_then(|k| g.get(k)).is_some_and(SGlyph::is_soft_hyphen);
let mut best: Vec<(usize, u8, usize, f64, usize)> = vec![]; // (line, fitness, hyphens, demerits, from)
let mut best: Vec<(M::State, u8, usize, f64, usize)> = vec![]; // (state, fitness, hyphens, demerits, from)
let mut keep = Vec::with_capacity(active.len());
for &ai in &active {
let Some(a) = nodes.get(ai) else { continue };
@@ -182,13 +236,25 @@ pub(crate) fn compose(
// last glyph's advance, so a longer line from the same node is never narrower.
let end = if hyphen || t <= a.pos { 0.0 } else { line_end_credit(t) };
let natural = w[t] - w[a.pos] - credit - end + bp.hyphen;
let lw = width(a.line);
// Adjustment ratio, badness and whether the line ends the active node's reach.
let Some(ratio) = adjustment_ratio(natural, lw, st[t] - st[a.pos], sh[t] - sh[a.pos], last, p) else { continue };
if ratio < -1.0 {
// Too long already; longer lines from this node only get worse.
// A line that can't be set (no room left in the frame) ends this node's reach, unless a
// longer line may be placed elsewhere (a taller line steps down the frame by its own
// leading).
let Some((lw, next)) = measure.line(&a.state, a.pos, bp.end).ok()? else {
if measure.may_widen(&a.state, a.pos, bp.end) {
keep.push(ai);
}
continue;
}
};
// Adjustment ratio, badness and whether the line ends the active node's reach.
let ratio = adjustment_ratio(natural, lw, st[t] - st[a.pos], sh[t] - sh[a.pos], last, p);
let Some(ratio) = ratio.filter(|&r| r >= -1.0 || r.is_nan()) else {
// Too long already; longer lines from this node only get worse, unless they may
// get more room (a taller line set lower down a shaped frame).
if measure.may_widen(&a.state, a.pos, bp.end) {
keep.push(ai);
}
continue;
};
keep.push(ai);
if automatic && p.hyphen_limit > 0 && a.hyphens >= p.hyphen_limit {
continue;
@@ -214,22 +280,21 @@ pub(crate) fn compose(
d += adj_demerits;
}
let total = a.demerits + d;
let line = (a.line + 1).min(p.uniform_from);
// With a limit, breaks reached with different runs of hyphenated lines stay apart.
let hyphens = match (hyphen, p.hyphen_limit) {
(false, _) => 0,
(true, 0) => 1,
(true, limit) => (a.hyphens + 1).min(limit),
};
match best.iter_mut().find(|b| b.0 == line && b.1 == fit && b.2 == hyphens) {
match best.iter_mut().find(|b| b.0 == next && b.1 == fit && b.2 == hyphens) {
Some(b) if b.3 <= total => {}
Some(b) => *b = (line, fit, hyphens, total, ai),
None => best.push((line, fit, hyphens, total, ai)),
Some(b) => *b = (next, fit, hyphens, total, ai),
None => best.push((next, fit, hyphens, total, ai)),
}
}
active = keep;
for (line, fitness, hyphens, demerits, from) in best {
nodes.push(Node { pos: bp.end, line, fitness, demerits, hyphens, prev: Some(from) });
for (state, fitness, hyphens, demerits, from) in best {
nodes.push(Node { pos: bp.end, state, fitness, demerits, hyphens, prev: Some(from) });
active.push(nodes.len() - 1);
}
if active.is_empty() || active.len() > MAX_ACTIVE {
+1
View File
@@ -31,6 +31,7 @@ mod flow;
mod on_path;
mod region;
mod ruby;
mod shaped;
mod vertical;
pub(crate) use bidi::{is_rtl, para_bidi};
+33 -10
View File
@@ -26,14 +26,18 @@ impl Metrics {
pub(super) fn max(g: &[SGlyph]) -> Option<Self> {
let mut it = g.iter();
let first = Self::of(it.next()?);
Some(it.fold(first, |m, g| Self {
asc: m.asc.max(g.ascent),
desc: m.desc.max(g.descent),
lead: m.lead.max(g.leading),
cap: m.cap.max(g.cap),
xh: m.xh.max(g.xh),
top: m.top.max(Self::of(g).top),
}))
Some(it.fold(first, |m, g| m.union(Self::of(g))))
}
/// The larger of each of the metrics of `self` and `o`: those of a line holding both.
pub(super) fn union(self, o: Self) -> Self {
Self {
asc: self.asc.max(o.asc),
desc: self.desc.max(o.desc),
lead: self.lead.max(o.lead),
cap: self.cap.max(o.cap),
xh: self.xh.max(o.xh),
top: self.top.max(o.top),
}
}
/// Distance from the frame top to the first baseline.
pub(super) fn first_baseline(&self, fb: FirstBaseline, min: f64) -> f64 {
@@ -67,6 +71,9 @@ pub(super) struct Pen<'r> {
/// Baseline steps still allowed over all the frame's cells and placements (shared by this pen's
/// copies): see [`MAX_TOTAL_STEPS`].
pub(super) steps_left: std::rc::Rc<std::cell::Cell<usize>>,
/// What is left of the every-line composer's search over the frame's paragraphs (shared by
/// this pen's copies): see [`super::shaped::Budget`].
pub(super) search: std::rc::Rc<super::shaped::Budget>,
}
/// The largest number of steps layout takes to move a line down a frame cell to find room for it:
@@ -103,9 +110,24 @@ impl Pen<'_> {
let b = self.prev.map_or(0.0, |b| self.next_baseline(b, est));
return Some((b, f64::NEG_INFINITY, f64::INFINITY, false));
};
if !self.queued.is_empty() {
// A further span was measured across the height of the line set before it on its baseline.
// This line gets the part of it that is free across its own height: a taller line can
// reach a wrap object the line before it stayed clear of. A span with no such part is
// left empty, and so is a part the line's height made narrower than `min`.
while !self.queued.is_empty() {
let (b, x0, x1) = self.queued.remove(0);
return Some((b, x0, x1, true));
let Some(r) = regions.get(self.ri) else { return Some((b, x0, x1, true)) };
let mut free = r.spans(b - est.asc, b + est.desc).into_iter().filter_map(|(s, e)| {
let (s, e) = (s.max(x0), e.min(x1));
let room = e - s - ind_l - ind_r;
let narrowed = s > x0 + EPS || e < x1 - EPS;
(room > est.asc.max(1.0) && (!narrowed || room + EPS >= min)).then_some((s, e))
});
let Some((s, e)) = free.next() else { continue };
// An object inside the span splits it: the parts after the first stay queued.
let rest: Vec<_> = free.map(|(s, e)| (b, s, e)).collect();
self.queued.splice(0..0, rest);
return Some((b, s, e, true));
}
loop {
let r = regions.get(self.ri)?;
@@ -841,6 +863,7 @@ fn a_pen_out_of_baseline_steps_finds_no_room() {
next_top: None,
no_room_for: f64::INFINITY,
steps_left: std::rc::Rc::new(std::cell::Cell::new(steps)),
search: Default::default(),
};
assert!(pen(MAX_TOTAL_STEPS).place(est, 0.0, 0.0, 0.0).is_some());
assert!(pen(0).place(est, 0.0, 0.0, 0.0).is_none());
+35 -13
View File
@@ -24,9 +24,15 @@ fn pushes_in(para: &ParaStyle) -> bool {
/// Paragraphs longer than this (glyphs) are always broken line by line.
const MAX_COMPOSE_GLYPHS: usize = 200_000;
/// Whether every line of area type in `regions` is as wide however tall it is and wherever it is set:
/// rectangular cells without text wrap.
fn constant_width(regions: Option<&[Region]>) -> bool {
regions.is_some_and(|r| r.iter().all(|r| (r.rect || r.polys.is_empty()) && r.wraps.is_empty()))
}
/// Every-line composition of paragraph glyphs `sg` if applicable (area text, justified or ragged,
/// with uniform line metrics or in rectangular cells without text wrap); `None` falls back to the
/// greedy single-line composer. With `continued`, the paragraph continues from the frame before,
/// in any frame shape, with or without text wrap); `None` falls back to the greedy single-line
/// composer. With `continued`, the paragraph continues from the frame before,
/// and its first composed line is not its first line. `line_end` is how much narrower a line
/// ending with each glyph is set ([`ParagraphData::line_end`]).
#[allow(clippy::too_many_arguments)]
@@ -66,14 +72,11 @@ fn compose_para(
let m = Metrics::of(&sg[0]);
// Line widths are independent of the breaks when every line has the same metrics, or when
// a line's width does not depend on how far down the frame it is set: rectangular cells
// without text wrap. Then a paragraph whose characters differ in line metrics (type size,
// font, leading, vertical scale, an inline graphic taller than the text) is composed too. In
// other shapes, or with text wrap, a taller line can move the lines below it to where the
// frame is narrower, so such a paragraph is broken line by line.
let plain = pen.regions.is_some_and(|r| r.iter().all(|r| (r.rect || r.polys.is_empty()) && r.wraps.is_empty()));
if !plain && sg.iter().any(|g| Metrics::of(g) != m) {
return None;
}
// without text wrap. In other shapes, or with text wrap, a paragraph whose characters differ
// in line metrics (type size, font, leading, vertical scale, an inline graphic taller than
// the text) can have a taller line move the lines below it to where the frame is narrower:
// each line the composer tries is then placed after the lines it follows (`shaped::Shaped`).
let shaped = !constant_width(pen.regions) && sg.iter().any(|g| Metrics::of(g) != m);
let total: f64 = sg.iter().map(|g| g.adv).sum();
// The same rule as the line loop below: an opening bracket starting a wrapped line gives up
// the space before it.
@@ -86,7 +89,7 @@ fn compose_para(
let mut acc = 0.0;
// A line's width and placement depend on the sum of its indents alone, which the mirrored
// indents of a right-to-left list ([`line_indents`]) keep.
while widths.len() < sg.len() {
while !shaped && widths.len() < sg.len() {
let first = widths.is_empty();
let ind_l = para.laid_out_left_indent() + if first && !continued { para.first_line_indent } else { 0.0 };
// Only the first line's first word is known here. Later lines are placed as the composed
@@ -103,7 +106,11 @@ fn compose_para(
break;
}
}
let last = *widths.last()?;
let last = match widths.last() {
Some(&last) => last,
None if shaped => 0.0,
None => return None,
};
let width = |k: usize| widths.get(k).copied().unwrap_or(last);
// Lines from `uniform_from` on are interchangeable (same width).
let uniform_from = widths.iter().rposition(|&w| (w - last).abs() > EPS).map_or(0, |k| k + 1);
@@ -119,6 +126,14 @@ fn compose_para(
let (strict, loose) = (1.0, f64::INFINITY);
// A line's last character is set narrower, as the line loop sets it.
let end_credit = |end: usize| end.checked_sub(1).and_then(|k| line_end.get(k)).copied().unwrap_or(0.0);
if shaped {
// Both passes share the placements tried and one search budget.
let mirror = mirrors_indents(para, rtl, cx.vertical);
let mut measure = super::shaped::Shaped::new(sg, line_end, para, &start_credit, continued, mirror, pen);
let mut pass = |p: &Params| crate::composer::compose_with(sg, &mut measure, &start_credit, &end_credit, &cands, flex, p);
let lines = pass(&params(strict));
return lines.or_else(|| pass(&params(loose)));
}
let pass = |p: &Params| compose(sg, &width, &start_credit, &end_credit, &cands, flex, p);
pass(&params(strict)).or_else(|| pass(&params(loose)))
}
@@ -142,7 +157,7 @@ fn mirrors_indents(para: &ParaStyle, rtl: bool, vertical: bool) -> bool {
/// the right and the right indent from the left: the bullet of a right-to-left list hangs at the
/// right, and the lines it wraps to line up with the text after the bullet. A list level past the
/// first adds to the left indent ([`ParaStyle::laid_out_left_indent`]), mirrored with it.
fn line_indents(para: &ParaStyle, first: bool, mirror: bool) -> (f64, f64) {
pub(super) fn line_indents(para: &ParaStyle, first: bool, mirror: bool) -> (f64, f64) {
let start = para.laid_out_left_indent() + if first { para.first_line_indent } else { 0.0 };
if mirror { (para.right_indent, start) } else { (start, para.right_indent) }
}
@@ -162,6 +177,7 @@ pub(super) fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], t: &TextObject, reg
next_top: None,
no_room_for: f64::INFINITY,
steps_left: std::rc::Rc::new(std::cell::Cell::new(super::breaking::MAX_TOTAL_STEPS)),
search: Default::default(),
};
let markers = list_markers(t, paras);
for (pi, pr) in paras.iter().enumerate() {
@@ -392,6 +408,12 @@ fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementC
}
// The line's first word, as narrow as the line sets it at both ends.
let word = word_width(sg, *i, line_end, para.kinsoku);
// A composed line in a frame whose lines are not all as wide is placed at its own tallest
// metrics, as the composer placed it (`shaped::Shaped`): its room is the room across its height.
let est = match composed.as_ref().and_then(|lines| lines.get(*para_line)) {
Some(&(end, _)) if end > *i && !constant_width(pen.regions) => sg.get(*i..end).and_then(Metrics::max).unwrap_or(est),
_ => est,
};
// Place, break, then settle the baseline on the line's real metrics (moving on to the
// next row/column if it no longer fits).
loop {
+273
View File
@@ -0,0 +1,273 @@
//! Every-line composition where a line's width depends on the lines above it: area type in a
//! shape other than a rectangle, or with text wrap, whose characters differ in line metrics (type
//! size, font, leading, vertical scale, an inline graphic taller than the text). A taller line
//! moves the lines below it down, to where the frame may be narrower or a wrap object splits the
//! line. Each line the composer tries is placed from where the lines above it left the pen, at its
//! own tallest metrics, as the line loop in [`super::flow`] places a composed line: the span it
//! gets is the room across its whole height.
use std::cell::Cell;
use std::collections::HashMap;
use std::rc::Rc;
use super::breaking::{Metrics, Pen, word_width};
use super::flow::line_indents;
use super::{ParaStyle, SGlyph};
use crate::composer::{Exhausted, Measure};
/// The most lines the composer tries to place for one paragraph before it stops and the paragraph
/// is broken line by line.
const MAX_PROBES: usize = 1_000_000;
/// The most baseline steps placing those lines takes in all (see [`super::breaking::MAX_TOTAL_STEPS`]).
const MAX_STEPS: usize = 2_000_000;
/// The most lines the composer tries to place for all the paragraphs of one frame's layout.
const MAX_LAYOUT_PROBES: usize = 2 * MAX_PROBES;
/// The most baseline steps placing them takes over all the paragraphs of one frame's layout.
const MAX_LAYOUT_STEPS: usize = 2 * MAX_STEPS;
/// What is left of the composer's search over one frame's layout: the lines it may still try to
/// place and the baseline steps placing them may still take. Every paragraph's search takes what
/// it spends from it, and stops when it runs out, or at its own limits of [`MAX_PROBES`] and
/// [`MAX_STEPS`]. It is separate from the layout's own steps ([`Pen::steps_left`]), which the
/// search leaves to the line loop.
pub(super) struct Budget {
probes: Cell<usize>,
steps: Cell<usize>,
}
impl Budget {
/// A budget of `probes` lines to try and `steps` baseline steps.
pub(super) fn new(probes: usize, steps: usize) -> Self {
Self { probes: Cell::new(probes), steps: Cell::new(steps) }
}
/// The lines left to try and the baseline steps left.
#[cfg(test)]
pub(super) fn left(&self) -> (usize, usize) {
(self.probes.get(), self.steps.get())
}
}
impl Default for Budget {
fn default() -> Self {
Self::new(MAX_LAYOUT_PROBES, MAX_LAYOUT_STEPS)
}
}
/// Where a line starts: glyph `pos`, with the pen where the lines above it left it.
struct Start<'r> {
pos: usize,
/// The paragraph's first line (it takes the first-line indent).
first: bool,
pen: Pen<'r>,
/// The line placed at each of the metrics it was tried with: its width and the pen after it,
/// or `None` when the frame has no room for it.
placed: Vec<(Metrics, Option<(f64, Pen<'r>)>)>,
/// The metrics of glyphs `pos..to`, measured so far.
to: usize,
m: Option<Metrics>,
}
/// What a line's placement depends on: its first glyph, whether it is the paragraph's first line,
/// and the pen's cell, baselines, pending space, narrowest word without room and further spans.
type Key = (usize, bool, usize, Option<u64>, Option<u64>, u64, u64, Vec<[u64; 3]>);
fn key(pos: usize, first: bool, pen: &Pen<'_>) -> Key {
(
pos,
first,
pen.ri,
pen.prev.map(f64::to_bits),
pen.next_top.map(f64::to_bits),
pen.pending.to_bits(),
pen.no_room_for.to_bits(),
pen.queued.iter().map(|&(b, x0, x1)| [b.to_bits(), x0.to_bits(), x1.to_bits()]).collect(),
)
}
/// What placing a line reads from the paragraph.
struct Para<'a> {
sg: &'a [SGlyph],
/// How much narrower a line ending with each glyph is set (`ParagraphData::line_end`).
line_end: &'a [f64],
para: &'a ParaStyle,
/// The width a line starting with each glyph gives back (Mojikumi's flush opening bracket).
start_credit: &'a dyn Fn(usize) -> f64,
mirror: bool,
/// The baseline steps left to the search.
steps: Rc<Cell<usize>>,
}
/// The measures of a paragraph's lines in a frame where they depend on the lines above.
pub(super) struct Shaped<'a, 'r> {
para: Para<'a>,
continued: bool,
pen: Pen<'r>,
starts: Vec<Start<'r>>,
known: HashMap<Key, usize>,
/// The lines tried so far, and the most this search may try.
probes: usize,
max_probes: usize,
/// The baseline steps this search started with.
max_steps: usize,
/// The layout's budget, which takes what the search spent when it ends.
budget: Rc<Budget>,
/// The tallest metrics of glyphs `k..` for each `k`, `None` past the last glyph.
rest: Vec<Option<Metrics>>,
}
impl<'a, 'r> Shaped<'a, 'r> {
/// The measures of the lines of glyphs `sg` of paragraph `para`, set from `pen`. `line_end`
/// and `start_credit` are how much narrower a line ending with each glyph is set and how much
/// a line starting with it gives back; `continued`: the paragraph's first line is in the
/// frame before; `mirror`: its indents are mirrored. The search places its lines with a copy
/// of `pen` and takes its baseline steps from the layout's search budget ([`Pen::search`]),
/// so the frame's layout keeps its own steps.
pub(super) fn new(
sg: &'a [SGlyph],
line_end: &'a [f64],
para: &'a ParaStyle,
start_credit: &'a dyn Fn(usize) -> f64,
continued: bool,
mirror: bool,
pen: &Pen<'r>,
) -> Self {
let mut pen = pen.clone();
let budget = Rc::clone(&pen.search);
let max_steps = budget.steps.get().min(pen.steps_left.get()).min(MAX_STEPS);
let steps = Rc::new(Cell::new(max_steps));
pen.steps_left = Rc::clone(&steps);
let mut rest = vec![None; sg.len() + 1];
for (k, g) in sg.iter().enumerate().rev() {
let m = Metrics::of(g);
rest[k] = Some(rest[k + 1].map_or(m, |r: Metrics| r.union(m)));
}
Self {
para: Para { sg, line_end, para, start_credit, mirror, steps },
continued,
pen,
starts: vec![],
known: HashMap::new(),
probes: 0,
max_probes: budget.probes.get().min(MAX_PROBES),
max_steps,
budget,
rest,
}
}
/// The start of a line at glyph `pos` with `pen`: an existing one when its placement is the same.
fn start(&mut self, pos: usize, first: bool, pen: Pen<'r>) -> usize {
let k = key(pos, first, &pen);
if let Some(&id) = self.known.get(&k) {
return id;
}
self.starts.push(Start { pos, first, pen, placed: vec![], to: pos, m: None });
let id = self.starts.len() - 1;
self.known.insert(k, id);
id
}
}
impl Measure for Shaped<'_, '_> {
type State = usize;
fn first(&mut self) -> usize {
let pen = self.pen.clone();
self.start(0, !self.continued, pen)
}
fn line(&mut self, &id: &usize, pos: usize, end: usize) -> Result<Option<(f64, usize)>, Exhausted> {
if self.probes >= self.max_probes {
return Err(Exhausted);
}
self.probes += 1;
let Some(start) = self.starts.get_mut(id) else { return Ok(None) };
let Some((width, pen)) = line(&self.para, start, pos, end)? else { return Ok(None) };
Ok(Some((width, self.start(end, false, pen))))
}
/// A longer line is placed as this one is, and gets the same room, unless a glyph after it
/// changes the line's tallest metrics.
fn may_widen(&mut self, &id: &usize, pos: usize, end: usize) -> bool {
let Some(start) = self.starts.get(id) else { return false };
let m = if start.pos == pos && start.to == end { start.m } else { self.para.sg.get(pos..end).and_then(Metrics::max) };
match (m, self.rest.get(end).copied().flatten()) {
(Some(m), Some(rest)) => m.union(rest) != m,
_ => false,
}
}
}
/// The search's lines tried and baseline steps spent come off the layout's budget, so the
/// paragraphs of one frame share it.
impl Drop for Shaped<'_, '_> {
fn drop(&mut self) {
let spent = self.max_steps.saturating_sub(self.para.steps.get());
self.budget.steps.set(self.budget.steps.get().saturating_sub(spent));
self.budget.probes.set(self.budget.probes.get().saturating_sub(self.probes));
}
}
/// The width of the line of glyphs `pos..end` from `start`, and the pen after it; `None` when
/// the frame has no room for it.
fn line<'r>(p: &Para<'_>, start: &mut Start<'r>, pos: usize, end: usize) -> Result<Option<(f64, Pen<'r>)>, Exhausted> {
if start.pos != pos || end <= pos {
return Ok(None);
}
// The line's tallest metrics, measured on from the glyphs already measured.
if end < start.to {
start.m = p.sg.get(pos..end).and_then(Metrics::max);
start.to = end;
} else if end > start.to {
let more = p.sg.get(start.to..end).and_then(Metrics::max);
start.m = match (start.m, more) {
(Some(m), Some(more)) => Some(m.union(more)),
(m, more) => m.or(more),
};
start.to = end;
}
let Some(m) = start.m else { return Ok(None) };
if let Some((_, placed)) = start.placed.iter().find(|(at, _)| *at == m) {
return Ok(placed.clone());
}
let placed = place(p, start, m)?;
start.placed.push((m, placed.clone()));
Ok(placed)
}
/// The line from `start` placed with metrics `m`: its width and the pen after it, as the line loop
/// places and settles it; `None` when no frame cell has room for it.
fn place<'r>(p: &Para<'_>, start: &Start<'r>, m: Metrics) -> Result<Option<(f64, Pen<'r>)>, Exhausted> {
let (ind_l, ind_r) = line_indents(p.para, start.first, p.mirror);
let word = word_width(p.sg, start.pos, p.line_end, p.para.kinsoku) - (p.start_credit)(start.pos);
let mut pen = start.pen.clone();
loop {
match pen.place(m, ind_l, ind_r, word) {
Some((baseline, x0, x1, _)) if baseline.is_finite() && (x1 - x0).is_finite() => {
if baseline + m.desc > pen.bottom() + 0.01 {
// A further span of the line above, too low for this line: the next row or column.
pen.ri += 1;
pen.prev = None;
pen.pending = 0.0;
pen.queued.clear();
continue;
}
pen.pending = 0.0;
pen.settled(baseline, m);
// Further spans of this line band share the settled baseline.
for queued in &mut pen.queued {
queued.0 = baseline;
}
return Ok(Some((x1 - x0 - ind_l - ind_r, pen)));
}
Some(_) => return Ok(None),
// Out of steps: the frame may still have room further down.
None if p.steps.get() == 0 => return Err(Exhausted),
None => return Ok(None),
}
}
}
+235
View File
@@ -533,6 +533,241 @@ fn composition_counts_the_narrower_line_end() {
assert_eq!(lines(38.0, &half, &[Breakpoint { end: 4, hyphen: 0.0 }]), Some(vec![(4, false), (7, false)]));
}
/// A line measure for `compose_with`: every line is `width` wide, its state is the glyph it starts
/// at, and the search stops once `calls` lines were measured.
struct Measured {
width: f64,
calls: usize,
/// A line from a start past this one finds the frame full.
full_after: usize,
}
impl crate::composer::Measure for Measured {
type State = usize;
fn first(&mut self) -> usize {
0
}
fn line(&mut self, &start: &usize, _: usize, end: usize) -> Result<Option<(f64, usize)>, crate::composer::Exhausted> {
if self.calls == 0 {
return Err(crate::composer::Exhausted);
}
self.calls -= 1;
Ok((start <= self.full_after).then_some((self.width, end)))
}
}
/// The every-line composer with a line measure that follows the lines above: it breaks as with
/// fixed widths when the widths are the same, finds no composition when the frame is full before
/// the paragraph ends, and gives up when the measure's search stops.
#[test]
fn composition_with_a_measure_from_the_lines_above() {
let text = "가나다 라마바";
let glyphs = korean(text, false);
let cands = candidates(text, &glyphs, None, Kinsoku::Hard);
let params = Params { justify_last: false, ragged: true, tolerance: f64::INFINITY, uniform_from: 0, hyphen_limit: 0 };
let with = |calls: usize, full_after: usize| {
let mut measure = Measured { width: 50.0, calls, full_after };
crate::composer::compose_with(&glyphs, &mut measure, &|_| 0.0, &|_| 0.0, &cands, &[], &params)
};
let fixed = compose(&glyphs, &|_| 50.0, &|_| 0.0, &|_| 0.0, &cands, &[], &params);
assert_eq!(fixed, Some(vec![(4, false), (7, false)]));
assert_eq!(with(usize::MAX, usize::MAX), fixed);
assert_eq!(with(usize::MAX, 0), None, "no room for a second line");
assert_eq!(with(1, usize::MAX), None, "the search stopped");
}
/// A line measure for `compose_with` whose lines are `narrow` wide, and `wide` wide when they reach
/// past glyph `from` (a line made taller there, and set lower in a frame that widens).
struct Widening {
narrow: f64,
wide: f64,
from: usize,
}
impl crate::composer::Measure for Widening {
type State = usize;
fn first(&mut self) -> usize {
0
}
fn line(&mut self, _: &usize, _: usize, end: usize) -> Result<Option<(f64, usize)>, crate::composer::Exhausted> {
Ok(Some((if end > self.from { self.wide } else { self.narrow }, end)))
}
fn may_widen(&mut self, _: &usize, _: usize, end: usize) -> bool {
end <= self.from
}
}
/// A line too long for its room does not end the composer's search from its start while a longer
/// line may get more room: the longer line is tried and set.
#[test]
fn composition_tries_a_longer_line_that_gets_more_room() {
let text = "가나다 라마바";
let glyphs = korean(text, false);
let cands = candidates(text, &glyphs, None, Kinsoku::Hard);
let params = Params { justify_last: false, ragged: true, tolerance: f64::INFINITY, uniform_from: 0, hyphen_limit: 0 };
// "가나다" is 30 wide and does not fit 25; the whole paragraph reaches past glyph 4 and fits 100.
let mut measure = Widening { narrow: 25.0, wide: 100.0, from: 4 };
let lines = crate::composer::compose_with(&glyphs, &mut measure, &|_| 0.0, &|_| 0.0, &cands, &[], &params);
assert_eq!(lines, Some(vec![(7, false)]));
}
/// A line measure for `compose_with` whose lines can't be set until they reach past glyph `from`
/// (a line made taller there steps down the frame by its own leading, to a band the shorter
/// line's steps miss), and are `wide` wide from there.
struct Unplaced {
wide: f64,
from: usize,
}
impl crate::composer::Measure for Unplaced {
type State = usize;
fn first(&mut self) -> usize {
0
}
fn line(&mut self, _: &usize, _: usize, end: usize) -> Result<Option<(f64, usize)>, crate::composer::Exhausted> {
Ok((end > self.from).then_some((self.wide, end)))
}
fn may_widen(&mut self, _: &usize, _: usize, end: usize) -> bool {
end <= self.from
}
}
/// A line that can't be set does not end the composer's search from its start while a longer line
/// may be placed elsewhere: the longer line is tried and set.
#[test]
fn composition_tries_a_longer_line_after_one_that_cannot_be_set() {
let text = "가나다 라마바";
let glyphs = korean(text, false);
let cands = candidates(text, &glyphs, None, Kinsoku::Hard);
let params = Params { justify_last: false, ragged: true, tolerance: f64::INFINITY, uniform_from: 0, hyphen_limit: 0 };
// "가나다" finds no room; the whole paragraph reaches past glyph 4 and fits 100.
let mut measure = Unplaced { wide: 100.0, from: 4 };
let lines = crate::composer::compose_with(&glyphs, &mut measure, &|_| 0.0, &|_| 0.0, &cands, &[], &params);
assert_eq!(lines, Some(vec![(7, false)]));
}
/// A further span beside a wrap object goes to the line set in it with the room across that line's
/// own height. A line taller than the one before it on the baseline keeps clear of an object the
/// shorter line did not reach, and moves down when the span has no room for it.
#[test]
fn a_further_span_is_measured_across_the_height_of_its_line() {
use std::cell::Cell;
use std::rc::Rc;
let frame = Rect::new(0.0, 0.0, 400.0, 400.0).to_path(0.1);
let wrap = |r: Rect| vectorcraft_doc::WrapShape {
path: PathData::from_bezpath(&r.to_path(0.1)),
wrap: vectorcraft_doc::TextWrap { offset: 0.0, invert: false },
};
let small = breaking::Metrics { asc: 10.0, desc: 3.0, lead: 14.0, cap: 7.0, xh: 5.0, top: 10.0 };
let tall = breaking::Metrics { asc: 30.0, desc: 9.0, lead: 42.0, cap: 21.0, xh: 15.0, top: 30.0 };
// An object down the middle of the frame splits every line in two, and `above` lies right of
// it over y 20 to 40. A small line after a baseline of 50 is set at 64, over y 54 to 67, clear
// of `above`; a tall line in the span right of it on that baseline covers y 34 to 73.
// The tall line starts with a word `word` wide.
let set = |above: Rect, word: f64| {
let regions = region::Region::cells(&frame, &LayoutOptions::default(), &[wrap(Rect::new(180.0, 0.0, 220.0, 400.0)), wrap(above)]);
let mut pen = breaking::Pen {
regions: Some(&regions),
ri: 0,
prev: Some(50.0),
pending: 0.0,
fb: FirstBaseline::Ascent,
fb_min: 0.0,
queued: vec![],
model: LeadingModel::RomanBaseline,
next_top: None,
no_room_for: f64::INFINITY,
steps_left: Rc::new(Cell::new(breaking::MAX_TOTAL_STEPS)),
search: Default::default(),
};
let left = pen.place(small, 0.0, 0.0, 0.0);
pen.settled(64.0, small);
let right = pen.place(tall, 0.0, 0.0, word);
(left, right)
};
let near = |got: Option<(f64, f64, f64, bool)>, want: (f64, f64, f64, bool)| {
got.is_some_and(|g| (g.0 - want.0).abs() < 0.01 && (g.1 - want.1).abs() < 0.01 && (g.2 - want.2).abs() < 0.01 && g.3 == want.3)
};
// `above` covers the right end of the span: the tall line gets the part left of it.
let (left, right) = set(Rect::new(300.0, 20.0, 400.0, 40.0), 0.0);
assert!(near(left, (64.0, 0.0, 180.0, false)), "{left:?}");
assert!(near(right, (64.0, 220.0, 300.0, true)), "{right:?}");
// That part is 80 wide: a tall line that starts with a wider word goes on the next baseline.
let (_, right) = set(Rect::new(300.0, 20.0, 400.0, 40.0), 100.0);
assert!(near(right, (106.0, 0.0, 180.0, false)), "{right:?}");
// `above` covers the whole span: the tall line goes on the next baseline, left of the object.
let (left, right) = set(Rect::new(220.0, 20.0, 400.0, 40.0), 0.0);
assert!(near(left, (64.0, 0.0, 180.0, false)), "{left:?}");
assert!(near(right, (106.0, 0.0, 180.0, false)), "{right:?}");
// `above` lies below the tall line: the span is as wide for it as for the small line, and it
// takes the span whatever its first word, as a further span always did.
let (_, right) = set(Rect::new(300.0, 300.0, 400.0, 320.0), 200.0);
assert!(near(right, (64.0, 220.0, 400.0, true)), "{right:?}");
}
/// The every-line composer's searches for the paragraphs of one frame share one budget: each takes
/// the lines it tried and the baseline steps it spent from it, the layout's own steps stay as they
/// were, and once the budget runs out the next paragraph's search stops (it is broken line by line).
#[test]
fn shaped_composition_searches_share_the_frame_budget() {
use super::shaped::{Budget, Shaped};
use std::cell::Cell;
use std::rc::Rc;
let text = "aa bb cc dd ee ff gg hh ii jj";
let mut glyphs = full_width(text);
glyphs[3].ascent = 30.0;
let cands = candidates(text, &glyphs, None, Kinsoku::Hard);
// 60 pt wide down to 40 pt, and 30 pt wide below.
let frame = BezPath::from_vec(vec![
PathEl::MoveTo((0.0, 0.0).into()),
PathEl::LineTo((60.0, 0.0).into()),
PathEl::LineTo((60.0, 40.0).into()),
PathEl::LineTo((30.0, 40.0).into()),
PathEl::LineTo((30.0, 400.0).into()),
PathEl::LineTo((0.0, 400.0).into()),
PathEl::ClosePath,
]);
let regions = region::Region::cells(&frame, &LayoutOptions::default(), &[]);
let para = ParaStyle::default();
let params = Params { justify_last: false, ragged: true, tolerance: f64::INFINITY, uniform_from: 0, hyphen_limit: 0 };
let pen = |search: Rc<Budget>| breaking::Pen {
regions: Some(&regions),
ri: 0,
prev: None,
pending: 0.0,
fb: FirstBaseline::Ascent,
fb_min: 0.0,
queued: vec![],
model: LeadingModel::default(),
next_top: None,
no_room_for: f64::INFINITY,
steps_left: Rc::new(Cell::new(breaking::MAX_TOTAL_STEPS)),
search,
};
let compose = |pen: &breaking::Pen<'_>| {
let mut measure = Shaped::new(&glyphs, &[], &para, &|_| 0.0, false, false, pen);
crate::composer::compose_with(&glyphs, &mut measure, &|_| 0.0, &|_| 0.0, &cands, &[], &params)
};
// One paragraph's search, with room to spare.
let one = pen(Rc::new(Budget::default()));
let before = one.search.left();
let lines = compose(&one);
assert!(lines.is_some());
let after = one.search.left();
let (probes, steps) = (before.0 - after.0, before.1 - after.1);
assert!(probes > 0 && steps > 0, "{probes} lines tried, {steps} steps");
assert_eq!(one.steps_left.get(), breaking::MAX_TOTAL_STEPS, "the layout's own steps");
// A budget of three such searches: three paragraphs are composed, the fourth is not.
let three = pen(Rc::new(Budget::new(3 * probes, 3 * steps)));
for k in 0..3 {
assert_eq!(compose(&three), lines, "paragraph {k}");
}
assert_eq!(three.search.left(), (0, 0));
assert_eq!(compose(&three), None, "the budget ran out");
assert_eq!(three.search.left(), (0, 0));
assert_eq!(three.steps_left.get(), breaking::MAX_TOTAL_STEPS, "the layout's own steps");
}
/// The glyphs of `text`, each 10 pt wide, with Korean words kept whole unless `anywhere` (Korean
/// Line Break Anywhere on).
fn korean(text: &str, anywhere: bool) -> Vec<SGlyph> {
+168 -6
View File
@@ -334,14 +334,14 @@ fn composer_x_mixed_sizes() {
assert_eq!(x.line, 0);
assert!(l.lines[0].ascent > l.lines[1].ascent + 1.0, "{:?}", l.lines);
// A rectangle that wraps around an object, and a frame that isn't a rectangle (a corner cut),
// both far below the text, break mixed sizes line by line and still break uniform sizes with
// the every-line composer.
// both far below the text, break mixed sizes and uniform sizes alike with the every-line
// composer.
let below = PathData::from_bezpath(&Rect::new(0.0, 180.0, 20.0, 190.0).to_path(0.1));
for (x, want) in [(&wide, every), (&big, greedy)] {
for x in [&wide, &big] {
let mut t = area(runs(x.clone()), Rect::new(0.0, 0.0, w, 200.0));
t.wrap = vec![vectorcraft_doc::WrapShape { path: below.clone(), wrap: Default::default() }];
t.para.composer = Composer::EveryLine;
assert_eq!(line_texts(&t, &layout(db(), &t)), want, "wrap, {}", x.size);
assert_eq!(line_texts(&t, &layout(db(), &t)), every, "wrap, {}", x.size);
}
let cut = kurbo::BezPath::from_vec(vec![
kurbo::PathEl::MoveTo((0.0, 0.0).into()),
@@ -351,14 +351,176 @@ fn composer_x_mixed_sizes() {
kurbo::PathEl::LineTo((0.0, 200.0).into()),
kurbo::PathEl::ClosePath,
]);
for (x, want) in [(&wide, every), (&big, greedy)] {
for x in [&wide, &big] {
let mut t = area(runs(x.clone()), Rect::ZERO);
t.kind = TextKind::Area { frame: PathData::from_bezpath(&cut) };
t.para.composer = Composer::EveryLine;
assert_eq!(line_texts(&t, &layout(db(), &t)), want, "cut corner, {}", x.size);
assert_eq!(line_texts(&t, &layout(db(), &t)), every, "cut corner, {}", x.size);
}
}
/// Text with the word `X` set in `x` and the rest in 12 pt.
fn with_x(x: CharStyle) -> Vec<TextRun> {
let st = sans(12.0);
vec![
run("Destroy all creatures with toughness ", st.clone()),
run("X", x),
run(" or less. Then draw a card for each creature destroyed this way.", st),
]
}
/// The width the text of [`with_x`] is set in.
fn x_width() -> f64 {
natural_width("Destroy all creatures with toughness X or", 12.0) + 2.0
}
#[test]
fn composer_x_mixed_sizes_in_a_stepped_frame() {
// A frame as wide as the first line down to 40 pt and half as wide below. A 36 pt word makes
// the line it is on reach below the step, where the frame is narrow: the every-line composer
// breaks the paragraph with each line's measure where its height sets it, not as the
// single-line composer does, and every line keeps within its room.
let w = x_width();
let set = |x: CharStyle, composer: Composer| {
let mut t = stepped(w, w * 0.5, 40.0, 300.0);
t.runs = with_x(x);
t.para.composer = composer;
let l = layout(db(), &t);
assert!(past_their_room(&t, &l).is_empty() && !l.overflow, "{composer:?}: {:?}", l.lines);
(line_texts(&t, &l), l)
};
let (every, l) = set(sans(36.0), Composer::EveryLine);
assert_eq!(every, ["Destroy all creatures with toughness", "X or less. Then", "draw a card for each", "creature destroyed", "this way."]);
// The 36 pt line reaches below the step and is set at the narrow measure.
let tall = &l.lines[1];
assert!(tall.ascent > 30.0 && tall.baseline + tall.descent > 40.0 && tall.avail.1 < w * 0.5 + 0.01, "{:?}", l.lines);
let (greedy, _) = set(sans(36.0), Composer::SingleLine);
assert_ne!(every, greedy);
// Uniform sizes keep the breaks they had.
let (uniform, _) = set(sans(12.0), Composer::EveryLine);
assert_eq!(uniform, ["Destroy all creatures with toughness X or", "less. Then draw a card for each creature", "destroyed this way."]);
}
#[test]
fn composer_x_mixed_sizes_around_text_wrap() {
// A wrap object across the middle of the frame from 40 pt down splits the lines beside it in
// two. The line of the 36 pt word, which starts with a 12 pt word, falls beside it, and the
// every-line composer measures each of the two spans across the line's whole height: no
// glyph runs into the object.
let w = x_width();
let object = Rect::new(w * 0.4, 40.0, w * 0.6, 70.0);
let set = |composer: Composer| {
let mut runs = with_x(sans(36.0));
// A no-break space keeps the 36 pt word on the line of the 12 pt word before it.
runs[0] = run("Destroy all creatures with toughness\u{a0}", sans(12.0));
let mut t = area(runs, Rect::new(0.0, 0.0, w, 300.0));
t.wrap = vec![vectorcraft_doc::WrapShape { path: PathData::from_bezpath(&object.to_path(0.1)), wrap: Default::default() }];
t.para.composer = composer;
let l = layout(db(), &t);
(line_texts(&t, &l), l, t)
};
let (every, l, t) = set(Composer::EveryLine);
assert_eq!(
every,
["Destroy all creatures with", "toughness\u{a0}X", "or less. Then", "draw a card for", "each creature", "destroyed this", "way."]
);
assert!(past_their_room(&t, &l).is_empty() && !l.overflow, "{:?}", l.lines);
// Two spans share each baseline beside the object.
assert_eq!(l.lines[1].baseline, l.lines[2].baseline);
assert!(l.lines[1].ascent > 30.0 && l.lines[2].avail.0 > object.x1, "{:?}", l.lines);
let text = t.plain_text();
let hits = l.glyphs.iter().filter(|g| {
let li = &l.lines[g.line];
let across = li.baseline - li.ascent < object.y1 && li.baseline + li.descent > object.y0;
across && !text[g.byte..].starts_with(char::is_whitespace) && g.origin.x < object.x1 && g.origin.x + g.advance > object.x0
});
assert_eq!(hits.count(), 0, "{:?}", l.lines);
let (greedy, _, _) = set(Composer::SingleLine);
assert_ne!(every, greedy);
}
#[test]
fn composer_x_mixed_sizes_in_a_frame_that_widens() {
// A frame as wide as "Destroy all" down to 20 pt and as wide as the first line of
// `composer_x_mixed_sizes_in_a_stepped_frame` below. The second line is too long for the narrow
// part until it takes in the 48 pt word, which sets it lower, where the frame is wide: the
// every-line composer tries that longer line and sets it there.
let w = x_width();
let narrow = natural_width("Destroy all", 12.0) + 2.0;
let set = |composer: Composer| {
let mut t = stepped(narrow, w, 20.0, 300.0);
t.runs = with_x(sans(48.0));
t.para.composer = composer;
let l = layout(db(), &t);
assert!(past_their_room(&t, &l).is_empty() && !l.overflow, "{composer:?}: {:?}", l.lines);
(line_texts(&t, &l), l)
};
let (every, l) = set(Composer::EveryLine);
assert_eq!(every, ["Destroy all", "creatures with toughness X or less.", "Then draw a card for each creature", "destroyed this way."]);
let tall = &l.lines[1];
assert!(tall.ascent > 40.0 && tall.baseline - tall.ascent > 20.0 && tall.avail.1 > narrow + 1.0, "{:?}", l.lines);
let (greedy, _) = set(Composer::SingleLine);
assert_eq!(greedy, ["Destroy all", "creatures", "with toughness X or less. Then draw", "a card for each creature destroyed this", "way."]);
}
#[test]
fn composer_x_mixed_sizes_overflow_a_shaped_frame() {
// A stepped frame too short for the paragraph: the lines that fit are set, in order, each once
// and within its room, and the frame overflows.
let w = x_width();
let mut t = stepped(w, w * 0.5, 40.0, 75.0);
t.runs = with_x(sans(36.0));
t.para.composer = Composer::EveryLine;
let l = layout(db(), &t);
assert!(l.overflow && l.lines.len() >= 2, "{:?}", l.lines);
assert_eq!(l.lines[0].start, 0);
assert!(l.lines.windows(2).all(|p| p[0].end == p[1].start && p[0].start < p[0].end), "{:?}", l.lines);
assert!(past_their_room(&t, &l).is_empty(), "{:?}", l.lines);
assert!(l.lines.iter().all(|li| li.baseline + li.descent <= 75.0 + 0.01), "{:?}", l.lines);
}
#[test]
fn composer_x_mixed_sizes_long_paragraph_in_a_shaped_frame() {
// A long paragraph of mixed sizes in a stepped frame is laid out whole, its lines in order and
// each within its room, whether the composer's search finishes or stops at its limits.
let w = x_width();
let mut t = stepped(w, w * 0.5, 40.0, 100_000.0);
t.runs = (0..150).flat_map(|_| with_x(sans(36.0))).collect();
t.para.composer = Composer::EveryLine;
let l = layout(db(), &t);
assert!(!l.overflow, "{} lines", l.lines.len());
assert_eq!(l.lines.last().map(|li| li.end), Some(t.plain_text().len()));
assert!(l.lines.windows(2).all(|p| p[0].end == p[1].start), "lines out of order");
assert!(past_their_room(&t, &l).is_empty());
}
/// A frame `w` wide down to `y`, and `narrow` wide below it.
fn stepped(w: f64, narrow: f64, y: f64, bottom: f64) -> TextObject {
let frame = kurbo::BezPath::from_vec(vec![
kurbo::PathEl::MoveTo((0.0, 0.0).into()),
kurbo::PathEl::LineTo((w, 0.0).into()),
kurbo::PathEl::LineTo((w, y).into()),
kurbo::PathEl::LineTo((narrow, y).into()),
kurbo::PathEl::LineTo((narrow, bottom).into()),
kurbo::PathEl::LineTo((0.0, bottom).into()),
kurbo::PathEl::ClosePath,
]);
let mut t = area(vec![], Rect::ZERO);
t.kind = TextKind::Area { frame: PathData::from_bezpath(&frame) };
t
}
/// The lines of `l` whose glyphs reach past either end of the room they were set in.
fn past_their_room(t: &TextObject, l: &TextLayout) -> Vec<usize> {
let text = t.plain_text();
let past = |i: usize| {
let (a, b) = l.lines[i].avail;
let mut glyphs = l.glyphs.iter().filter(|g| g.line == i && !text[g.byte..].starts_with(char::is_whitespace));
glyphs.any(|g| g.origin.x < a - 0.01 || g.origin.x + g.advance > b + 0.01)
};
(0..l.lines.len()).filter(|&i| past(i)).collect()
}
#[test]
fn composer_x_mixed_line_metrics() {
// In a rectangle the every-line composer also breaks a paragraph whose characters differ in
+6 -2
View File
@@ -9918,8 +9918,12 @@ justified text gets even word spacing, ragged text (left, centre or right aligne
that fits to the next line to avoid a lone short word on the last line or a full line next to a short one.
Single-line fills each line as far as it goes. Point type and type on a path have no line width, so both composers
give the same result there. A paragraph whose characters differ in line metrics (type size, font, leading or vertical
scale, or an inline graphic taller than the text) is composed with Every-line in rectangular area type, rows and
columns included; in area type of another shape, or with text wrap, it falls back to Single-line.
scale, or an inline graphic taller than the text) is composed with Every-line in area type of any shape, rows,
columns and text wrap included. In a frame that is not a rectangle, or with text wrap, each line it tries is measured
where the lines above it put it, across the line's own height, so a taller line that reaches where the frame is
narrower, or beside a wrap object, gets the room there. Such a paragraph falls back to Single-line when the frame has
no room for all of it, or when the search for its breaks passes VectorCraft's own limits (1,000,000 lines tried and
2,000,000 baseline steps for one paragraph, 2,000,000 lines and 4,000,000 steps for all the paragraphs of a frame).
The UI command `type.lineComposer` (Cmd+Alt+Shift+C, in remote mode `press_key {key: "C", mods: {cmd: true, alt: true,
shift: true}}` or `invoke_menu`) sets `singleLine` when every paragraph of the selected type (or every paragraph the
Type tool's selection touches) uses Every-line, else `everyLine`, and returns the composer it set.
File diff suppressed because one or more lines are too long