Defer a twice-asked widget's local redraw to its parent
A span asks a share child twice in one draw: in the room, whose answer its slots rest on, and in the decided slot, whose cross-axis answer it reads. The record keeps only the second question, so a local redraw that found that answer unchanged never told the row that the first had -- seed 946 at depth 6, where emptying a fixed-height column turns it from a share into a fixed width as wide as the row. A widget its parent asked more than once in one draw now defers to that parent, like one whose declared length changed. Pinned as unsettled::emptying_a_column_the_row_asked_twice_asks_the_row_again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
3091fb86df
commit
0ef87ebfcf
4 files changed
+105
-3
No files matched your search
@@ -40,6 +40,11 @@ pub struct ActiveData {
|
|||||||
/// The measured answer and its dependencies. A hint-only dependency or
|
/// The measured answer and its dependencies. A hint-only dependency or
|
||||||
/// a widget first encountered during placement has no measurement yet.
|
/// a widget first encountered during placement has no measurement yet.
|
||||||
pub answer: Option<(Size, LayoutHolds)>,
|
pub answer: Option<(Size, LayoutHolds)>,
|
||||||
|
/// Asked more than once in its parent's last draw -- measured in one box
|
||||||
|
/// and then asked in the one the parent decided. The parent's layout
|
||||||
|
/// rests on the first answer and its drawing on the last, so only the
|
||||||
|
/// parent can ask either again.
|
||||||
|
pub re_asked: bool,
|
||||||
/// What the widget said it used of its frame, the last time it drew.
|
/// What the widget said it used of its frame, the last time it drew.
|
||||||
pub size: Size,
|
pub size: Size,
|
||||||
/// The frame and extent reads that this drawing holds for.
|
/// The frame and extent reads that this drawing holds for.
|
||||||
|
|||||||
@@ -189,7 +189,8 @@ impl<'a> Painter<'a> {
|
|||||||
diag::region_node(id.id(), self.id, within);
|
diag::region_node(id.id(), self.id, within);
|
||||||
}
|
}
|
||||||
// A child listed twice would be moved twice.
|
// A child listed twice would be moved twice.
|
||||||
if !self.children.contains(&id.id()) {
|
let re_asked = self.children.contains(&id.id());
|
||||||
|
if !re_asked {
|
||||||
self.children.push(id.id());
|
self.children.push(id.id());
|
||||||
}
|
}
|
||||||
let px = local.size().to_px(self.px);
|
let px = local.size().to_px(self.px);
|
||||||
@@ -208,6 +209,7 @@ impl<'a> Painter<'a> {
|
|||||||
place,
|
place,
|
||||||
offer_place: place,
|
offer_place: place,
|
||||||
narrow,
|
narrow,
|
||||||
|
re_asked,
|
||||||
px,
|
px,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ pub(super) struct DrawInfo {
|
|||||||
/// The length the frame was narrowed to on each axis, as a length of
|
/// The length the frame was narrowed to on each axis, as a length of
|
||||||
/// the parent's frame, where anything narrowed it.
|
/// the parent's frame, where anything narrowed it.
|
||||||
pub narrow: [Option<Len>; 2],
|
pub narrow: [Option<Len>; 2],
|
||||||
|
/// Whether the parent already asked about this widget in this draw.
|
||||||
|
pub re_asked: bool,
|
||||||
/// The frame in pixels: one multiply from the parent's own, which is
|
/// The frame in pixels: one multiply from the parent's own, which is
|
||||||
/// where every pixel length in layout comes from.
|
/// where every pixel length in layout comes from.
|
||||||
pub px: PxVec2,
|
pub px: PxVec2,
|
||||||
@@ -154,6 +156,7 @@ impl UiRenderState {
|
|||||||
place: [Place::Within(Part::All); 2],
|
place: [Place::Within(Part::All); 2],
|
||||||
offer_place: [Place::Within(Part::All); 2],
|
offer_place: [Place::Within(Part::All); 2],
|
||||||
narrow: [None; 2],
|
narrow: [None; 2],
|
||||||
|
re_asked: false,
|
||||||
px,
|
px,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,6 +271,7 @@ impl UiRenderState {
|
|||||||
active.frame_abs = frame;
|
active.frame_abs = frame;
|
||||||
active.frame = info.frame;
|
active.frame = info.frame;
|
||||||
active.narrow = info.narrow;
|
active.narrow = info.narrow;
|
||||||
|
active.re_asked = info.re_asked;
|
||||||
active.answer = Some(answer);
|
active.answer = Some(answer);
|
||||||
active.offer_place = info.offer_place;
|
active.offer_place = info.offer_place;
|
||||||
active.offer_part = part;
|
active.offer_part = part;
|
||||||
@@ -438,6 +442,7 @@ impl UiRenderState {
|
|||||||
place: [Place::Within(Part::All); 2],
|
place: [Place::Within(Part::All); 2],
|
||||||
offer_place: [Place::Within(Part::All); 2],
|
offer_place: [Place::Within(Part::All); 2],
|
||||||
narrow: [None; 2],
|
narrow: [None; 2],
|
||||||
|
re_asked: false,
|
||||||
px,
|
px,
|
||||||
},
|
},
|
||||||
rsc,
|
rsc,
|
||||||
@@ -457,6 +462,7 @@ impl UiRenderState {
|
|||||||
offer_part: extent,
|
offer_part: extent,
|
||||||
// Whoever asked writes the answer.
|
// Whoever asked writes the answer.
|
||||||
answer: None,
|
answer: None,
|
||||||
|
re_asked: info.re_asked,
|
||||||
size,
|
size,
|
||||||
holds,
|
holds,
|
||||||
drawn: true,
|
drawn: true,
|
||||||
@@ -724,6 +730,7 @@ impl UiRenderState {
|
|||||||
place,
|
place,
|
||||||
offer_place: active.offer_place,
|
offer_place: active.offer_place,
|
||||||
narrow: active.narrow,
|
narrow: active.narrow,
|
||||||
|
re_asked: active.re_asked,
|
||||||
px: frame.size().to_px(at.px),
|
px: frame.size().to_px(at.px),
|
||||||
};
|
};
|
||||||
self.relocate(child, info.frame_abs, extent, info, rsc);
|
self.relocate(child, info.frame_abs, extent, info, rsc);
|
||||||
@@ -914,6 +921,7 @@ impl UiRenderState {
|
|||||||
offer_place: [Place::Within(Part::All); 2],
|
offer_place: [Place::Within(Part::All); 2],
|
||||||
offer_part: UiRegion::FULL,
|
offer_part: UiRegion::FULL,
|
||||||
answer: None,
|
answer: None,
|
||||||
|
re_asked: false,
|
||||||
size,
|
size,
|
||||||
holds: LayoutHolds::ANY,
|
holds: LayoutHolds::ANY,
|
||||||
drawn: false,
|
drawn: false,
|
||||||
@@ -1113,11 +1121,16 @@ impl UiRenderState {
|
|||||||
// Its parent resolved its declared lengths into its box and decided
|
// Its parent resolved its declared lengths into its box and decided
|
||||||
// whether to draw it at all, so a change to either is the parent's
|
// whether to draw it at all, so a change to either is the parent's
|
||||||
// to draw -- with the mark left on, so the parent draws it rather
|
// to draw -- with the mark left on, so the parent draws it rather
|
||||||
// than keeping it.
|
// than keeping it. So is a widget the parent asked twice: its
|
||||||
|
// layout rests on an answer this widget cannot give again alone.
|
||||||
let declared_changed = declared_lens(rsc.widgets(), id) != active.declared;
|
let declared_changed = declared_lens(rsc.widgets(), id) != active.declared;
|
||||||
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
|
let alignment_changed = rsc.widgets().alignment(id) != active.own_align;
|
||||||
if let Some(parent) = active.parent
|
if let Some(parent) = active.parent
|
||||||
&& (declared_changed || alignment_changed || !active.drawn || active.answer.is_none())
|
&& (declared_changed
|
||||||
|
|| alignment_changed
|
||||||
|
|| active.re_asked
|
||||||
|
|| !active.drawn
|
||||||
|
|| active.answer.is_none())
|
||||||
{
|
{
|
||||||
// Both stay marked: the parent because it has this to draw, and
|
// Both stay marked: the parent because it has this to draw, and
|
||||||
// this because the parent must draw it rather than keep what it
|
// this because the parent must draw it rather than keep what it
|
||||||
@@ -1172,6 +1185,7 @@ impl UiRenderState {
|
|||||||
place: active.offer_place,
|
place: active.offer_place,
|
||||||
offer_place: active.offer_place,
|
offer_place: active.offer_place,
|
||||||
narrow: active.narrow,
|
narrow: active.narrow,
|
||||||
|
re_asked: false,
|
||||||
px,
|
px,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "layout-diagnostics")]
|
#[cfg(feature = "layout-diagnostics")]
|
||||||
|
|||||||
@@ -846,3 +846,84 @@ fn adding_text_to_a_reverse_row_keeps_its_shared_height() {
|
|||||||
let (_, other, _) = build(&mut cold, true);
|
let (_, other, _) = build(&mut cold, true);
|
||||||
assert_eq!(warm.region(&shared), cold.region(&other));
|
assert_eq!(warm.region(&shared), cold.region(&other));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Nine widgets, shrunk from seed 946 at depth 6. The column is a share of
|
||||||
|
/// the row while its rect has room to draw and a fixed width once it has
|
||||||
|
/// not, so the row asks it twice: in the room, where it answers a share,
|
||||||
|
/// and in its slot, where it answers its text's width. Emptying the column
|
||||||
|
/// changes only the first answer. A local redraw that asked only the second
|
||||||
|
/// question kept the row as it was; the column has to defer to the row.
|
||||||
|
fn plant_column_that_is_a_share_only_while_its_rect_fits(
|
||||||
|
h: &mut Harness,
|
||||||
|
emptied: bool,
|
||||||
|
) -> (Vec<WidgetId>, WeakWidget<Span>, Vec<StrongWidget>) {
|
||||||
|
let first = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let filler = rect(Color::CYAN.alpha(126)).add(&mut h.rsc);
|
||||||
|
let second = wtext(PARAGRAPH).size(16).wrap(true).add(&mut h.rsc);
|
||||||
|
let mut spare: Vec<StrongWidget> =
|
||||||
|
vec![filler.add_strong(&mut h.rsc), second.add_strong(&mut h.rsc)];
|
||||||
|
let mut children: Vec<StrongWidget> = vec![first.add_strong(&mut h.rsc)];
|
||||||
|
if !emptied {
|
||||||
|
children.append(&mut spare);
|
||||||
|
}
|
||||||
|
let column = Span {
|
||||||
|
children,
|
||||||
|
dir: Dir::DOWN,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.height(159)
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let left = rect(Color::MAGENTA.alpha(189)).add(&mut h.rsc);
|
||||||
|
let right = rect(Color::BLUE.alpha(0)).add(&mut h.rsc);
|
||||||
|
let row = Span {
|
||||||
|
children: vec![
|
||||||
|
left.add_strong(&mut h.rsc),
|
||||||
|
column.add_strong(&mut h.rsc),
|
||||||
|
right.add_strong(&mut h.rsc),
|
||||||
|
],
|
||||||
|
dir: Dir::RIGHT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
let end = rect(Color::MAGENTA.alpha(189)).add(&mut h.rsc);
|
||||||
|
let root = Span {
|
||||||
|
children: vec![end.add_strong(&mut h.rsc), row.add_strong(&mut h.rsc)],
|
||||||
|
dir: Dir::LEFT,
|
||||||
|
gap: Px::ZERO,
|
||||||
|
}
|
||||||
|
.add(&mut h.rsc);
|
||||||
|
h.set_root(root);
|
||||||
|
(
|
||||||
|
vec![
|
||||||
|
first.id(),
|
||||||
|
filler.id(),
|
||||||
|
second.id(),
|
||||||
|
column.id(),
|
||||||
|
left.id(),
|
||||||
|
right.id(),
|
||||||
|
row.id(),
|
||||||
|
end.id(),
|
||||||
|
root.id(),
|
||||||
|
],
|
||||||
|
column,
|
||||||
|
spare,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emptying_a_column_the_row_asked_twice_asks_the_row_again() {
|
||||||
|
let mut warm = Harness::new((900, 1200));
|
||||||
|
let (ids, column, _spare) =
|
||||||
|
plant_column_that_is_a_share_only_while_its_rect_fits(&mut warm, false);
|
||||||
|
warm.frame();
|
||||||
|
// Kept alive: dropping the last share of a widget frees its id.
|
||||||
|
let _removed: Vec<StrongWidget> = warm.rsc[column].children.drain(1..).collect();
|
||||||
|
warm.frame();
|
||||||
|
|
||||||
|
let mut cold = Harness::new((900, 1200));
|
||||||
|
let (cold_ids, _, _spare) =
|
||||||
|
plant_column_that_is_a_share_only_while_its_rect_fits(&mut cold, true);
|
||||||
|
cold.frame();
|
||||||
|
|
||||||
|
assert_same_regions(&warm, &ids, &cold, &cold_ids);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user