From cac2e8bf4130827bc75de5c2ef04818559943d12 Mon Sep 17 00:00:00 2001 From: DWAA1660 Date: Sat, 10 Oct 2026 13:44:29 -0600 Subject: [PATCH] Notice bar: the message wraps beside its buttons and lines up with them (#785) (#832) The notice bar above the pages (security, forms, XFA, repaired file, warnings, signatures) laid its message out before its buttons. In a narrow document area (a side panel open, a longer translation) the message ran under "Security settings" and on into the side panel, and it wasn't centred on the buttons. A shared notice_bar helper now lays the buttons out first, right to left, centred on a 28 px row, and gives the message the width that's left: it wraps there (cut short with the full text on hover when less than 120 px is left) and its first line is centred on the buttons' row. Regression test: editing::the_security_notice_wraps_beside_its_buttons_and_lines_up_with_them (one line, wrapped and cut-short widths; fails on main). Co-authored-by: DWAA1660 Co-authored-by: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 85 ++++++++++++++++++++------------- crates/ui-egui/tests/editing.rs | 29 +++++++++++ 2 files changed, 81 insertions(+), 33 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 4a84bec90..5605292d1 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2950,16 +2950,10 @@ fn notices( let xfa = doc.xfa.as_ref(); if let Some((icon, color, template, arg)) = signed { let mut open = false; - egui::Frame::NONE.fill(t.accent_soft).inner_margin(egui::Margin::symmetric(14, 7)).show(ui, |ui| { - ui.horizontal(|ui| { - ui.add(icons::image(icon, 16.0, color)); - ui.label(egui::RichText::new(crate::i18n::fmt(tl!(template), &[("by", &arg)])).color(t.text)); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if crate::widgets::pill_button(ui, tl!("Signature panel"), false).clicked() { - open = true; - } - }); - }); + notice_bar(ui, t, icon, color, crate::i18n::fmt(tl!(template), &[("by", &arg)]), |ui| { + if crate::widgets::pill_button(ui, tl!("Signature panel"), false).clicked() { + open = true; + } }); return open.then_some(Notice::Signatures); } @@ -3010,29 +3004,23 @@ fn notices( None }; let (icon, text, fields) = msg?; - egui::Frame::NONE.fill(t.accent_soft).inner_margin(egui::Margin::symmetric(14, 7)).show(ui, |ui| { - ui.horizontal(|ui| { - ui.add(icons::image(icon, 16.0, t.accent_text)); - ui.label(egui::RichText::new(text).color(t.text)); - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if icons::button(ui, "x", 22.0, false, tl!("Dismiss")).clicked() { - view.notice_dismissed = true; - } - if fields { - let label = if view.highlight_fields { tl!("Hide field highlights") } else { tl!("Highlight fields") }; - if crate::widgets::pill_button(ui, label, view.highlight_fields).clicked() { - view.highlight_fields = !view.highlight_fields; - toggled = Some(Notice::FieldHighlights(view.highlight_fields)); - } - } - if secured && crate::widgets::pill_button(ui, tl!("Security settings"), false).clicked() { - open_security = true; - } - if repaired && !secured && info.fields.is_empty() && crate::widgets::pill_button(ui, tl!("Details"), false).clicked() { - open_repairs = true; - } - }); - }); + notice_bar(ui, t, icon, t.accent_text, text, |ui| { + if icons::button(ui, "x", 22.0, false, tl!("Dismiss")).clicked() { + view.notice_dismissed = true; + } + if fields { + let label = if view.highlight_fields { tl!("Hide field highlights") } else { tl!("Highlight fields") }; + if crate::widgets::pill_button(ui, label, view.highlight_fields).clicked() { + view.highlight_fields = !view.highlight_fields; + toggled = Some(Notice::FieldHighlights(view.highlight_fields)); + } + } + if secured && crate::widgets::pill_button(ui, tl!("Security settings"), false).clicked() { + open_security = true; + } + if repaired && !secured && info.fields.is_empty() && crate::widgets::pill_button(ui, tl!("Details"), false).clicked() { + open_repairs = true; + } }); if open_repairs { return Some(Notice::Repairs); @@ -3040,6 +3028,37 @@ fn notices( open_security.then_some(Notice::Security).or(toggled) } +/// One notice bar: an icon and a message on the left, `buttons` (added right to left) on the +/// right. The buttons are laid out first, so the message only gets the width they leave: it +/// wraps there (or, when very little is left, is cut short with the full text on hover) instead +/// of running under the buttons and past the document area into the side panel (#785). The +/// buttons are centred on a row of button height, and the message's first line is centred on +/// the same row; a wrapped message grows the bar downwards. +fn notice_bar(ui: &mut egui::Ui, t: &Tokens, icon: &str, icon_color: Color32, text: String, buttons: impl FnOnce(&mut egui::Ui)) { + const ROW: f32 = 28.0; + const ICON: f32 = 16.0; + egui::Frame::NONE.fill(t.accent_soft).inner_margin(egui::Margin::symmetric(14, 7)).show(ui, |ui| { + let width = ui.available_width(); + ui.allocate_ui_with_layout(vec2(width, ROW), egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.set_min_height(ROW); + buttons(ui); + let rest = ui.available_width().max(0.0); + ui.allocate_ui_with_layout(vec2(rest, ROW), egui::Layout::top_down(egui::Align::Min), |ui| { + let font = egui::TextStyle::Body.resolve(ui.style()); + let line = ui.fonts_mut(|f| f.row_height(&font)).max(ICON); + ui.add_space(((ROW - line) / 2.0).max(0.0)); + ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| { + let (slot, _) = ui.allocate_exact_size(vec2(ICON, line), Sense::hover()); + icons::paint(ui, Rect::from_center_size(slot.center(), vec2(ICON, ICON)), icon, ICON, icon_color); + let label = egui::Label::new(egui::RichText::new(text).color(t.text)); + // Wrapping into a sliver would stack the message a few letters per line. + ui.add(if ui.available_width() >= 120.0 { label.wrap() } else { label.truncate() }); + }); + }); + }); + }); +} + /// The floating quick-action bar at the left edge of the document area. fn quick_bar(app: &mut PdfCraftApp, area: Rect, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); diff --git a/crates/ui-egui/tests/editing.rs b/crates/ui-egui/tests/editing.rs index c9ffcbd14..f1a267ee2 100644 --- a/crates/ui-egui/tests/editing.rs +++ b/crates/ui-egui/tests/editing.rs @@ -1467,6 +1467,35 @@ fn password_prompt_opens_and_security_tab_reports_the_details() { h.get_by_label("User password"); } +/// #785: the notice's message used to be laid out before its buttons, so in a narrow document +/// area (a side panel open) it ran under "Security settings" and on into the panel, and it sat +/// off the buttons' centre line. +#[test] +fn the_security_notice_wraps_beside_its_buttons_and_lines_up_with_them() { + // Wide: one line. Narrower: wrapped. Narrowest: cut short (the full text is on hover). + for (width, wraps) in [(1700.0, false), (1250.0, true), (900.0, false)] { + let mut h = Harness::builder().with_size(egui::vec2(width, 800.0)).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app.open_bytes("locked.pdf", None, protected("", "owner", 0b0100)).unwrap(); + app.set_option("panel", "bookmarks").unwrap(); + app + }); + h.run_steps(4); + let msg = h.get_by_label_contains("This document is secured").rect(); + let button = h.get_by_label("Security settings").rect(); + let dismiss = h.get_by_label("Dismiss").rect(); + assert!(msg.right() <= button.left() + 0.5, "{width}: the message stops before the buttons: {msg:?} vs {button:?}"); + assert!(button.right() <= dismiss.left() + 0.5, "{width}: {button:?} vs {dismiss:?}"); + assert!((button.center().y - dismiss.center().y).abs() <= 0.5, "{width}: the buttons share a centre line"); + assert!(msg.top() >= button.top(), "{width}: the message stays inside the bar: {msg:?} vs {button:?}"); + let lines = (msg.height() / 14.0).floor().max(1.0); + let first_line = msg.top() + msg.height() / lines / 2.0; + assert!((first_line - button.center().y).abs() <= 1.5, "{width}: first line centred on the buttons: {msg:?} vs {button:?}"); + assert_eq!(msg.height() > button.height(), wraps, "{width}: {msg:?}"); + } +} + #[test] fn restricted_documents_show_a_notice_and_block_page_changes() { let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {