diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index f1243f2d5e..141355344d 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1344,18 +1344,24 @@ fn save_prompt(app: &mut PdfCraftApp, ctx: &egui::Context) { return; }; let t = Tokens::get(ctx); + // Wrapping alone (#161) still let a long enough name (a web `?file=` URL has no limit) grow + // the prompt taller than the window (#236); 80 characters wrap to a few lines. + let shown = shorten_middle(&name, 80); let mut choice: Option> = None; let modal = egui::Modal::new(egui::Id::new("save_prompt")).show(ctx, |ui| { ui.set_width(420.0); ui.horizontal(|ui| { ui.add(crate::icons::image("save", 22.0, t.accent)); - ui.add( + let title = ui.add( egui::Label::new( - egui::RichText::new(crate::i18n::fmt(tl!("Save changes to “{name}” before closing?"), &[("name", &name)])) + egui::RichText::new(crate::i18n::fmt(tl!("Save changes to “{name}” before closing?"), &[("name", &shown)])) .font(theme::semibold(16.0)), ) .wrap(), ); + if shown != name { + title.on_hover_text(&name); + } }); ui.add_space(6.0); ui.label(egui::RichText::new(tl!("Your changes will be lost if you don't save them.")).color(t.text_muted)); @@ -1381,6 +1387,19 @@ fn save_prompt(app: &mut PdfCraftApp, ctx: &egui::Context) { } } +/// `name` cut to at most `max` characters by replacing its middle with "…", keeping the start and +/// the end, where the extension and version suffixes sit. Counts `char`s, so it never splits one. +fn shorten_middle(name: &str, max: usize) -> String { + let count = name.chars().count(); + if count <= max { + return name.to_string(); + } + let tail = max / 4; + let head: String = name.chars().take(max.saturating_sub(tail + 1)).collect(); + let end: String = name.chars().skip(count.saturating_sub(tail)).collect(); + format!("{head}…{end}") +} + /// "Open this web page?" when a document's link, button or script asks to open an address /// (#90, #91). Shows where the address really goes and the whole address; Cancel is the default, /// and Escape or clicking outside cancels. diff --git a/crates/ui-egui/tests/editing.rs b/crates/ui-egui/tests/editing.rs index 21baf309a7..3aed0f6cb0 100644 --- a/crates/ui-egui/tests/editing.rs +++ b/crates/ui-egui/tests/editing.rs @@ -411,6 +411,46 @@ fn save_prompt_stays_inside_the_screen_for_a_long_filename() { } } +#[test] +fn save_prompt_fits_the_smallest_window_whatever_the_name() { + // Issue #236: wrapping (#161) kept the prompt narrow, but a long enough name still grew it + // taller than the window and pushed the buttons off-screen. On the web a `?file=` URL names + // the document, so the name has no length limit. Long names now give way in the middle. + let names = [ + format!("{}.pdf", "a".repeat(400)), + format!("{}.pdf", "Quarterly_Report_FY2026_Final_v3_".repeat(12)), + format!("{}.pdf", "รายงานประจำปีงบประมาณ".repeat(15)), + format!("{}.pdf", "年".repeat(251)), + format!("{}.pdf", "W".repeat(2000)), + ]; + // The desktop window's minimum inner size (apps/pdfcraft/src/main.rs). + let size = egui::vec2(820.0, 520.0); + for name in names { + let start: String = name.chars().take(10).collect(); + let mut h = Harness::builder().with_size(size).build_eframe(move |_cc| { + let mut app = PdfCraftApp::new(); + app.open_bytes(&name, None, fixture(1)).expect("fixture opens"); + app.close_request = Some(CloseRequest::Tab(app.views[0].id)); + app + }); + h.run_steps(4); + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, size); + let inside = |r: egui::Rect| screen.contains(r.min) && screen.contains(r.max); + let title = h.get_by_label_contains("Save changes to"); + let title_rect = title.rect(); + assert!(inside(title_rect), "{start}…: the title rect {title_rect:?} leaves the screen"); + let opening = format!("Save changes to “{start}"); + assert!( + h.query_by(|n| n.value().is_some_and(|l| l.starts_with(&opening) && l.contains('…') && l.contains(".pdf” before closing?"))).is_some(), + "{start}…: the title keeps the start and the extension" + ); + for button in ["Save", "Cancel", "Don't save"] { + let rect = h.get_by_label(button).rect(); + assert!(inside(rect), "{start}…: the {button} button rect {rect:?} leaves the screen"); + } + } +} + #[test] fn document_properties_edit_is_one_undoable_step() { let mut h = harness(1, |app| app.set_option("dialog", "properties").unwrap());