diff --git a/crates/typst/src/layout/inline/linebreak.rs b/crates/typst/src/layout/inline/linebreak.rs index 2dc3edb5e..b72507fdb 100644 --- a/crates/typst/src/layout/inline/linebreak.rs +++ b/crates/typst/src/layout/inline/linebreak.rs @@ -490,7 +490,7 @@ fn linebreak_optimized_approximate( // becomes useless and actively harmful (it could be lower than what // optimal layout produces). Thus, we immediately bail with an infinite // bound in this case. - if ratio < metrics.min_ratio(false) { + if ratio < metrics.min_ratio { return Cost::INFINITY; } @@ -536,6 +536,11 @@ fn ratio_and_cost( } /// Determine the stretch ratio for a line given raw metrics. +/// +/// - A ratio < min_ratio indicates an overfull line. +/// - A negative ratio indicates a line that needs shrinking. +/// - A ratio of zero indicates a perfect line. +/// - A positive ratio indicates a line that needs stretching. fn raw_ratio( p: &Preparation, available_width: Abs, @@ -548,30 +553,39 @@ fn raw_ratio( // to make it the desired width. let delta = available_width - line_width; - // Determine how much stretch is permitted. - let adjust = if delta >= Abs::zero() { stretchability } else { shrinkability }; + // Determine how much stretch or shrink is natural. + let adjustability = if delta >= Abs::zero() { stretchability } else { shrinkability }; - // Ideally, the ratio should between -1.0 and 1.0. - // - // A ratio above 1.0 is possible for an underfull line, but a ratio below - // -1.0 is forbidden because the line would overflow. - let mut ratio = delta / adjust; + // Observations: + // - `delta` is negative for a line that needs shrinking and positive for a + // line that needs stretching. + // - `adjustability` must be non-negative to make sense. + // - `ratio` inherits the sign of `delta`. + let mut ratio = delta / adjustability.max(Abs::zero()); - // The line is not stretchable, but it just fits. This often happens with - // monospace fonts and CJK texts. + // The most likely cause of a NaN result is that `delta` was zero. This + // often happens with monospace fonts and CJK texts. It means that the line + // already fits perfectly, so `ratio` should be zero then. if ratio.is_nan() { ratio = 0.0; } + // If the ratio exceeds 1, we should stretch above the natural + // stretchability using justifiables. if ratio > 1.0 { // We should stretch the line above its stretchability. Now // calculate the extra amount. Also, don't divide by zero. - let extra_stretch = (delta - adjust) / justifiables.max(1) as f64; + let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64; // Normalize the amount by half the em size. ratio = 1.0 + extra_stretch / (p.size / 2.0); } - ratio + // The min value must be < MIN_RATIO, but how much smaller doesn't matter + // since overfull lines have hard-coded huge costs anyway. + // + // The max value is clamped to 10 since it doesn't really matter whether a + // line is stretched 10x or 20x. + ratio.clamp(MIN_RATIO - 1.0, 10.0) } /// Compute the cost of a line given raw metrics. @@ -595,7 +609,7 @@ fn raw_cost( // If the line shall be justified or needs shrinking, it has normal // badness with cost 100|ratio|^3. We limit the ratio to 10 as to not // get to close to our maximum cost. - 100.0 * ratio.abs().min(10.0).powi(3) + 100.0 * ratio.abs().powi(3) } else { // If the line shouldn't be justified and doesn't need shrink, we don't // pay any cost. diff --git a/tests/ref/issue-4938-par-bad-ratio.png b/tests/ref/issue-4938-par-bad-ratio.png new file mode 100644 index 000000000..5021e2998 Binary files /dev/null and b/tests/ref/issue-4938-par-bad-ratio.png differ diff --git a/tests/suite/model/par.typ b/tests/suite/model/par.typ index 80bc9f3e1..7a5a37f53 100644 --- a/tests/suite/model/par.typ +++ b/tests/suite/model/par.typ @@ -97,3 +97,7 @@ Lorem ipsum dolor #metadata(none) nonumy eirmod tempor. --- issue-4278-par-trim-before-equation --- #set par(justify: true) #lorem(6) aa $a = c + b$ + +--- issue-4938-par-bad-ratio --- +#set par(justify: true) +#box($k in NN_0$)