Refactor compose for strict Clippy checks

This commit is contained in:
Dylan M. Taylor
2026-10-28 11:39:26 +00:00
parent 07366af1b6
commit 71a579e8da
4 changed files with 2317 additions and 1208 deletions
+85 -60
View File
@@ -526,7 +526,9 @@ pub fn knuth_plass(glyphs: &[Glyph], hyph_after: &[bool], sp: &Spacing, width: &
(items, ig) = self::items(glyphs, hyph_after, &emergency_after, sp);
}
for tolerance in [2.5, 12.0, f64::INFINITY] {
if let Some(b) = kp_pass(&items, &ig, glyphs, sp, width, tolerance, tolerance.is_infinite()) {
if let Some(b) =
kp_pass(PassInput { items: &items, item_glyphs: &ig, glyphs, spacing: sp, width, tolerance, emergency: tolerance.is_infinite() })
{
return b;
}
}
@@ -603,22 +605,78 @@ fn overlong_words(items: &[Item], ig: &[usize], glyphs: &[Glyph], measure: f64)
/// Upper bound on the hyphen-count states tracked per breakpoint.
const MAX_HYPHEN_STATES: usize = 8;
#[allow(clippy::too_many_arguments)]
fn kp_pass(
items: &[Item],
ig: &[usize],
glyphs: &[Glyph],
sp: &Spacing,
width: &dyn Fn(usize) -> f64,
tol: f64,
struct PassInput<'a> {
items: &'a [Item],
item_glyphs: &'a [usize],
glyphs: &'a [Glyph],
spacing: &'a Spacing,
width: &'a dyn Fn(usize) -> f64,
tolerance: f64,
emergency: bool,
) -> Option<Vec<Break>> {
let m = items.len();
// Prefix sums (before item i).
let mut sw = vec![0.0; m + 1];
let mut sy = vec![[0.0; 3]; m + 1];
let mut sz = vec![[0.0; 3]; m + 1];
let mut so = vec![0.0; m + 1];
}
/// Enforce consecutive-hyphen limits and the unjustified hyphenation zone.
fn hyphen_allowed(it: Item, n: Node, sp: &Spacing, widths: &[f64], items: &[Item], target: f64) -> bool {
let limit = sp.hyphen_limit as usize;
if limit > 0 && n.hyphens as usize >= limit {
return false;
}
if it.auto && !sp.justify && sp.hyph_zone > 0.0 && it.zone_from != NONE && it.zone_from > n.pos {
let before = widths[it.zone_from] - n.tw - items[it.zone_from].hang;
if target - before < sp.hyph_zone {
return false;
}
}
true
}
/// Fitness class and cumulative penalty for a candidate line break.
fn break_demerits(n: Node, r: f64, pp: f64, flagged: bool, priority: u8) -> (usize, f64) {
let preference = match priority {
1 if r > 0.0 => 4.0,
2 if r < 0.0 => 4.0,
_ => 1.0,
};
let bad = preference * 100.0 * r.abs().powi(3);
let lp = 10.0 + bad;
let mut d = if pp >= 0.0 {
lp * lp + pp * pp
} else if pp > -INF {
lp * lp - pp * pp
} else {
lp * lp
};
if flagged && n.flagged {
d += 3000.0;
}
let fit = if r < -0.5 {
0
} else if r <= 0.5 {
1
} else if r <= 1.0 {
2
} else {
3
};
if (fit as i32 - n.fitness as i32).abs() > 1 {
d += 3000.0;
}
(fit, n.demerits + d)
}
/// Width, stretch, shrink and overhang totals before each item.
struct PrefixSums {
sw: Vec<f64>,
sy: Vec<[f64; 3]>,
sz: Vec<[f64; 3]>,
so: Vec<f64>,
}
fn prefix_sums(items: &[Item]) -> PrefixSums {
let mut sw = vec![0.0; items.len() + 1];
let mut sy = vec![[0.0; 3]; items.len() + 1];
let mut sz = vec![[0.0; 3]; items.len() + 1];
let mut so = vec![0.0; items.len() + 1];
for (i, it) in items.iter().enumerate() {
let (w, y, z) = match it.kind {
Kind::Box | Kind::Glue => (it.w, it.st, it.sh),
@@ -631,6 +689,13 @@ fn kp_pass(
sz[i + 1][t] = sz[i][t] + z[t];
}
}
PrefixSums { sw, sy, sz, so }
}
fn kp_pass(input: PassInput<'_>) -> Option<Vec<Break>> {
let PassInput { items, item_glyphs, glyphs, spacing: sp, width, tolerance: tol, emergency } = input;
let m = items.len();
let PrefixSums { sw, sy, sz, so } = prefix_sums(items);
let first_box = items.iter().find(|i| i.kind == Kind::Box);
let first_hang = first_box.map_or(0.0, |b| b.hang);
let first_elastic = first_box.map_or([0.0; 2], |b| b.start_elastic);
@@ -695,51 +760,11 @@ fn kp_pass(
if !feasible || (r > tol && !forced) {
continue;
}
if flagged {
// Hyphen limit: a hard constraint.
if limit > 0 && n.hyphens as usize >= limit {
continue;
}
// Hyphenation zone (unjustified): break before the word instead when that leaves
// less than the zone at the line end.
if it.auto && !sp.justify && sp.hyph_zone > 0.0 && it.zone_from != NONE && it.zone_from > n.pos {
let before = sw[it.zone_from] - n.tw - items[it.zone_from].hang;
if target - before < sp.hyph_zone {
continue;
}
}
}
let preference = match sp.kinsoku_priority {
1 if r > 0.0 => 4.0,
2 if r < 0.0 => 4.0,
_ => 1.0,
};
let bad = preference * 100.0 * r.abs().powi(3);
let lp = 10.0 + bad;
let mut d = if pp >= 0.0 {
lp * lp + pp * pp
} else if pp > -INF {
lp * lp - pp * pp
} else {
lp * lp
};
if flagged && n.flagged {
d += 3000.0;
}
let fit = if r < -0.5 {
0
} else if r <= 0.5 {
1
} else if r <= 1.0 {
2
} else {
3
};
if (fit as i32 - n.fitness as i32).abs() > 1 {
d += 3000.0;
if flagged && !hyphen_allowed(it, n, sp, &sw, items, target) {
continue;
}
let (fit, total) = break_demerits(n, r, pp, flagged, sp.kinsoku_priority);
let hs = if flagged { (n.hyphens as usize + 1).min(states - 1) } else { 0 };
let total = n.demerits + d;
let slot = &mut best[fit * states + hs];
if slot.is_none_or(|(bd, _)| total < bd) {
*slot = Some((total, a));
@@ -820,7 +845,7 @@ fn kp_pass(
cur = arena[c].prev;
}
chain.reverse();
Some(breaks_from_positions(items, ig, glyphs, &chain))
Some(breaks_from_positions(items, item_glyphs, glyphs, &chain))
}
fn breaks_from_positions(items: &[Item], ig: &[usize], glyphs: &[Glyph], chain: &[usize]) -> Vec<Break> {
+2163 -1113
View File
File diff suppressed because it is too large Load Diff
+16 -8
View File
@@ -46,6 +46,14 @@ pub(crate) struct NoteMetrics {
descent: f64,
}
#[derive(Clone, Copy)]
pub(crate) struct NoteColumn<'a> {
pub(crate) frame_index: usize,
pub(crate) column_index: usize,
pub(crate) width: f64,
pub(crate) frame: &'a FrameSpec,
}
/// Height of footnotes stacked baseline to baseline, from the top of the footnote area to the
/// last footnote's descent.
fn stack_height<'a>(notes: impl IntoIterator<Item = &'a NoteMetrics>, space_between: f64) -> f64 {
@@ -194,16 +202,16 @@ impl Notes {
}
/// Height the footnotes of column (fi, col) take, with `extra` footnotes added.
#[allow(clippy::too_many_arguments)]
pub fn reserve(&mut self, doc: &Document, fi: usize, col: usize, extra: &[usize], w: f64, f: &FrameSpec, opts: &ComposeOptions) -> f64 {
pub fn reserve(&mut self, doc: &Document, column: NoteColumn<'_>, extra: &[usize], opts: &ComposeOptions) -> f64 {
let o = &doc.footnote_options;
let (space_between, space_before) = (o.space_between, o.space_before);
let mut ms: Vec<NoteMetrics> = self.placed.iter().filter(|p| self.same_area(p, fi, col)).map(|p| p.m).collect();
let mut ms: Vec<NoteMetrics> =
self.placed.iter().filter(|p| self.same_area(p, column.frame_index, column.column_index)).map(|p| p.m).collect();
for &k in extra {
ms.push(self.compose(doc, k, w, f, opts).1);
ms.push(self.compose(doc, k, column.width, column.frame, opts).1);
}
let r = if ms.is_empty() { 0.0 } else { stack_height(&ms, space_between) + space_before };
if self.span { r.max(self.floor.get(&fi).copied().unwrap_or(0.0)) } else { r }
if self.span { r.max(self.floor.get(&column.frame_index).copied().unwrap_or(0.0)) } else { r }
}
/// After spanning notes were placed in column `col` of frame `fi`: whether a line of an earlier
@@ -221,12 +229,12 @@ impl Notes {
hit
}
pub fn place(&mut self, doc: &Document, k: usize, fi: usize, col: usize, w: f64, f: &FrameSpec, opts: &ComposeOptions) {
pub fn place(&mut self, doc: &Document, k: usize, column: NoteColumn<'_>, opts: &ComposeOptions) {
if self.placed.iter().any(|p| p.k == k) {
return;
}
let (text, m) = self.compose(doc, k, w, f, opts);
self.placed.push(Placed { k, fi, col, m, text });
let (text, m) = self.compose(doc, k, column.width, column.frame, opts);
self.placed.push(Placed { k, fi: column.frame_index, col: column.column_index, m, text });
}
/// Stack each column's footnotes against its bottom, rule above the first.
+53 -27
View File
@@ -319,30 +319,50 @@ impl StyleTable<'_> {
/// Resolve and shape one paragraph. Its drop cap ends at byte `drop.0` (`range.start` without
/// one) and takes the character style `drop.1` ("" = none).
pub(crate) fn shape_para(
db: &ScopedFonts<'_>,
styles: &Styles,
story: &Story,
pi: usize,
range: std::ops::Range<usize>,
para_chars: &CharProps,
auto_leading: TypeEnv,
sub: &SubstCtx,
table: &mut StyleTable<'_>,
nested: &[designcraft_doc::NestedStyle],
grep: &[designcraft_doc::GrepStyle],
lines: &[(std::ops::Range<usize>, String)],
drop: (usize, &str),
) -> ShapedPara {
let _ = pi;
let mut glyphs = Vec::with_capacity(range.len());
// Nested line styles lie under nested and GREP styles, and the drop cap's style over them all
// (later overlays win).
pub(crate) struct ShapeInput<'a, 'font> {
pub(crate) db: &'a ScopedFonts<'font>,
pub(crate) styles: &'a Styles,
pub(crate) story: &'a Story,
pub(crate) range: std::ops::Range<usize>,
pub(crate) para_chars: &'a CharProps,
pub(crate) auto_leading: TypeEnv,
pub(crate) sub: &'a SubstCtx,
pub(crate) nested: &'a [designcraft_doc::NestedStyle],
pub(crate) grep: &'a [designcraft_doc::GrepStyle],
pub(crate) lines: &'a [(std::ops::Range<usize>, String)],
pub(crate) drop_cap: (usize, &'a str),
}
pub(crate) fn shape_para(input: ShapeInput<'_, '_>, table: &mut StyleTable<'_>) -> ShapedPara {
let db = input.db;
let sub = input.sub;
let range = input.range.clone();
let auto_leading = input.auto_leading;
let (pieces, auto_tcy) = resolve_pieces(&input, table);
let mut glyphs = shape_pieces(&input, table, pieces);
fit_auto_tcy(&mut glyphs, &auto_tcy);
collapse_tcy(&mut glyphs, sub.vertical);
crate::ruby::reserve(db, table.styles, &mut glyphs, auto_leading.glyph_fallback, sub.vertical);
ShapedPara { glyphs, range }
}
fn resolve_pieces(input: &ShapeInput<'_, '_>, table: &mut StyleTable<'_>) -> (Vec<Piece>, Vec<std::ops::Range<usize>>) {
let db = input.db;
let styles = input.styles;
let story = input.story;
let range = input.range.clone();
let para_chars = input.para_chars;
let auto_leading = input.auto_leading;
let sub = input.sub;
let nested = input.nested;
let grep = input.grep;
let lines = input.lines;
let drop_cap = input.drop_cap;
let mut overlays: Vec<(std::ops::Range<usize>, String)> = lines.to_vec();
// The drop cap's style is the nested styles' leading "through 1 Dropcap" entry (the form IDML
// stores it in). Set (`[None]` included), it restyles that entry; a real one stands in for a
// missing entry. Either way the nested styles after it start after the drop cap.
let set = Some(drop.1).filter(|s| !s.is_empty());
let set = Some(drop_cap.1).filter(|s| !s.is_empty());
let cap_style = set.filter(|s| *s != designcraft_doc::NO_CHAR_STYLE);
let with_cap: Vec<designcraft_doc::NestedStyle>;
let nested = match set {
@@ -357,12 +377,12 @@ pub(crate) fn shape_para(
_ => nested,
};
if !nested.is_empty() || !grep.is_empty() {
overlays.extend(crate::overlay::overlays(&story.text, range.clone(), nested, grep, drop.0));
overlays.extend(crate::overlay::overlays(&story.text, range.clone(), nested, grep, drop_cap.0));
}
if let Some(s) = cap_style
&& drop.0 > range.start
&& drop_cap.0 > range.start
{
overlays.push((range.start..drop.0, s.to_string()));
overlays.push((range.start..drop_cap.0, s.to_string()));
}
// Each run, cut where nested / GREP styles start and end.
let mut segments: Vec<(usize, usize, Option<&str>, &designcraft_doc::CharFormat)> = Vec::new();
@@ -474,6 +494,15 @@ pub(crate) fn shape_para(
pieces.push(Piece { range: k..b, props, style, env: auto_leading, hidden: false, composite_note: false });
}
}
(pieces, auto_tcy)
}
fn shape_pieces(input: &ShapeInput<'_, '_>, table: &mut StyleTable<'_>, pieces: Vec<Piece>) -> Vec<Glyph> {
let db = input.db;
let styles = input.styles;
let story = input.story;
let sub = input.sub;
let mut glyphs = Vec::new();
let mut i = 0;
while i < pieces.len() {
let first = &pieces[i];
@@ -562,10 +591,7 @@ pub(crate) fn shape_para(
}
i = j;
}
fit_auto_tcy(&mut glyphs, &auto_tcy);
collapse_tcy(&mut glyphs, sub.vertical);
crate::ruby::reserve(db, table.styles, &mut glyphs, auto_leading.glyph_fallback, sub.vertical);
ShapedPara { glyphs, range }
glyphs
}
/// Combining marks that start a run are shaped with the letter before them when the two runs