Answer a break from the one in hand wherever it is still the same break
A parent that sizes to a child offers it back the length it just reported, so a wrapping text was re-broken at exactly its own longest line. That is a knife edge: the length is composed back through the box chain, so it lands an ulp either side of where it started, and which side decides whether the longest line still fits. One side kept three lines at 167.41, the other took four at 163.49 -- from the same text in the same box, differing only in what the output size had been. A greedy break does not need recomputing there. Breaking at one width gives lines that each fit, none of which could have taken another word; at any narrower width down to the longest of them, every line still fits and none can take a word that did not fit in more room. So one break answers a whole interval, and the cache now hits across it rather than on the exact width. The tolerance is what makes it hold at the edge, which is the case that matters: sub-pixel, so no break it admits is one anybody could see. The generated sweep passes at depth 6, where it failed; the shrinking fuzzer agrees over 800 trees at depth 7 on all three scenarios, where two of them failed. `tests/unsettled.rs` is green, so the whole suite is. Depth 7 of the generated sweep still fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e5a3e640d4
commit
99131940ab
1 file changed
+24
@@ -106,6 +106,13 @@ impl Default for TextAttrs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How far below the longest line a width may fall and still be answered by
|
||||||
|
/// the break in hand. A parent that offers a child the length it reported
|
||||||
|
/// composes that length back through the box chain, so the two differ in the
|
||||||
|
/// last bits -- and at exactly the longest line, that decides whether a line
|
||||||
|
/// fits. Sub-pixel, so no break it admits is one a reader could see.
|
||||||
|
const BREAK_EPSILON_PX: f32 = 0.05;
|
||||||
|
|
||||||
/// Keeps text and its corresponding layout from getting out of sync.
|
/// Keeps text and its corresponding layout from getting out of sync.
|
||||||
pub struct TextBuffer {
|
pub struct TextBuffer {
|
||||||
text: String,
|
text: String,
|
||||||
@@ -189,6 +196,23 @@ impl TextBuffer {
|
|||||||
diag::bump(Counter::TextShapeHits);
|
diag::bump(Counter::TextShapeHits);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// A greedy break at one width is the same break at every width down
|
||||||
|
// to the longest line it produced: each line still fits, and none can
|
||||||
|
// take a word that would not fit in the wider box. So the layout in
|
||||||
|
// hand already answers, and re-breaking would only be a chance to
|
||||||
|
// disagree with itself -- which is what happens when a parent offers
|
||||||
|
// a child the length that child just reported, and the two land
|
||||||
|
// either side of a float.
|
||||||
|
if let Some(key) = &self.layout_key
|
||||||
|
&& key.attrs == *attrs
|
||||||
|
&& let (Some(broke_at), Some(want)) = (key.max_width, width)
|
||||||
|
&& want <= broke_at
|
||||||
|
&& want + BREAK_EPSILON_PX >= self.layout.width()
|
||||||
|
{
|
||||||
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
diag::bump(Counter::TextShapeHits);
|
||||||
|
return;
|
||||||
|
}
|
||||||
let same_shaping = self
|
let same_shaping = self
|
||||||
.layout_key
|
.layout_key
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|||||||
Reference in new issue
Block a user