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 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-10 15:44:29 -04:00
1 parent ef63ba3680
commit cac2e8bf41
2 files changed
+81 -33

No files matched your search

+52 -33
View File
@@ -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());
+29
View File
@@ -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| {