Hand a redrawn widget the mask it inherited, not its own

`ActiveData::mask` is the mask a widget's drawing is clipped to, which is
either one it set itself or the one it inherited. `redraw` passed it back as
the *inherited* mask, so a `Masked` widget settled on its own was handed its
own mask and `set_mask` asserted -- a panic on any local redraw of one, for
as long as there has been a local-redraw path. The two are separate facts, so
`parent_mask` keeps the second.

That also states the question `remap_subtree` was asking. It compared a
widget's mask with the one threaded down from its parent to find out whether
the widget owned it; the comparison is now between the two fields on the
widget, which is the same question asked where the answer lives, and the
parameter goes.

Checked: fmt, clippy, 87 suite tests including the new one, which panics
without this; 18 core unit tests; the release oracle at 100 seeds; the
fifteen shrinker cases at 400 seeds of depth 5; and `tabs` renders
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-17 00:18:39 -04:00
1 parent 32542d0c0b
commit ea6dbae0dc
3 files changed
+35 -14

No files matched your search

+7
View File
@@ -58,7 +58,14 @@ pub struct ActiveData {
pub own_align: RegionAlign, pub own_align: RegionAlign,
/// The movable region whose coordinates `region` uses. /// The movable region whose coordinates `region` uses.
pub parent_move: MoveIdx, pub parent_move: MoveIdx,
/// The mask its drawing is clipped to: one it set itself, or the one it
/// inherited from whoever drew it.
pub mask: MaskIdx, pub mask: MaskIdx,
/// That inherited one. The two differ exactly where the widget set a
/// mask of its own, which is the one it owns and the one a move rewrites
/// -- and the one a redraw of it must not be handed back, since setting
/// a mask asserts there is none.
pub parent_mask: MaskIdx,
pub layer: LayerId, pub layer: LayerId,
} }
+13 -14
View File
@@ -437,6 +437,7 @@ impl UiRenderState {
move_idx, move_idx,
parent_move: info.parent_move, parent_move: info.parent_move,
mask, mask,
parent_mask: info.mask,
layer: info.layer, layer: info.layer,
}; };
rsc.on_draw(&active); rsc.on_draw(&active);
@@ -623,18 +624,14 @@ impl UiRenderState {
return None; return None;
} }
let moved = active.region != region; let moved = active.region != region;
let (answer, old_region, slot, mask) = ( let (answer, old_region, slot) =
(active.size, active.holds), ((active.size, active.holds), active.region, active.move_idx);
active.region,
active.move_idx,
info.mask,
);
if moved { if moved {
if has_region_node { if has_region_node {
self.moves.set(slot, region); self.moves.set(slot, region);
} else { } else {
let remap = RegionRemap::new(old_region, region)?; let remap = RegionRemap::new(old_region, region)?;
self.remap_subtree(id, &remap, info.parent_move, mask, rsc); self.remap_subtree(id, &remap, info.parent_move, rsc);
} }
} }
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
@@ -674,7 +671,6 @@ impl UiRenderState {
id: WidgetId, id: WidgetId,
remap: &RegionRemap, remap: &RegionRemap,
parent_move: MoveIdx, parent_move: MoveIdx,
inherited_mask: MaskIdx,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) { ) {
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
@@ -690,16 +686,18 @@ impl UiRenderState {
*region = remap.apply(*region); *region = remap.apply(*region);
} }
active.region = remap.apply(active.region); active.region = remap.apply(active.region);
let mask = active.mask; let own_mask = (active.mask != active.parent_mask).then_some(active.mask);
let children = active.children.len(); let children = active.children.len();
if mask != inherited_mask && mask != MaskIdx::NONE { // A mask the widget set itself moves with it; one it inherited
let mask = rsc.ui_mut().masks.get_mut(mask); // belongs to the widget that set it, and moves there or not at all.
if let Some(idx) = own_mask {
let mask = rsc.ui_mut().masks.get_mut(idx);
debug_assert_eq!(mask.move_idx, parent_move); debug_assert_eq!(mask.move_idx, parent_move);
mask.region = remap.apply(mask.region); mask.region = remap.apply(mask.region);
} }
for index in 0..children { for index in 0..children {
let child = self.active[&id].children[index]; let child = self.active[&id].children[index];
self.remap_subtree(child, remap, parent_move, mask, rsc); self.remap_subtree(child, remap, parent_move, rsc);
} }
} }
@@ -793,6 +791,7 @@ impl UiRenderState {
own_align: rsc.widgets().alignment(id), own_align: rsc.widgets().alignment(id),
parent_move: info.parent_move, parent_move: info.parent_move,
mask: info.mask, mask: info.mask,
parent_mask: info.mask,
layer: info.layer, layer: info.layer,
}, },
); );
@@ -963,7 +962,7 @@ impl UiRenderState {
let Some(parent) = active.parent else { let Some(parent) = active.parent else {
let region = Self::root_region(id, rsc.widgets()); let region = Self::root_region(id, rsc.widgets());
let info = DrawInfo { let info = DrawInfo {
mask: active.mask, mask: active.parent_mask,
..self.root_info(region) ..self.root_info(region)
}; };
#[cfg(feature = "layout-diagnostics")] #[cfg(feature = "layout-diagnostics")]
@@ -990,7 +989,7 @@ impl UiRenderState {
depth: active.depth, depth: active.depth,
parent_move: active.parent_move, parent_move: active.parent_move,
region_node: rsc.widgets().is_region_node(id), region_node: rsc.widgets().is_region_node(id),
mask: active.mask, mask: active.parent_mask,
given_len: active.given_len, given_len: active.given_len,
offer_len: active.offer_len, offer_len: active.offer_len,
px: given_px, px: given_px,
+15
View File
@@ -613,3 +613,18 @@ fn a_stacks_sizing_child_is_drawn_once_where_it_belongs() {
assert_ne!(layer(front.id()), layer(background.id())); assert_ne!(layer(front.id()), layer(background.id()));
assert_eq!(draws.get(), 1); assert_eq!(draws.get(), 1);
} }
/// A widget's own mask is not the one it inherited, and a redraw of it
/// inherits the second: handing back the first is handing it its own mask to
/// set a second time, which `set_mask` asserts against.
#[test]
fn a_masked_widget_redrawn_on_its_own_sets_its_mask_again() {
let mut h = Harness::new((400, 200));
let inner = rect(Color::BLUE).add(&mut h.rsc);
let masked = inner.masked().add(&mut h.rsc);
let other = rect(Color::RED).width(100).add(&mut h.rsc);
h.set_root((other, masked).span(Dir::RIGHT));
h.rsc.widgets_mut().get_dyn_mut(masked.id());
h.frame();
assert_corners!(h, inner, (100, 0), (400, 200));
}