From 3c771e0395ca6d453a63847cf27aeef829119473 Mon Sep 17 00:00:00 2001 From: pauliusiv123-rgb <235694617+pauliusiv123-rgb@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:15:00 +0300 Subject: [PATCH] =?UTF-8?q?Issue=20#425:=20long=20file=20names=20and=20pat?= =?UTF-8?q?hs=20on=20Home=20end=20in=20"=E2=80=A6"=20before=20the=20row's?= =?UTF-8?q?=20details=20(#443)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/ui-egui/src/folders_ui.rs | 5 +++-- crates/ui-egui/src/home.rs | 10 ++++++---- crates/ui-egui/src/widgets.rs | 10 ++++++++++ crates/ui-egui/tests/folders.rs | 26 ++++++++++++++++++++++++++ crates/ui-egui/tests/ui.rs | 30 ++++++++++++++++++++++++++++++ 5 files changed, 75 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/folders_ui.rs b/crates/ui-egui/src/folders_ui.rs index f5cd90b5..727417e2 100644 --- a/crates/ui-egui/src/folders_ui.rs +++ b/crates/ui-egui/src/folders_ui.rs @@ -364,11 +364,12 @@ pub(crate) fn section(app: &mut PdfCraftApp, ui: &mut egui::Ui) { 18.0, egui::Color32::from_rgb(0xE0, 0x3E, 0x3E), ); - ui.painter().text(rect.min + vec2(62.0, 19.0), Align2::LEFT_CENTER, crate::bidi::visual(&f.name), theme::medium(13.0), t.text); let size = crate::panels::human_size(usize::try_from(f.size).unwrap_or(usize::MAX)); let when = f.modified.map(crate::combine_ui::ago).unwrap_or_default(); let detail = if when.is_empty() { size } else { format!("{when} · {size}") }; - ui.painter().text(rect.right_center() - vec2(12.0, 0.0), Align2::RIGHT_CENTER, detail, theme::regular(12.0), t.text_muted); + let detail = ui.painter().text(rect.right_center() - vec2(12.0, 0.0), Align2::RIGHT_CENTER, detail, theme::regular(12.0), t.text_muted); + let width = detail.left() - 16.0 - (rect.left() + 62.0); + widgets::row_text(ui, rect.min + vec2(62.0, 19.0), crate::bidi::visual(&f.name), theme::medium(13.0), t.text, width); if resp.on_hover_text(&f.path).clicked() { open = Some(f.path.clone()); } diff --git a/crates/ui-egui/src/home.rs b/crates/ui-egui/src/home.rs index 7e21c126..35f4efb6 100644 --- a/crates/ui-egui/src/home.rs +++ b/crates/ui-egui/src/home.rs @@ -123,16 +123,18 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) { 22.0, egui::Color32::from_rgb(0xE0, 0x3E, 0x3E), ); - ui.painter().text(rect.min + vec2(46.0, 15.0), Align2::LEFT_CENTER, crate::bidi::visual(&r.name), theme::medium(13.5), t.text); - ui.painter().text(rect.min + vec2(46.0, 32.0), Align2::LEFT_CENTER, crate::bidi::visual(&r.path), theme::regular(11.0), t.text_faint); - ui.painter().text( + let detail = ui.painter().text( rect.right_center() - vec2(12.0, 0.0), Align2::RIGHT_CENTER, format!("{} {} · {}", r.pages, tl!("pages"), human_size(r.size)), theme::regular(12.0), t.text_muted, ); - if resp.clicked() { + // The name and path stop short of the page count; hovering shows the whole path. + let width = detail.left() - 16.0 - (rect.left() + 46.0); + widgets::row_text(ui, rect.min + vec2(46.0, 15.0), crate::bidi::visual(&r.name), theme::medium(13.5), t.text, width); + widgets::row_text(ui, rect.min + vec2(46.0, 32.0), crate::bidi::visual(&r.path), theme::regular(11.0), t.text_faint, width); + if resp.on_hover_text(&r.path).clicked() { open = Some(r.path.clone()); } } diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index dc06ff1c..8351c126 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -81,6 +81,16 @@ pub fn section_title(ui: &mut egui::Ui, text: &str) { ui.add_space(2.0); } +/// Paint one line of `text` left-aligned and vertically centred on `pos`, cut with "…" so it is +/// at most `max_width` wide: a long file name or path stays inside its row (#425). +pub fn row_text(ui: &egui::Ui, pos: egui::Pos2, text: impl Into, font: egui::FontId, color: Color32, max_width: f32) { + let mut job = egui::text::LayoutJob::simple_singleline(text.into(), font, color); + job.wrap = egui::text::TextWrapping::truncate_at_width(max_width.max(0.0)); + let galley = ui.painter().layout_job(job); + let rect = Align2::LEFT_CENTER.anchor_size(pos, galley.size()); + ui.painter().galley(rect.min, galley, color); +} + /// Transient message at the bottom centre. pub fn toast(app: &mut PdfCraftApp, ctx: &egui::Context) { let Some((msg, start)) = app.toast.clone() else { return }; diff --git a/crates/ui-egui/tests/folders.rs b/crates/ui-egui/tests/folders.rs index 0f9fe80b..583cc35e 100644 --- a/crates/ui-egui/tests/folders.rs +++ b/crates/ui-egui/tests/folders.rs @@ -197,6 +197,32 @@ fn show_more_reveals_the_older_pdfs() { h.get_by_label("Show less"); } +#[test] +fn a_long_pdf_name_is_cut_before_its_date_and_size() { + let dir = TempFolder::new("long"); + // Short enough for Windows' 260-character paths under the temp folder. + let name = format!("{}.pdf", "Signed contract with a long name ".repeat(4)); + dir.file(&name, FIXTURE, Duration::from_secs(10)); + let folder = dir.path(); + let mut h = harness(move |app| { + app.pin_folder(&folder); + }); + h.run_steps(2); + let text: Vec<(String, bool, egui::Rect)> = h + .output() + .shapes + .iter() + .filter_map(|c| match &c.shape { + egui::Shape::Text(t) => Some((t.galley.text().to_owned(), t.galley.elided, t.visual_bounding_rect())), + _ => None, + }) + .collect(); + let (_, cut, rect) = text.iter().find(|(s, ..)| *s == name).expect("the PDF is listed"); + let detail = text.iter().find(|(s, _, r)| s.contains('·') && r.y_range().contains(rect.center().y)).expect("its row shows the date and size").2; + assert!(*cut, "the name is cut short"); + assert!(rect.right() < detail.left(), "the name ends before the date and size"); +} + #[test] fn pinning_is_limited_and_never_duplicates() { let mut app = PdfCraftApp::new(); diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index ecc7a21a..2582ce88 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -37,6 +37,36 @@ fn home_shows_welcome_and_tools() { h.get_by_label("Open file"); } +/// Every piece of text painted on the last frame: its whole text, whether it was cut with "…", +/// and where it was drawn. +fn painted_text(h: &Harness<'static, PdfCraftApp>) -> Vec<(String, bool, egui::Rect)> { + h.output() + .shapes + .iter() + .filter_map(|c| match &c.shape { + egui::Shape::Text(t) => Some((t.galley.text().to_owned(), t.galley.elided, t.visual_bounding_rect())), + _ => None, + }) + .collect() +} + +#[test] +fn long_recent_names_and_paths_are_cut_before_the_page_count() { + let name = format!("{}.pdf", "Quarterly report final version ".repeat(10)); + let path = format!("/{}/{name}", "A folder with a long name".repeat(8)); + let h = harness({ + let (name, path) = (name.clone(), path.clone()); + move |app| app.recent.push(pdfcraft_ui_egui::RecentFile { name, path, pages: 7, size: 2048 }) + }); + let text = painted_text(&h); + let detail = text.iter().find(|(s, ..)| s.starts_with("7 pages")).expect("the row shows its page count").2; + for whole in [&name, &path] { + let (_, cut, rect) = text.iter().find(|(s, ..)| s == whole).expect("the row shows the name and the path"); + assert!(*cut, "{whole:?} is cut short"); + assert!(rect.right() < detail.left(), "{whole:?} ends before the page count"); + } +} + #[test] fn every_catalog_tool_is_listed_after_view_more() { let mut h = harness(|_| {});