diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 8ee23ff1..9309f96d 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "386d5e5e5dcdf80487d304f914a0ac0f7e17c5d2134ff5df37b477b8cb5bf3ca" +sha256 = "6bb45afb959816bc6c69a1b262d568d49fe8c9a3dcec8a321231196b0831251f" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 63b8f07d..296e0880 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -29,7 +29,7 @@ Create PDF from images… PDF aus Bildern erstellen… Create PDF from multiple files… PDF aus mehreren Dateien erstellen… Create PDF from clipboard PDF aus Zwischenablage erstellen - Combine files… Dateien kombinieren… + Combine files… Dateien zusammenführen… Save Speichern Save as… Speichern unter… Close file Datei schließen @@ -54,7 +54,7 @@ Comments panel Kommentarbereich Form fields panel Formularfeldbereich Clear form Formular zurücksetzen - Find tools and commands… Tools und Befehle suchen… + Find tools and commands… Werkzeuge und Befehle suchen… Zoom Zoom Actual size Originalgröße Zoom to page level Ganze Seite @@ -96,7 +96,7 @@ Print ({key}) Drucken ({key}) Save ({key}) Speichern ({key}) Document properties ({key}) Dokumenteigenschaften ({key}) - Find tools and commands Tools und Befehle suchen + Find tools and commands Werkzeuge und Befehle suchen Fit to height Seitenhöhe Zoom in Vergrößern Zoom out Verkleinern @@ -322,9 +322,9 @@ Images Bilder Clipboard Zwischenablage Blank page Leere Seite - Combine files Dateien kombinieren - Combine Kombinieren - Add files to combine Dateien zum Kombinieren hinzufügen + Combine files Dateien zusammenführen + Combine Zusammenführen + Add files to combine Dateien zum Zusammenführen hinzufügen Page options Seitenoptionen Page grid Seitenraster Rotate Drehen @@ -337,7 +337,7 @@ Set page boxes Seitenrahmen festlegen Page labels Seitenbeschriftungen Page transitions Seitenübergänge - Add comments Kommentieren + Add comments Kommentare hinzufügen Markup Markierungen Sticky note Notiz Underline Unterstreichen diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index bdf7348f..26de5a6e 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -35,12 +35,14 @@ fn panel_header(ui: &mut egui::Ui, t: &Tokens, title: &str, back: bool) -> (bool if back && icons::button(ui, "chevron-left", 26.0, false, tl!("Back to all tools")).clicked() { go_back = true; } - ui.label(egui::RichText::new(tl!(title)).font(theme::semibold(15.5)).color(t.text)); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if icons::button(ui, "x", 26.0, false, tl!("Close panel")).clicked() { - close = true; - } - }); + // Reserve space for Close before laying out the translated title. Long tool names + // must never expand the fixed-width sidebar or push the Close control off-screen. + let title_width = (ui.available_width() - 26.0 - ui.spacing().item_spacing.x).max(1.0); + ui.add_sized([title_width, 26.0], egui::Label::new(egui::RichText::new(tl!(title)).font(theme::semibold(15.5)).color(t.text)).truncate()) + .on_hover_text(tl!(title)); + if icons::button(ui, "x", 26.0, false, tl!("Close panel")).clicked() { + close = true; + } }); ui.add_space(6.0); (go_back, close) @@ -66,6 +68,17 @@ fn all_tools(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) { }); } +/// Draw a one-line row label without painting into the status chip or beyond the sidebar. +/// Accessibility retains the complete name through the row's WidgetInfo. +fn row_label(ui: &mut egui::Ui, text: &str, font: egui::FontId, color: Color32, left_center: egui::Pos2, max_width: f32) -> bool { + let mut job = egui::text::LayoutJob::simple_singleline(text.to_owned(), font, color); + job.wrap = egui::text::TextWrapping { max_width: max_width.max(1.0), max_rows: 1, break_anywhere: true, ..Default::default() }; + let galley = ui.fonts_mut(|f| f.layout_job(job)); + let elided = galley.elided; + ui.painter().galley(left_center - vec2(0.0, galley.size().y / 2.0), galley, color); + elided +} + fn tool_row(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup) -> egui::Response { let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::click()); resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tl!(g.label))); @@ -73,7 +86,16 @@ fn tool_row(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup) -> egui::Response { ui.painter().rect_filled(rect, CornerRadius::same(6), t.hover); } icons::paint(ui, Rect::from_min_size(rect.min + vec2(6.0, 7.0), vec2(20.0, 20.0)), g.icon, 19.0, hue(g)); - ui.painter().text(rect.left_center() + vec2(36.0, 0.0), Align2::LEFT_CENTER, tl!(g.label), theme::regular(13.5), t.text); + let right_reserved = match (g.badge, g.availability) { + (Some(b), _) => ui.fonts_mut(|f| f.layout_no_wrap(tl!(b).to_owned(), theme::semibold(9.5), Color32::WHITE).size().x) + 26.0, + (None, Availability::Planned(m)) if resp.hovered() => { + let hint = format!("{} · {m}", tl!("Planned")); + ui.fonts_mut(|f| f.layout_no_wrap(hint, theme::medium(10.5), t.text_faint).size().x) + 18.0 + } + _ => 8.0, + }; + let clipped = + row_label(ui, tl!(g.label), theme::regular(13.5), t.text, rect.left_center() + vec2(36.0, 0.0), rect.width() - 36.0 - right_reserved); match (g.badge, g.availability) { (Some(b), _) => { let font = theme::semibold(9.5); @@ -100,6 +122,7 @@ fn tool_row(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup) -> egui::Response { Availability::Planned(m) => format!("{} {m} — {}", tl!("Planned for milestone"), tl!("open to see what it will include")), Availability::Provider => tl!("Optional: needs an AI provider you configure").into(), }; + let tip = if clipped { format!("{} — {tip}", tl!(g.label)) } else { tip }; resp.on_hover_text(tip) } @@ -185,7 +208,6 @@ fn tool_detail(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static 17.0, if ready { hue(g) } else { t.text_faint }, ); - ui.painter().text(rect.left_center() + vec2(34.0, 0.0), Align2::LEFT_CENTER, tl!(item.label), theme::regular(13.0), fg); let (chip, fill, cfg) = match item.availability { Availability::Ready => (tl!("Ready"), Color32::from_rgb(0xDD, 0xF3, 0xE4), Color32::from_rgb(0x1E, 0x7B, 0x43)), Availability::Planned(m) => (m, t.pressed, t.text_muted), @@ -194,9 +216,10 @@ fn tool_detail(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static let font = theme::semibold(9.5); let w = ui.fonts_mut(|f| f.layout_no_wrap(tl!(chip).to_string(), font.clone(), cfg).size().x); let r = Rect::from_center_size(rect.right_center() - vec2(w / 2.0 + 10.0, 0.0), vec2(w + 10.0, 16.0)); + row_label(ui, tl!(item.label), theme::regular(13.0), fg, rect.left_center() + vec2(34.0, 0.0), r.left() - rect.left() - 42.0); ui.painter().rect_filled(r, CornerRadius::same(4), fill); ui.painter().text(r.center(), Align2::CENTER_CENTER, tl!(chip), font, cfg); - if resp.on_hover_text(item.command).clicked() { + if resp.on_hover_text(format!("{} — {}", tl!(item.label), item.command)).clicked() { run = Some(item.command); } } diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index 38cde809..ded8c0ca 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -937,6 +937,27 @@ fn cancelling_then_discarding_an_earlier_dirty_tab_preserves_the_selected_docume } } +#[test] +fn long_german_tool_labels_stay_within_the_sidebar() { + use pdfcraft_ui_egui::{LeftPanel, Mode}; + let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens")); + h.state_mut().set_option("language", "de").unwrap(); + h.state_mut().mode = Mode::AllTools; + h.state_mut().left_open = true; + // The tool heading has to leave room for Back and Close even when translated. + h.state_mut().left = LeftPanel::Tool("fill_sign"); + h.run_steps(4); + let heading = + painted_text(&h).into_iter().find(|(s, ..)| s.starts_with("Ausfüllen und")).expect("translated heading is painted, whole or elided"); + assert!(heading.2.right() <= 272.0, "translated heading runs past the sidebar: {:?}", heading.2); + // A long sub-tool name must stop before its status chip, not paint over the PDF. + h.state_mut().left = LeftPanel::Tool("protect"); + h.run_steps(4); + let item = + painted_text(&h).into_iter().find(|(s, ..)| s == "Ausgeblendete Informationen entfernen").expect("long translated sub-tool is painted"); + assert!(item.2.right() <= 272.0, "translated tool item runs past the sidebar: {:?}", item.2); +} + #[test] fn document_tabs_expose_selection_and_activate_from_keyboard() { use egui::accesskit::Role;