diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 93dcffe35c..c4751fb367 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -127,7 +127,18 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { app.custom_draft = Some((id, crate::CustomDraft { props: doc.custom_info(), ..Default::default() })); } let mut req = Pending::default(); - let modal = widgets::show_modal(ctx, "dialog", |ui| { + // Every dialog shares this modal. A different dialog than the one shown before starts from + // its own size, not from the last one's: rows that fill the height they are offered would + // otherwise keep, say, Print as tall as Preferences (#5118). Tabs of one dialog keep its size; + // the kinds of Marks and Export are different dialogs. + let shown = egui::Id::unique("dialog").with("shown"); + let kind = match dialog { + Dialog::Properties(_) => Dialog::Properties(PropsTab::Description), + _ => dialog, + }; + let remeasure = ctx.data(|d| d.get_temp::(shown)) != Some(kind); + ctx.data_mut(|d| d.insert_temp(shown, kind)); + let modal = widgets::show_modal_sized(ctx, "dialog", remeasure, |ui| { ui.set_width(match dialog { Dialog::Properties(_) => 640.0, Dialog::Print => 820.0, diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index d7f4213711..4a45817744 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -35,7 +35,22 @@ pub fn modal(ctx: &egui::Context, id: impl std::hash::Hash + std::fmt::Debug) -> /// Show [`modal`], outline its controls, and remember the card rect for layout checks. pub fn show_modal(ctx: &egui::Context, id: &str, content: impl FnOnce(&mut egui::Ui) -> R) -> egui::ModalResponse { + show_modal_sized(ctx, id, false, content) +} + +/// [`show_modal`], growing from its contents' own height when `remeasure` is set rather than +/// from the height it had last frame: one modal id that shows different contents would +/// otherwise keep the height of the previous ones, since rows that fill the height offered to +/// them take all of it (#5118). The width stays, and the contents stay visible (no hidden +/// sizing frame), so the control channel can inspect a dialog the frame it opens. +pub fn show_modal_sized(ctx: &egui::Context, id: &str, remeasure: bool, content: impl FnOnce(&mut egui::Ui) -> R) -> egui::ModalResponse { let stored = egui::Id::unique(id); + if remeasure && let Some(mut state) = egui::AreaState::load(ctx, stored) { + state.size = state.size.map(|s| egui::vec2(s.x, 0.0)); + ctx.memory_mut(|m| m.areas_mut().set_state(egui::Modal::default_area(stored).layer(), state)); + // The card is centred on the height it ends with: draw once more at that height. + ctx.request_repaint(); + } modal(ctx, id).show(ctx, |ui| { begin_dialog(ui); let inner = content(ui); diff --git a/crates/ui-egui/tests/it/dialog_chrome.rs b/crates/ui-egui/tests/it/dialog_chrome.rs index c495e608ff..cf829e7341 100644 --- a/crates/ui-egui/tests/it/dialog_chrome.rs +++ b/crates/ui-egui/tests/it/dialog_chrome.rs @@ -258,3 +258,54 @@ fn page_hover_cursors_stay_off_while_a_dialog_is_open() { h.run_steps(2); assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::Default, "nothing under the dialog's backdrop can be clicked"); } + +#[test] +fn a_dialog_keeps_its_own_height_after_a_taller_one() { + // #5118: Print opened after Preferences was as tall as Preferences, with blank space below + // its buttons. Every dialog shares one modal, which kept the last dialog's size. + use pdfcraft_ui_egui::Dialog; + for dialog in [Dialog::Print, Dialog::RotatePages] { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app.open_bytes("doc.pdf", None, fixture()).unwrap(); + app + }); + let show = |h: &mut Harness<'_, PdfCraftApp>, d: Dialog| { + h.state_mut().dialog = None; + h.run_steps(2); + h.state_mut().dialog = Some(d); + h.run_steps(6); + card(h, "dialog") + }; + let fresh = show(&mut h, dialog); + let prefs = show(&mut h, Dialog::Preferences); + assert!(prefs.height() > fresh.height() + 20.0, "{dialog:?} {fresh:?} isn't shorter than Preferences {prefs:?}"); + let after = show(&mut h, dialog); + assert!((after.height() - fresh.height()).abs() < 1.0, "{dialog:?} {fresh:?}, after Preferences {after:?}"); + } +} + +#[test] +fn each_kind_of_marks_dialog_keeps_its_own_height() { + // #5118: Header & Footer, Watermark and Background are one `Dialog::Marks` with different + // contents. Background opened after Header & Footer kept the taller dialog's height. + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app.open_bytes("doc.pdf", None, fixture()).unwrap(); + app + }); + let show = |h: &mut Harness<'_, PdfCraftApp>, d: &str| { + h.state_mut().dialog = None; + h.run_steps(2); + h.state_mut().set_option("dialog", d).unwrap(); + h.run_steps(6); + card(h, "dialog") + }; + let fresh = show(&mut h, "background"); + let header = show(&mut h, "header-footer"); + assert!(header.height() > fresh.height() + 20.0, "Background {fresh:?} isn't shorter than Header & Footer {header:?}"); + let after = show(&mut h, "background"); + assert!((after.height() - fresh.height()).abs() < 1.0, "Background {fresh:?}, after Header & Footer {after:?}"); +}