mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 07:39:08 +00:00
fix(ui): form fields and Prepare decide a press by one hit test
Forms: forms_ui::hit is the fields' only hit test (cursor and press). The press opens a text field (#2213) and its click acts on the field the press was on, from the page router, instead of the field under the release point: a press on one check box released over the one beside it ticked the second. Now it ticks neither, as a button pressed and released off itself does nothing. Forms no longer wait on comments::select_grabs: a comment that moves (a note, a shape, a signature, a Fill & Sign mark) hits on the Placed layer, over form fields, so it still takes a press over a field (a signature can be dragged off the field it covers), and text markup stays on the Comment layer, under them, so a click on a box under a highlight ticks the box. select_grabs goes. Prepare Form: prepare::hit (the selected field's corner handles, then a field, on Placed so a field being moved stays over the comments as before) gives the cursor, and the drag, click and double click act on what the press was on; a press held by something else is not Prepare's. New tests: a_press_on_one_check_box_released_over_another_ticks_neither (fails without this commit: the second box is ticked) and a_check_box_under_a_highlight_takes_the_click (fails with markup on the Placed layer). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
7 files changed
+184
-75
No files matched your search
@@ -1129,7 +1129,24 @@ fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut PageFrame<
|
||||
let PageFrame { resp, ptr, pointer, .. } = *frame;
|
||||
let cx = PageToolContext { ui, resp, xf: &xf, ptr, page: i, doc, info };
|
||||
let pcx = comments::PageCx { page: i, xf: &xf, ptr, info, tool, prefs, allowed, hidden: comments_hidden, redact_mark };
|
||||
let hits = |view: &DocView, p: Pos2| [link_hit(info, &xf, i, p), comments::hit(&pcx, view, p)].into_iter().flatten();
|
||||
let field_tool = match tool {
|
||||
QuickTool::Field(f) => Some(f),
|
||||
_ => None,
|
||||
};
|
||||
// While a table is being set up, nothing else is drawn or moved.
|
||||
let prepare_allowed = can_modify && table_preview.is_none() && frame.out.open_table.is_none();
|
||||
// What lies under a point, bottom up: links, comments, then form fields (Prepare's fields,
|
||||
// being moved, over the comments; filled ones under the comments that move).
|
||||
let hits = |view: &DocView, p: Pos2| {
|
||||
let fields = if preparing {
|
||||
crate::prepare::hit(&xf, info, i, form, field_tool, prepare_allowed, &view.prepare, p)
|
||||
} else if tool == QuickTool::Select {
|
||||
crate::forms_ui::hit(&xf, info, i, form, can_fill, p)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
[link_hit(info, &xf, i, p), comments::hit(&pcx, view, p), fields].into_iter().flatten()
|
||||
};
|
||||
if let Some(o) = ptr.pressed().filter(|o| xf.rect.contains(*o)) {
|
||||
let at = hits(view, o).collect::<Vec<_>>();
|
||||
view.router.press(i, at, tool);
|
||||
@@ -1231,13 +1248,7 @@ fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut PageFrame<
|
||||
frame.out.open_boxes = true;
|
||||
}
|
||||
let on_field = if preparing {
|
||||
let field_tool = match tool {
|
||||
QuickTool::Field(f) => Some(f),
|
||||
_ => None,
|
||||
};
|
||||
// While a table is being set up, nothing else is drawn or moved.
|
||||
let allowed = can_modify && table_preview.is_none() && frame.out.open_table.is_none();
|
||||
let o = crate::prepare::page_input(cx, form, field_tool, allowed, view);
|
||||
let o = crate::prepare::page_input(cx, form, field_tool, prepare_allowed, view);
|
||||
frame.out.field_props |= o.properties;
|
||||
frame.out.field_placed |= o.placed;
|
||||
frame.out.open_table = frame.out.open_table.or(o.table);
|
||||
@@ -1245,7 +1256,6 @@ fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut PageFrame<
|
||||
} else {
|
||||
tool == QuickTool::Select
|
||||
&& !(editing_content && can_modify && crate::content_ui::select_grabs(&xf, ptr, i, info, added, view))
|
||||
&& !comments::select_grabs(&pcx, view)
|
||||
&& crate::forms_ui::page_input(cx, form, can_fill, view)
|
||||
};
|
||||
// Redact draws boxes off text; so does Highlight (an area highlight, as in Acrobat).
|
||||
|
||||
@@ -20,6 +20,12 @@ pub(crate) enum Owner {
|
||||
Comment { index: usize, part: CommentPart },
|
||||
/// A link, by its index in the document's links.
|
||||
Link { index: usize },
|
||||
/// A form field's widget, by the field's index in the form and the widget's in the field.
|
||||
Field { field: usize, widget: usize },
|
||||
/// Prepare Form: a field's widget, to select and move.
|
||||
PrepareField { field: usize, widget: usize },
|
||||
/// Prepare Form: a corner handle of the selected field, to resize it.
|
||||
PrepareCorner { left: bool, top: bool },
|
||||
}
|
||||
|
||||
/// The part of a comment a hit is on.
|
||||
|
||||
@@ -37,7 +37,7 @@ pub(crate) use geometry::{corner_ratio, page_rotation, resized};
|
||||
pub use geometry::{drawn_text_box_rect, text_box_rect};
|
||||
#[cfg(test)]
|
||||
use input::hover_tip;
|
||||
pub(crate) use input::{describe, fill_grabs, hit, keys, nudge_keys, page_after_text, page_input, select_grabs};
|
||||
pub(crate) use input::{describe, fill_grabs, hit, keys, nudge_keys, page_after_text, page_input};
|
||||
pub use menu::{CanvasAction, fill_picker, recent_custom_colours, remember_custom_colour, status_badge, swatch_grid};
|
||||
#[cfg(test)]
|
||||
use menu::{RECENT_CUSTOM, same_colour};
|
||||
|
||||
@@ -98,8 +98,9 @@ pub(crate) fn hit(cx: &PageCx<'_>, view: &DocView, p: Pos2) -> Option<Hit> {
|
||||
}
|
||||
let markup = is_markup(&a.subtype);
|
||||
let icon = if cx.allowed && !markup { egui::CursorIcon::Move } else { egui::CursorIcon::PointingHand };
|
||||
// Signatures and Fill & Sign's marks are placed on the page rather than comments on it.
|
||||
let layer = if a.fill_sign { Layer::Placed } else { Layer::Comment };
|
||||
// What a drag moves whole (a note, a shape, a signature, a Fill & Sign mark) is placed on the
|
||||
// page, over a form field; text markup lies on the text, under one.
|
||||
let layer = if a.fill_sign || !markup { Layer::Placed } else { Layer::Comment };
|
||||
// Text markup does not move: a click selects it, a drag across it selects the text.
|
||||
Some(Hit { declines_drag: markup, ..Hit::new(Owner::Comment { index: a.index, part: CommentPart::Body }, layer, icon) })
|
||||
}
|
||||
@@ -124,12 +125,6 @@ pub(crate) fn fill_grabs(cx: &PageCx<'_>, view: &DocView) -> bool {
|
||||
matches!(cx.tool, QuickTool::Fill(_)) && grabs(cx, view)
|
||||
}
|
||||
|
||||
/// With the Select tool, a comment that would move (not text markup) is picked up before a form
|
||||
/// field under it, so a signature placed over a field can still be dragged off it.
|
||||
pub(crate) fn select_grabs(cx: &PageCx<'_>, view: &DocView) -> bool {
|
||||
cx.tool == QuickTool::Select && grabs(cx, view)
|
||||
}
|
||||
|
||||
/// With a shape tool, the selected comment's handles are picked up ([`hit`]).
|
||||
fn draw_grabs(cx: &PageCx<'_>, view: &DocView) -> bool {
|
||||
matches!(cx.tool, QuickTool::Comment(_)) && grabs(cx, view)
|
||||
|
||||
@@ -15,6 +15,7 @@ use pdfcraft_render::DocInfo;
|
||||
|
||||
use crate::canvas::{DocView, PageToolContext, PageXform};
|
||||
use crate::page_cursor::Layer;
|
||||
use crate::page_router::{Hit, Owner};
|
||||
use crate::theme::Tokens;
|
||||
|
||||
const FOCUS_BLUE: Color32 = Color32::from_rgb(0x14, 0x73, 0xE6);
|
||||
@@ -151,48 +152,64 @@ fn toggle_edit(f: &FormField, w: &FormWidget) -> Option<Edit> {
|
||||
Some(Edit::SetFieldValue { name: f.name.clone(), value })
|
||||
}
|
||||
|
||||
/// Clicks on form widgets of one page. `true` when a field owns the press or click.
|
||||
pub(crate) fn page_input(cx: PageToolContext<'_>, form: &[FormField], allowed: bool, view: &mut DocView) -> bool {
|
||||
let PageToolContext { ui, resp, xf, ptr, page, info, .. } = cx;
|
||||
let Some(p) = ptr.within(xf.rect) else { return false };
|
||||
let hit = form.iter().find_map(|f| {
|
||||
/// Whether `w`, a widget of `f`, takes a press: fillable, a push button with an action, or an
|
||||
/// empty signature field (signed by clicking it, Use a certificate). A radio button of a group can
|
||||
/// be read-only on its own.
|
||||
fn usable(f: &FormField, w: &FormWidget, allowed: bool) -> bool {
|
||||
let pressable = f.kind == FormFieldKind::PushButton && !f.read_only() && f.button.is_some();
|
||||
allowed && !w.read_only && (fillable(f) || pressable || f.kind == FormFieldKind::Signature)
|
||||
}
|
||||
|
||||
/// The form widget under `p` on `page`: the one hit test of the fields, for the cursor and the
|
||||
/// press. Over the page's text too: a check box drawn with a glyph, or set on printed text, is a
|
||||
/// button, not text to select.
|
||||
pub(crate) fn hit(xf: &PageXform, info: &DocInfo, page: usize, form: &[FormField], allowed: bool, p: Pos2) -> Option<Hit> {
|
||||
if !xf.rect.contains(p) {
|
||||
return None;
|
||||
}
|
||||
let (field, f, widget, w) = form.iter().enumerate().find_map(|(fi, f)| {
|
||||
f.widgets
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, w)| w.page == Some(page) && !w.hidden && widget_rect(xf, info, page, w.rect).contains(p))
|
||||
.map(|(i, w)| (f, i, w))
|
||||
});
|
||||
let Some((f, wi, w)) = hit else { return false };
|
||||
// The field's tooltip (`/TU`), as Acrobat shows it on hover (#3854). ISO 32000-2 Table 226
|
||||
// makes it the name the user interface identifies the field by.
|
||||
if let Some(tip) = f.tooltip.as_deref().filter(|t| !t.trim().is_empty()) {
|
||||
.map(|(wi, w)| (fi, f, wi, w))
|
||||
})?;
|
||||
let icon = match (usable(f, w, allowed), f.kind) {
|
||||
(false, _) => egui::CursorIcon::NotAllowed,
|
||||
(true, FormFieldKind::Text) => egui::CursorIcon::Text,
|
||||
_ => egui::CursorIcon::PointingHand,
|
||||
};
|
||||
Some(Hit::new(Owner::Field { field, widget }, Layer::Field, icon))
|
||||
}
|
||||
|
||||
/// Presses and clicks on form widgets of one page, on the field the press was on ([`hit`]).
|
||||
/// `true` while a field holds the press.
|
||||
pub(crate) fn page_input(cx: PageToolContext<'_>, form: &[FormField], allowed: bool, view: &mut DocView) -> bool {
|
||||
let PageToolContext { ui, resp, xf, ptr, page, info, .. } = cx;
|
||||
// The field's tooltip (`/TU`) under the pointer, as Acrobat shows it on hover (#3854). ISO
|
||||
// 32000-2 Table 226 makes it the name the user interface identifies the field by.
|
||||
if let Some(p) = ptr.within(xf.rect)
|
||||
&& let Some(Owner::Field { field, .. }) = hit(xf, info, page, form, allowed, p).map(|h| h.owner)
|
||||
&& let Some(tip) = form.get(field).and_then(|f| f.tooltip.as_deref()).filter(|t| !t.trim().is_empty())
|
||||
{
|
||||
resp.clone().on_hover_text_at_pointer(tip);
|
||||
}
|
||||
// Empty signature fields are signed by clicking them (Use a certificate); push buttons run
|
||||
// their action.
|
||||
let pressable = f.kind == FormFieldKind::PushButton && !f.read_only() && f.button.is_some();
|
||||
// A radio button of a group can be read-only on its own.
|
||||
let usable = allowed && !w.read_only && (fillable(f) || pressable || f.kind == FormFieldKind::Signature);
|
||||
// Over the page's text too: a check box drawn with a glyph, or set on printed text, is a
|
||||
// button, not text to select.
|
||||
view.cursor.claim(
|
||||
Layer::Field,
|
||||
match (usable, f.kind) {
|
||||
(false, _) => egui::CursorIcon::NotAllowed,
|
||||
(true, FormFieldKind::Text) => egui::CursorIcon::Text,
|
||||
_ => egui::CursorIcon::PointingHand,
|
||||
},
|
||||
);
|
||||
let Some(Owner::Field { field, widget: wi }) = view.router.owner(page) else { return false };
|
||||
let Some((f, w)) = form.get(field).and_then(|f| f.widgets.get(wi).map(|w| (f, w))) else { return false };
|
||||
let usable = usable(f, w, allowed);
|
||||
// A text field opens on the press rather than the click, so a drag that starts on it
|
||||
// selects its text (#2213).
|
||||
let pressed = f.kind == FormFieldKind::Text && usable && resp.is_pointer_button_down_on() && ptr.pressed().is_some();
|
||||
if pressed && view.forms.focus.as_ref().is_none_or(|x| x.name != f.name || x.widget != wi) {
|
||||
if f.kind == FormFieldKind::Text
|
||||
&& usable
|
||||
&& let Some(at) = ptr.pressed()
|
||||
&& view.forms.focus.as_ref().is_none_or(|x| x.name != f.name || x.widget != wi)
|
||||
{
|
||||
commit(view, form);
|
||||
view.forms.focus = Some(Focus { press: Some(p), ..open_focus(f, wi) });
|
||||
view.forms.focus = Some(Focus { press: Some(at), ..open_focus(f, wi) });
|
||||
view.forms.pressed = true;
|
||||
}
|
||||
if !resp.clicked() {
|
||||
return resp.is_pointer_button_down_on();
|
||||
return true;
|
||||
}
|
||||
if !allowed {
|
||||
view.forms.notice = Some(FormNotice::Security);
|
||||
@@ -231,6 +248,7 @@ pub(crate) fn page_input(cx: PageToolContext<'_>, form: &[FormField], allowed: b
|
||||
}
|
||||
}
|
||||
FormFieldKind::List => {
|
||||
let Some(p) = ptr.within(xf.rect) else { return true };
|
||||
commit(view, form);
|
||||
view.forms.focus = Some(open_focus(f, wi));
|
||||
let r = widget_rect(xf, info, page, w.rect);
|
||||
|
||||
@@ -13,6 +13,7 @@ use pdfcraft_render::DocInfo;
|
||||
use crate::canvas::{DocView, PageToolContext, PageXform};
|
||||
use crate::comments;
|
||||
use crate::page_cursor::Layer;
|
||||
use crate::page_router::{Hit, Owner};
|
||||
use crate::theme;
|
||||
use crate::widgets;
|
||||
|
||||
@@ -252,6 +253,38 @@ pub(crate) struct Outcome {
|
||||
pub table: Option<(usize, [f64; 4])>,
|
||||
}
|
||||
|
||||
/// What Prepare Form's Select tool has under `p` on `page`: the selected field's corner handles,
|
||||
/// then a field. A field being moved is placed on the page over the comments; nothing with a field
|
||||
/// tool, which draws anywhere.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn hit(
|
||||
xf: &PageXform,
|
||||
info: &DocInfo,
|
||||
page: usize,
|
||||
form: &[FormField],
|
||||
tool: Option<FieldTool>,
|
||||
allowed: bool,
|
||||
prep: &PrepareView,
|
||||
p: Pos2,
|
||||
) -> Option<Hit> {
|
||||
if tool.is_some() || !allowed || !xf.rect.contains(p) {
|
||||
return None;
|
||||
}
|
||||
let selected_rect = prep
|
||||
.selected
|
||||
.as_ref()
|
||||
.and_then(|(n, wi)| form.iter().find(|f| &f.name == n)?.widgets.get(*wi).filter(|w| w.page == Some(page)))
|
||||
.map(|w| screen_rect(xf, info, page, w.rect));
|
||||
if let Some((_, left, top)) = selected_rect.and_then(|r| handles(r).into_iter().find(|(c, ..)| c.distance(p) <= 6.0)) {
|
||||
let icon = if left == top { egui::CursorIcon::ResizeNwSe } else { egui::CursorIcon::ResizeNeSw };
|
||||
return Some(Hit::new(Owner::PrepareCorner { left, top }, Layer::Handle, icon));
|
||||
}
|
||||
let (field, widget) = form.iter().enumerate().find_map(|(fi, f)| {
|
||||
f.widgets.iter().enumerate().find(|(_, w)| w.page == Some(page) && screen_rect(xf, info, page, w.rect).contains(p)).map(|(wi, _)| (fi, wi))
|
||||
})?;
|
||||
Some(Hit::new(Owner::PrepareField { field, widget }, Layer::Placed, egui::CursorIcon::Move))
|
||||
}
|
||||
|
||||
/// Pointer input on one page while preparing a form.
|
||||
pub(crate) fn page_input(cx: PageToolContext<'_>, form: &[FormField], tool: Option<FieldTool>, allowed: bool, view: &mut DocView) -> Outcome {
|
||||
let PageToolContext { ui, resp, xf, ptr, page, info, .. } = cx;
|
||||
@@ -383,33 +416,21 @@ pub(crate) fn page_input(cx: PageToolContext<'_>, form: &[FormField], tool: Opti
|
||||
prep.also.clear();
|
||||
}
|
||||
}
|
||||
// The Select tool: corner handles of the selection, then fields.
|
||||
let selected_rect = prep
|
||||
.selected
|
||||
.as_ref()
|
||||
.and_then(|(n, wi)| form.iter().find(|f| &f.name == n)?.widgets.get(*wi).filter(|w| w.page == Some(page)))
|
||||
.map(|w| screen_rect(xf, info, page, w.rect));
|
||||
let corner = selected_rect.and_then(|r| handles(r).into_iter().find(|(c, ..)| c.distance(p) <= 6.0));
|
||||
let hit = form.iter().find_map(|f| {
|
||||
f.widgets.iter().enumerate().find(|(_, w)| w.page == Some(page) && screen_rect(xf, info, page, w.rect).contains(p)).map(|(i, _)| (f, i))
|
||||
});
|
||||
if let Some((_, left, top)) = corner {
|
||||
view.cursor.claim(Layer::Handle, if left == top { egui::CursorIcon::ResizeNwSe } else { egui::CursorIcon::ResizeNeSw });
|
||||
} else if hit.is_some() {
|
||||
view.cursor.claim(Layer::Field, egui::CursorIcon::Move);
|
||||
}
|
||||
if resp.drag_started() {
|
||||
// The Select tool: what the press was on ([`hit`]), a corner handle or a field; elsewhere a
|
||||
// drag draws a selection rectangle. A press held by something drawn over the fields (a
|
||||
// comment) is not Prepare's.
|
||||
let owner = view.router.owner(page);
|
||||
let held = matches!(owner, Some(Owner::PrepareField { .. } | Owner::PrepareCorner { .. }));
|
||||
let hit = match owner {
|
||||
Some(Owner::PrepareField { field, widget }) => form.get(field).map(|f| (f, widget)),
|
||||
_ => None,
|
||||
};
|
||||
if resp.drag_started() && (held || owner.is_none()) {
|
||||
let origin = ptr.origin.unwrap_or(p);
|
||||
if let Some((_, left, top)) = selected_rect.and_then(|r| handles(r).into_iter().find(|(c, ..)| c.distance(origin) <= 6.0)) {
|
||||
if let Some(Owner::PrepareCorner { left, top }) = owner {
|
||||
prep.grab = Some((page, Grab::Corner(left, top), origin));
|
||||
out.consumed = true;
|
||||
} else if let Some((f, wi)) = form.iter().find_map(|f| {
|
||||
f.widgets
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, w)| w.page == Some(page) && screen_rect(xf, info, page, w.rect).contains(origin))
|
||||
.map(|(i, _)| (f, i))
|
||||
}) {
|
||||
} else if let Some((f, wi)) = hit {
|
||||
prep.selected = Some((f.name.clone(), wi));
|
||||
prep.grab = Some((page, Grab::Move, origin));
|
||||
out.consumed = true;
|
||||
@@ -429,7 +450,7 @@ pub(crate) fn page_input(cx: PageToolContext<'_>, form: &[FormField], tool: Opti
|
||||
out.properties = true;
|
||||
return out;
|
||||
}
|
||||
if resp.clicked() {
|
||||
if resp.clicked() && (held || owner.is_none()) {
|
||||
let extend = ui.input(|i| i.modifiers.shift || i.modifiers.command);
|
||||
match hit {
|
||||
// Shift/⌘-click adds or removes a field from the selection.
|
||||
@@ -437,14 +458,14 @@ pub(crate) fn page_input(cx: PageToolContext<'_>, form: &[FormField], tool: Opti
|
||||
prep.select((f.name.clone(), wi), extend);
|
||||
out.consumed = true;
|
||||
}
|
||||
None if corner.is_none() => {
|
||||
None if !held => {
|
||||
prep.selected = None;
|
||||
prep.also.clear();
|
||||
}
|
||||
None => out.consumed = true,
|
||||
}
|
||||
}
|
||||
out.consumed |= hit.is_some() && resp.is_pointer_button_down_on();
|
||||
out.consumed |= held;
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,31 @@ fn checkbox_pdf() -> Vec<u8> {
|
||||
])
|
||||
}
|
||||
|
||||
/// A 300×400 page with two unchecked check boxes side by side: `agree` at x 100..120 and
|
||||
/// `other` at x 120..140, both at y 200..220.
|
||||
fn two_checkboxes_pdf() -> Vec<u8> {
|
||||
pdf(&[
|
||||
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R 5 0 R] /DA (/ZaDb 0 Tf 0 g) /DR << /Font << /ZaDb 6 0 R >> >> >> >>",
|
||||
PAGES,
|
||||
"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R] >>",
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Btn /T (agree) /V /Off /AS /Off /Rect [100 200 120 220] /P 3 0 R /F 4 /MK << /CA (4) >> /DA (/ZaDb 11 Tf 0 g) >>",
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Btn /T (other) /V /Off /AS /Off /Rect [120 200 140 220] /P 3 0 R /F 4 /MK << /CA (4) >> /DA (/ZaDb 11 Tf 0 g) >>",
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /ZapfDingbats >>",
|
||||
])
|
||||
}
|
||||
|
||||
/// The check box `agree` of [`checkbox_pdf`], with a highlight drawn over it.
|
||||
fn highlighted_checkbox_pdf() -> Vec<u8> {
|
||||
pdf(&[
|
||||
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R] /DA (/ZaDb 0 Tf 0 g) /DR << /Font << /ZaDb 5 0 R >> >> >> >>",
|
||||
PAGES,
|
||||
"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 6 0 R] >>",
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Btn /T (agree) /V /Off /AS /Off /Rect [100 200 120 220] /P 3 0 R /F 4 /MK << /CA (4) >> /DA (/ZaDb 11 Tf 0 g) >>",
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /ZapfDingbats >>",
|
||||
"<< /Type /Annot /Subtype /Highlight /Rect [95 195 125 225] /QuadPoints [95 225 125 225 95 195 125 195] /C [1 1 0] /F 4 /T (Ana) >>",
|
||||
])
|
||||
}
|
||||
|
||||
/// The app with `pdf` open at `zoom` percent and its first frames run. The left panel stays
|
||||
/// open only where a test places the page relative to the window's chrome.
|
||||
fn open(pdf: Vec<u8>, zoom: &'static str, left_open: bool) -> Harness<'static, PdfCraftApp> {
|
||||
@@ -688,6 +713,40 @@ fn coalesced_click_places_a_stamp() {
|
||||
assert_eq!(stamps(&h), 1, "a click whose press and release share a frame places the stamp");
|
||||
}
|
||||
|
||||
/// The value of the field named `name` on the first view's document.
|
||||
fn field_value(h: &Harness<'_, PdfCraftApp>, name: &str) -> Vec<String> {
|
||||
let s = h.state();
|
||||
s.session.get(s.views[0].id).unwrap().form.iter().find(|f| f.name == name).unwrap().value.clone()
|
||||
}
|
||||
|
||||
/// A press belongs to the field it lands on: pressed at the right edge of `agree` and released just
|
||||
/// over the edge of `other` beside it (a click still, to egui), it ticks neither, as a button
|
||||
/// pressed and released off itself does nothing. The release used to tick `other`.
|
||||
#[test]
|
||||
fn a_press_on_one_check_box_released_over_another_ticks_neither() {
|
||||
let mut h = open(two_checkboxes_pdf(), "150", false);
|
||||
let (press, release) = (at(&h, 119.0, 210.0), at(&h, 121.0, 210.0));
|
||||
h.hover_at(press);
|
||||
h.run_steps(1);
|
||||
h.drag_at(press);
|
||||
h.run_steps(1);
|
||||
h.drop_at(release);
|
||||
h.run_steps(4);
|
||||
assert_eq!((field_value(&h, "agree"), field_value(&h, "other")), (Vec::<String>::new(), Vec::new()), "neither box is ticked");
|
||||
}
|
||||
|
||||
/// Text markup lies on the text, under a form field: a click on a check box under a highlight
|
||||
/// ticks the box, and the field's hand shows there.
|
||||
#[test]
|
||||
fn a_check_box_under_a_highlight_takes_the_click() {
|
||||
let mut h = open(highlighted_checkbox_pdf(), "150", false);
|
||||
let p = at(&h, 110.0, 210.0);
|
||||
assert_eq!(cursor_at(&mut h, p), egui::CursorIcon::PointingHand);
|
||||
mouse_click(&mut h, p);
|
||||
assert_eq!(field_value(&h, "agree"), ["Yes"], "the box is ticked");
|
||||
assert_eq!(h.state().views[0].comments.selected, None, "and the highlight is not selected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_click_ticks_a_check_box() {
|
||||
// Control for the tap below.
|
||||
|
||||
Reference in new issue
Block a user