Text: shrink on overflow stops at once when even the smallest scale overflows

This commit is contained in:
Nikita Rakitin
2026-10-14 03:56:19 +03:00
parent 457406a7a5
commit 77de43266e
2 changed files with 35 additions and 2 deletions
+19 -2
View File
@@ -1534,6 +1534,12 @@ fn tab_advance(x: f64, stops: &[Tab], default_tab: f64, seg: (f64, f64)) -> f64
(next - x).max(1.0)
}
#[cfg(test)]
thread_local! {
/// How many times `layout_scaled` ran on this thread (tests count autofit's passes).
pub(crate) static LAYOUT_PASSES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// Lay out a shape's text body.
pub fn layout(ctx: &Ctx, shape: &Shape, body: &TextBody, opts: &Opts) -> TextLayout {
let bp = resolve::body(ctx, shape);
@@ -1542,9 +1548,16 @@ pub fn layout(ctx: &Ctx, shape: &Shape, body: &TextBody, opts: &Opts) -> TextLay
if let (AutoFit::Shrink { .. }, false) = (autofit, opts.no_shrink)
&& l.overflow
{
// Text that overflows even at the smallest scale gets that scale: every larger one
// overflows too, so searching would only lay it out a dozen more times (seconds for a
// body of tens of thousands of paragraphs, on every redraw of the slide).
let smallest = layout_scaled(ctx, shape, body, &bp, opts, 0.1, 0.2);
if smallest.overflow {
return smallest;
}
// Binary search the largest scale that fits (steps like 90%, 80%… with line reduction).
let (mut lo, mut hi) = (0.1_f64, 1.0_f64);
let mut best = None;
let mut best = Some(smallest);
for _ in 0..12 {
let mid = (lo + hi) / 2.0;
let red: f64 = ((1.0 - mid) * 0.8_f64).min(0.2);
@@ -1556,13 +1569,17 @@ pub fn layout(ctx: &Ctx, shape: &Shape, body: &TextBody, opts: &Opts) -> TextLay
best = Some(t);
}
}
l = best.unwrap_or_else(|| layout_scaled(ctx, shape, body, &bp, opts, 0.1, 0.2));
if let Some(b) = best {
l = b;
}
}
l
}
#[allow(clippy::too_many_arguments)]
fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts: &Opts, scale: f64, reduce: f64) -> TextLayout {
#[cfg(test)]
LAYOUT_PASSES.with(|n| n.set(n.get() + 1));
let r = opts.rect;
let vert = bp.vert.unwrap_or(TextDir::Horizontal);
// East Asian vertical text: CJK stands upright in text turned a quarter turn; stacked text
+16
View File
@@ -181,6 +181,22 @@ fn shrink_on_overflow() {
assert!(!l.overflow || l.font_scale <= 0.11);
}
#[test]
fn shrink_gives_up_at_once_when_even_the_smallest_scale_overflows() {
// Found by fuzzing: a tiny box with 20,000 paragraphs and shrink on overflow took 15 s to
// open, laying out the whole body 13 times in a search that could never fit.
let text = (0..400).map(|i| format!("Line number {i}")).collect::<Vec<_>>().join("\n");
let (p, sh) = setup(&text, 100.0, 20.0);
crate::LAYOUT_PASSES.with(|n| n.set(0));
let l = lay(&p, &sh, 100.0, 20.0);
assert!(l.overflow && l.font_scale <= 0.11, "scale {}", l.font_scale);
assert!(crate::LAYOUT_PASSES.with(|n| n.get()) <= 2, "{} passes", crate::LAYOUT_PASSES.with(|n| n.get()));
// Text that fits somewhere in the range still gets the search.
let (p, sh) = setup(&(0..12).map(|i| format!("Line number {i}")).collect::<Vec<_>>().join("\n"), 600.0, 200.0);
let l = lay(&p, &sh, 600.0, 200.0);
assert!(l.font_scale > 0.11 && l.font_scale < 1.0 && !l.overflow, "scale {} overflow {}", l.font_scale, l.overflow);
}
#[test]
fn caret_and_hit_roundtrip() {
let (p, sh) = setup("Hello world", 600.0, 100.0);