Hold a clipping widget to its box, and check that it is

`Scroll` reports `LEFTOVER` on both axes because it clips its content to its
box: it can neither take less of one nor honestly ask for more. `Masked` is
the other widget that clips and was passing its inner's size up, so a mask
over something taller than its box asked to be placed at the length it had
just cut off. It reports its box now, for the same reason.

The `debug_assert` the handoff has been asking for is the one that would have
caught both, narrowed to what is actually true: a widget that set a mask this
draw has to report inside the box it drew in. Reported as "does not exceed
the box" it fires on ordinary overflow instead -- measured, a hundred fuzzer
trees produce thousands of them, every one a text too tall for the box it was
offered, which is what a text is meant to say.

`tests/cases/scroll.rs` has a clipping widget that reports its content, to
show the assertion catches it.

Checked: fmt, clippy, 104 tests, all five shrinker cases at 300 seeds, 100
generated seeds, five examples byte-identical at 1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-16 03:48:23 -04:00
1 parent bdab55824f
commit 95fb4f962c
3 files changed
+49 -1

No files matched your search

+19
View File
@@ -366,6 +366,16 @@ impl UiRenderState {
x: rules.x.apply(size.x), x: rules.x.apply(size.x),
y: rules.y.apply(size.y), y: rules.y.apply(size.y),
}; };
// A widget that clipped its contents to its box drew nothing outside
// it, so reporting more than the box asks to be placed at a length it
// does not occupy -- and its parent would place the part it cut off.
// Overflowing is otherwise ordinary: a text too tall for the box it
// was offered reports the height it needs.
debug_assert!(
mask == info.mask || AXES.into_iter().all(|axis| within_box(size, px, axis)),
"'{}' ({id:?}) clips to {px:?} and reports {size}",
rsc.widgets().label(id),
);
let holds = [own[0].and(under[0]), own[1].and(under[1])]; let holds = [own[0].and(under[0]), own[1].and(under[1])];
debug_assert!( debug_assert!(
holds[0].contains(px.x) && holds[1].contains(px.y), holds[0].contains(px.x) && holds[1].contains(px.y),
@@ -1011,6 +1021,15 @@ impl UiRenderState {
} }
} }
/// Whether what a widget reports along `axis` is inside the box it drew in.
/// A share is a length only to whoever divides one, so it is not a claim
/// about this box and cannot exceed it.
fn within_box(size: Size, px: PxVec2, axis: Axis) -> bool {
let len = size.axis(axis);
let box_len = px.axis(axis);
len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len
}
/// The same box is the same number of steps, both of these being lengths on /// The same box is the same number of steps, both of these being lengths on
/// the grid rather than floats to be compared for nearness. /// the grid rather than floats to be compared for nearness.
fn same_px(a: PxVec2, b: PxVec2) -> bool { fn same_px(a: PxVec2, b: PxVec2) -> bool {
+7 -1
View File
@@ -7,6 +7,12 @@ pub struct Masked {
impl Widget for Masked { impl Widget for Masked {
fn draw(&mut self, painter: &mut Painter) -> Size { fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region()); painter.set_mask(painter.region());
painter.widget(&self.inner).size() painter.widget(&self.inner);
// What it occupies is its box, on both axes, for the reason `Scroll`
// reports the same: it clips what is inside to that box, so it can
// neither take less of one nor honestly ask for more. Passing the
// inner size up instead asks to be placed at a length it does not
// draw, and the framework would place the drawing it clipped away.
Size::LEFTOVER
} }
} }
+23
View File
@@ -59,3 +59,26 @@ fn a_wheel_scrolls_the_content_and_stops_at_its_end() {
h.frame(); h.frame();
assert_corners!(h, top, (0, 0), (400, 200)); assert_corners!(h, top, (0, 0), (400, 200));
} }
/// A widget that clips to its box may not report more than the box: its
/// parent would place the part it cut off, and the framework would put a
/// drawing longer than its box somewhere. `Masked` is the second of these
/// after `Scroll`, and the assertion in `draw_at` is what says so.
#[test]
#[should_panic = "clips to"]
fn a_clipping_widget_reporting_more_than_its_box_is_caught() {
struct Clipper(StrongWidget);
impl Widget for Clipper {
fn draw(&mut self, painter: &mut Painter) -> Size {
painter.set_mask(painter.region());
painter.widget(&self.0).size()
}
}
let mut h = Harness::new((100, 100));
let tall = rect(Color::RED).height(400).add_strong(&mut h.rsc);
let clipper = Clipper(tall).add(&mut h.rsc);
h.set_root(clipper);
h.frame();
}