diff --git a/core/src/ui/active.rs b/core/src/ui/active.rs index cb65e9f..946447e 100644 --- a/core/src/ui/active.rs +++ b/core/src/ui/active.rs @@ -40,6 +40,11 @@ pub struct ActiveData { /// The measured answer and its dependencies. A hint-only dependency or /// a widget first encountered during placement has no measurement yet. 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. pub size: Size, /// The frame and extent reads that this drawing holds for. diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 6c85578..3b306c8 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -189,7 +189,8 @@ impl<'a> Painter<'a> { diag::region_node(id.id(), self.id, within); } // 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()); } let px = local.size().to_px(self.px); @@ -208,6 +209,7 @@ impl<'a> Painter<'a> { place, offer_place: place, narrow, + re_asked, px, }, None, diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 1bc879d..de68a80 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -38,6 +38,8 @@ pub(super) struct DrawInfo { /// The length the frame was narrowed to on each axis, as a length of /// the parent's frame, where anything narrowed it. pub narrow: [Option; 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 /// where every pixel length in layout comes from. pub px: PxVec2, @@ -154,6 +156,7 @@ impl UiRenderState { place: [Place::Within(Part::All); 2], offer_place: [Place::Within(Part::All); 2], narrow: [None; 2], + re_asked: false, px, } } @@ -268,6 +271,7 @@ impl UiRenderState { active.frame_abs = frame; active.frame = info.frame; active.narrow = info.narrow; + active.re_asked = info.re_asked; active.answer = Some(answer); active.offer_place = info.offer_place; active.offer_part = part; @@ -438,6 +442,7 @@ impl UiRenderState { place: [Place::Within(Part::All); 2], offer_place: [Place::Within(Part::All); 2], narrow: [None; 2], + re_asked: false, px, }, rsc, @@ -457,6 +462,7 @@ impl UiRenderState { offer_part: extent, // Whoever asked writes the answer. answer: None, + re_asked: info.re_asked, size, holds, drawn: true, @@ -724,6 +730,7 @@ impl UiRenderState { place, offer_place: active.offer_place, narrow: active.narrow, + re_asked: active.re_asked, px: frame.size().to_px(at.px), }; self.relocate(child, info.frame_abs, extent, info, rsc); @@ -914,6 +921,7 @@ impl UiRenderState { offer_place: [Place::Within(Part::All); 2], offer_part: UiRegion::FULL, answer: None, + re_asked: false, size, holds: LayoutHolds::ANY, drawn: false, @@ -1113,11 +1121,16 @@ impl UiRenderState { // 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 // 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 alignment_changed = rsc.widgets().alignment(id) != active.own_align; 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 // this because the parent must draw it rather than keep what it @@ -1172,6 +1185,7 @@ impl UiRenderState { place: active.offer_place, offer_place: active.offer_place, narrow: active.narrow, + re_asked: false, px, }; #[cfg(feature = "layout-diagnostics")] diff --git a/tests/cases/unsettled.rs b/tests/cases/unsettled.rs index c2f766f..70f921a 100644 --- a/tests/cases/unsettled.rs +++ b/tests/cases/unsettled.rs @@ -846,3 +846,84 @@ fn adding_text_to_a_reverse_row_keeps_its_shared_height() { let (_, other, _) = build(&mut cold, true); 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, WeakWidget, Vec) { + 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 = + vec![filler.add_strong(&mut h.rsc), second.add_strong(&mut h.rsc)]; + let mut children: Vec = 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 = 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); +}