diff --git a/crates/ui-egui/src/dialogs/mod.rs b/crates/ui-egui/src/dialogs/mod.rs index dde768d5..8f049f8a 100644 --- a/crates/ui-egui/src/dialogs/mod.rs +++ b/crates/ui-egui/src/dialogs/mod.rs @@ -91,6 +91,10 @@ use crate::{VectorcraftApp, widgets}; type DialogResult = Result; +/// The shared dialog frame's inner margin, and the least room it leaves at the window's edges. +const MARGIN: i8 = 22; +const EDGE_GAP: f32 = 8.0; + /// How a dialog draws and applies itself. Specs start from [`DialogSpec::FORM`] and override what /// differs. pub(crate) struct DialogSpec { @@ -298,12 +302,12 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { .resizable(false) .title_bar(false) .anchor(egui::Align2::CENTER_CENTER, [0.0, -40.0]) - .frame(egui::Frame::window(&ctx.global_style()).fill(t.panel).inner_margin(egui::Margin::same(22))) + .frame(egui::Frame::window(&ctx.global_style()).fill(t.panel).inner_margin(egui::Margin::same(MARGIN))) .show(ctx, |ui| { - ui.set_min_width(spec.min_width); - if let Some(w) = spec.max_width { - ui.set_max_width(w); - } + // Never wider than the window (a large UI scale in a small window): the text wraps. + let room = (ctx.content_rect().width() - 2.0 * (f32::from(MARGIN) + EDGE_GAP)).max(EDGE_GAP); + ui.set_min_width(spec.min_width.min(room)); + ui.set_max_width(spec.max_width.map_or(room, |w| w.min(room))); ui.label(egui::RichText::new(heading.as_str()).font(theme::semibold(16.0)).color(t.text)); ui.add_space(12.0); cancel = (spec.body)(app, ui, &mut d); @@ -383,3 +387,6 @@ mod tests_print_presets; #[cfg(test)] mod tests_print_advanced; + +#[cfg(test)] +mod tests_scale; diff --git a/crates/ui-egui/src/dialogs/tests_scale.rs b/crates/ui-egui/src/dialogs/tests_scale.rs new file mode 100644 index 00000000..efe9bdc1 --- /dev/null +++ b/crates/ui-egui/src/dialogs/tests_scale.rs @@ -0,0 +1,41 @@ +//! The shared dialog frame at every UI scale (Preferences → User Interface → UI Scaling zooms the +//! whole interface, so a window holds fewer points). + +use serde_json::json; +use vectorcraft_engine::Session; + +use super::*; + +/// The save prompt's rect after a few frames in a window of `physical` pixels at UI scale `zoom`. +fn prompt(app: &mut VectorcraftApp, ctx: &egui::Context, physical: egui::Vec2, zoom: f32) -> (egui::Rect, egui::Rect) { + ctx.set_zoom_factor(zoom); + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, physical / zoom); + for _ in 0..10 { + let input = egui::RawInput { screen_rect: Some(screen), ..Default::default() }; + ctx.run_ui(input, |ui| show(app, ui.ctx())).textures_delta.clear(); + } + let rect = ctx.memory(|m| m.area_rect(egui::Id::new(("dialog", crate::unsaved::KIND)))).unwrap(); + (rect, screen) +} + +#[test] +fn the_save_prompt_fits_its_content_and_the_window_at_every_ui_scale() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + let ctx = egui::Context::default(); + theme::install_fonts(&ctx); + theme::apply(&ctx, Default::default()); + for name in ["Untitled-1", "Quarterly campaign poster, final revision with the printer's notes"] { + app.ui.dialog = Some(Dialog::new(crate::unsaved::KIND, json!({"index": 0, "name": name, "then": "close"}))); + // Scaled up and down while the prompt is open, in a full-size and in the smallest window. + for (w, h, zoom) in + [(1440.0, 900.0, 1.0), (1440.0, 900.0, 2.0), (1440.0, 900.0, 1.5), (800.0, 500.0, 2.0), (800.0, 500.0, 1.0), (1440.0, 900.0, 0.75)] + { + let (rect, screen) = prompt(&mut app, &ctx, egui::vec2(w, h), zoom); + let at = format!("{name}: {w} × {h} px at {zoom}×: {rect:?} in {screen:?}"); + assert!(screen.contains_rect(rect), "inside the window, {at}"); + // As tall as its text and buttons (no empty band around the buttons). + assert!(rect.height() < 200.0, "{at}"); + } + } +}