craft-ui: combo footer remeasures reset once the footer is stable (#394)

* craft-ui: combo footer remeasures reset once the footer is stable

* craft-ui: test combo footer recovery after the remeasure cap
This commit is contained in:
aiongg
2026-10-27 10:26:11 -04:00
committed by GitHub
parent 92d392cff1
commit d40c2be47d
2 changed files with 85 additions and 3 deletions
+11 -3
View File
@@ -527,7 +527,8 @@ impl<'a> Menu<'a> {
let remeasure_id = id.with("craft-remeasure");
let remeasure = ctx.data_mut(|d| d.remove_temp::<bool>(remeasure_id)).unwrap_or(false);
let (align, settled) = side(&popup, remeasure);
// At most two remeasures per open, so a footer that changes height on every pass settles.
// At most two remeasures in a row, so a footer that changes height on every pass settles.
// A pass where the footer keeps its height clears the count.
let count_id = id.with("craft-remeasures");
if ctx.read_response(id).is_none() {
ctx.data_mut(|d| d.remove::<u8>(count_id));
@@ -571,12 +572,19 @@ impl<'a> Menu<'a> {
let top = ui.cursor().top();
let f = footer(ui);
let height = ui.min_rect().bottom() - top;
if footer_height.is_none_or(|h| (h - height).abs() > 0.5) && remeasures < 2 {
let changed = footer_height.is_none_or(|h| (h - height).abs() > 0.5);
if !changed && remeasures > 0 {
ui.data_mut(|d| d.remove::<u8>(count_id));
}
if changed {
// Recorded even past the cap, so "unchanged" means unchanged since the last pass.
ui.data_mut(|d| d.insert_temp(footer_id, height));
}
if changed && remeasures < 2 {
// The list took the wrong room: lay the menu out again next pass. Not a
// discarded pass: the header's side effects (focusing a search field on
// open) would run twice.
ui.data_mut(|d| {
d.insert_temp(footer_id, height);
d.insert_temp(remeasure_id, true);
d.insert_temp(count_id, remeasures + 1);
});
+74
View File
@@ -524,3 +524,77 @@ fn a_footer_that_changes_height_every_pass_settles() {
h.run();
assert!(*h.state() - before <= 4, "the menu keeps remeasuring: {} passes in two frames", *h.state() - before);
}
#[test]
fn a_footer_that_changes_height_after_settling_still_lands() {
// (footer height, bottom of the footer)
let mut h = Harness::builder().with_size(egui::vec2(400.0, 600.0)).build_ui_state(
|ui, s: &mut (f32, f32)| {
let field = ui.button("Send");
let _ = super::Menu::new(ui, &field).height(300.0).show_ui_with_header_footer(
|ui| ui.label("Title"),
|ui, _| {
for n in 0..50 {
let _ = ui.label(format!("Row {n}"));
}
},
|ui| {
ui.add_space(s.0);
s.1 = ui.min_rect().bottom();
},
);
},
(20.0, 0.0),
);
h.run();
h.get_by_label("Send").click();
h.run();
let bottom = h.state().1;
// The list fills the menu's height, so the footer's bottom stays put when the footer's height
// changes and the list gives or takes the room. Changes are spaced over several passes: the
// remeasure budget resets while the footer is stable.
for (n, height) in [60.0, 20.0, 80.0, 30.0].into_iter().enumerate() {
h.state_mut().0 = height;
h.run();
let now = h.state().1;
assert!((now - bottom).abs() < 1.0, "change {n} to {height}: the footer ends at {now}, was {bottom}");
h.run();
h.run();
}
}
#[test]
fn a_footer_that_hits_the_remeasure_cap_recovers_once_it_holds_still() {
// (held footer height, footer passes so far, bottom of the footer)
let mut h = Harness::builder().with_size(egui::vec2(400.0, 600.0)).build_ui_state(
|ui, s: &mut (f32, u32, f32)| {
let field = ui.button("Send");
let _ = super::Menu::new(ui, &field).height(300.0).show_ui_with_header_footer(
|ui| ui.label("Title"),
|ui, _| {
for n in 0..50 {
let _ = ui.label(format!("Row {n}"));
}
},
|ui| {
s.1 += 1;
// Grows on each of the first passes, past the cap of two remeasures.
ui.add_space(if s.1 <= 5 { 20.0 + 15.0 * s.1 as f32 } else { s.0 });
s.2 = ui.min_rect().bottom();
},
);
},
(40.0, 0, 0.0),
);
h.run();
h.get_by_label("Send").click();
// Passes with a stable footer follow the growth: each one resets the remeasure budget.
for _ in 0..3 {
h.step();
}
let bottom = h.state().2;
h.state_mut().0 = 90.0;
h.run();
let now = h.state().2;
assert!((now - bottom).abs() < 1.0, "the footer ends at {now}, was {bottom}");
}