diff --git a/crates/ui/src/combo.rs b/crates/ui/src/combo.rs index d0138b7d..91d70f52 100644 --- a/crates/ui/src/combo.rs +++ b/crates/ui/src/combo.rs @@ -527,7 +527,8 @@ impl<'a> Menu<'a> { let remeasure_id = id.with("craft-remeasure"); let remeasure = ctx.data_mut(|d| d.remove_temp::(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::(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::(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); }); diff --git a/crates/ui/src/combo/tests.rs b/crates/ui/src/combo/tests.rs index 48e02da2..90e1afb1 100644 --- a/crates/ui/src/combo/tests.rs +++ b/crates/ui/src/combo/tests.rs @@ -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}"); +}