Issue #236: the Save-changes prompt shortens very long names to fit the window (#322)

Wrapping the title (#161) kept the prompt narrow, but a long enough name
still grew it taller than the window and pushed Save, Cancel and Don't
save off-screen. On the web a `?file=` URL names the document, so the
name has no length limit.

Past 80 characters the name now gives way in the middle ("start…end"),
keeping the start and the extension and version suffix; the full name
shows on hover. The new test checks the title and buttons stay inside
the desktop's minimum window (820×520) for long Latin, Thai and CJK
names and a 2000-character one.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-08 15:37:21 -04:00
1 parent 9140b44792
commit ca9541069f
2 files changed
+61 -2

No files matched your search

+21 -2
View File
@@ -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<Option<bool>> = 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.
+40
View File
@@ -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());