M9: a sign tab stays on the page, and the one painted last takes the click

This commit is contained in:
Eric Kaden
2026-10-21 23:11:27 -04:00
parent 515ba9677f
commit 2f7f99d21a
2 changed files with 48 additions and 8 deletions
+40 -5
View File
@@ -428,10 +428,18 @@ fn sign_tab_label(ui: &egui::Ui, size: f32) -> std::sync::Arc<egui::Galley> {
ui.fonts_mut(|f| f.layout_no_wrap(tl!("Sign").to_string(), theme::semibold(size), Color32::WHITE))
}
/// The sign tab of an empty signature field whose widget is at `field` on screen, laid out for
/// its label measured at its largest size: where it is painted and where a click on it signs.
fn sign_tab_at(ui: &egui::Ui, field: Rect) -> Option<SignTab> {
sign_tab_layout(field, sign_tab_label(ui, SIGN_TAB_FONT).size().x)
/// The sign tab of an empty signature field whose widget is at `field` on screen, on the page at
/// `page`, laid out for its label measured at its largest size: where it is painted and where a
/// click on it signs.
fn sign_tab_at(ui: &egui::Ui, field: Rect, page: Rect) -> Option<SignTab> {
sign_tab_on_page(field, page, sign_tab_label(ui, SIGN_TAB_FONT).size().x)
}
/// [`sign_tab_layout`] in the part of `field` that is on `page`: form fields take clicks on the
/// page only, so a widget that straddles the page's edge gets its tab where a click reaches it,
/// or none when that part has no room for one.
pub(crate) fn sign_tab_on_page(field: Rect, page: Rect, label_width: f32) -> Option<SignTab> {
sign_tab_layout(field.intersect(page), label_width)
}
/// Paints a sign tab laid out by [`sign_tab_at`]: an accent chip with the signature icon and
@@ -3262,7 +3270,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
sign_tabs(&doc.signatures, &form, i, can_fill)
.into_iter()
.filter_map(|(name, [x0, y0, x1, y1])| {
sign_tab_at(ui, xf.user_rect(info, i, [x0 as f32, y0 as f32, x1 as f32, y1 as f32])).map(|tab| (name, tab))
sign_tab_at(ui, xf.user_rect(info, i, [x0 as f32, y0 as f32, x1 as f32, y1 as f32]), xf.rect).map(|tab| (name, tab))
})
.collect()
} else {
@@ -5692,6 +5700,33 @@ mod sign_tab_tests {
assert!(near(tab.rect.height(), 14.4) && tab.rect.width() <= 46.0, "{tab:?}");
}
/// A field that straddles the page's edge gets its tab in the part on the page, where clicks
/// reach it, or none when that part has no room for one.
#[test]
fn a_sign_tab_stays_on_the_page() {
let page = Rect::from_min_size(pos2(100.0, 50.0), vec2(600.0, 800.0));
let straddling = Rect::from_min_size(pos2(60.0, 20.0), vec2(300.0, 90.0));
let tab = sign_tab_on_page(straddling, page, 30.0).expect("the part on the page has room");
assert!(page.contains_rect(tab.rect), "{tab:?} on {page:?}");
assert_eq!(tab.rect.height(), SIGN_TAB_MAX, "sized to the 60 px on the page");
let sliver = Rect::from_min_size(pos2(60.0, 20.0), vec2(300.0, 36.0));
assert_eq!(sign_tab_on_page(sliver, page, 30.0), None, "6 px on the page");
let off = Rect::from_min_size(pos2(60.0, 900.0), vec2(300.0, 90.0));
assert_eq!(sign_tab_on_page(off, page, 30.0), None, "wholly off the page");
}
/// Where two sign tabs overlap, a click takes the one painted last, which is on top.
#[test]
fn the_sign_tab_painted_last_takes_the_click() {
let tab = |x: f32| sign_tab_layout(Rect::from_min_size(pos2(x, 0.0), vec2(300.0, 60.0)), 30.0).expect("fits");
let tabs = [("Approver", tab(0.0)), ("Witness", tab(10.0))];
let both = tabs[1].1.rect.left_center() + vec2(2.0, 0.0);
assert!(tabs[0].1.rect.contains(both), "the tabs overlap there");
assert_eq!(crate::forms_ui::tab_under(&tabs, both), Some("Witness"));
assert_eq!(crate::forms_ui::tab_under(&tabs, tabs[0].1.rect.left_center() + vec2(2.0, 0.0)), Some("Approver"));
assert_eq!(crate::forms_ui::tab_under(&tabs, pos2(-5.0, -5.0)), None);
}
/// One page with three signature fields: Approver is empty, Witness is signed (a signature
/// dictionary as its value is enough to list it as signed) and Locked is empty but read-only.
const SIGN_FIELDS: &[u8] = b"%PDF-1.7
+8 -3
View File
@@ -119,6 +119,12 @@ fn widget_at<'a>(xf: &PageXform, info: &DocInfo, page: usize, f: &'a FormField,
.map(|(i, w)| (f, i, w))
}
/// The field whose sign tab is under `p`, of `tabs` in the order they are painted: where tabs
/// overlap, the one painted last is on top.
pub(crate) fn tab_under<'a>(tabs: &[(&'a str, SignTab)], p: egui::Pos2) -> Option<&'a str> {
tabs.iter().rev().find(|(_, tab)| tab.rect.contains(p)).map(|(name, _)| *name)
}
/// Clicks on form widgets of one page. The pointer is on the first widget under it in the form's
/// order, except on a sign tab (`tabs`, with the signature field each one signs), which is drawn
/// over every field and so is on its own field.
@@ -137,9 +143,8 @@ pub(crate) fn page_input(
let pointer = ui.input(|i| i.pointer.hover_pos());
let Some(p) = pointer.filter(|p| xf.rect.contains(*p)) else { return FieldPointer::Away };
// A tab lies inside its field's widget, so that widget is under the pointer too.
let on_tab = tabs.iter().find(|(_, tab)| tab.rect.contains(p)).and_then(|(name, _)| {
form.iter().filter(|f| f.name == *name && f.kind == FormFieldKind::Signature).find_map(|f| widget_at(xf, info, page, f, p))
});
let on_tab = tab_under(tabs, p)
.and_then(|name| form.iter().filter(|f| f.name == name && f.kind == FormFieldKind::Signature).find_map(|f| widget_at(xf, info, page, f, p)));
let hit = on_tab.or_else(|| form.iter().find_map(|f| widget_at(xf, info, page, f, p)));
let Some((f, wi, w)) = hit else { return FieldPointer::Away };
// Empty signature fields are signed by clicking them (Use a certificate); push buttons run