Merge PR #2621: feat(forms): nudge the selected field with the arrow keys in Prepare a form (#2608)

# Conflicts:
#	ROADMAP.md
ROADMAP.md: union of the progress-log lines
This commit is contained in:
aiongg
2026-10-22 01:45:38 +00:00
6 changed files with 100 additions and 5 deletions
+1
View File
@@ -214,6 +214,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
- **2026-10-15 (M6, AcroForm XML text):** Plain AcroForm XML imports retain all direct text around comments and processing instructions, including CDATA and character references. Synthetic regressions verify exact values and unknown catalog data after incremental and full save/reopen. Nested-element and XFA interpretation are unchanged. Estimates unchanged.
- **2026-10-13 (M6, data exchange fidelity):** XML and XFDF exports preserve tabs and line breaks in field names and comment metadata using character references. Saved-PDF regressions cover distinct names that XML would normalize to the same attribute value, restored field values and replacement of the original comment identities. Estimates unchanged.
- **2026-10-15 (M12, XFA, #2574):** A `pageArea` with no `<occur>` repeats without limit instead of taking a subform's default of one use, so a later page area with `occur max="1"` is no longer drawn (with a copy of its fields) over every body page. Synthetic regression test; the page area's `occur min` (a once-only page of its own) is still not laid out, recorded in `form.xfa-dynamic`. Estimates unchanged.
- **2026-10-15 (community, M6, #2608):** In Prepare a form the selected field moves with the arrow keys: a point per press, ten with Shift, as the object nudge and dragging it do. Each press is one undo step ("Change field properties"), and the keys no longer turn the page while a field is selected. Estimates unchanged.
- **2026-10-13 (community, M3, #1569 review):** Pixel-only Wayland wheel, wheel-tilt and continuous input use standalone moved events, so Ctrl zoom does not remain latched after the modifier is released (#991); vendored winit patch (13), with a policy test. Estimates unchanged.
- **2026-10-15 (M14, #2383 candidate):** Device-limit requests now clamp unsupported maxima and minimum alignments before enabling the adapter's full 2D texture extent. This addresses the reported V3D request of 16 shader variables on an adapter supporting 15. Synthetic regressions fail before the clamp and pass afterward, along with the existing HiDPI case and Windows GPU-required application tests. Raspberry Pi startup remains unverified; milestone and overall estimates are unchanged.
- **2026-10-12 (M6):** XFDF field import and data-file extraction join all text nodes in a value, preserving text after XML comments and processing instructions. Synthetic regressions cover Unicode, CDATA, empty values and PDF save/reopen. Estimates unchanged.
+7
View File
@@ -2868,6 +2868,13 @@ impl PdfCraftApp {
{
object_ui::nudge_keys(ctx, &mut self.views[i], doc);
}
// A selected field moves with the arrows too (Prepare a form, #2608), before the view
// turns the page with them.
if self.is_preparing()
&& let Some(doc) = self.session.get(self.views[i].id).filter(|d| d.allows_modification())
{
prepare::nudge_keys(ctx, &mut self.views[i], doc);
}
canvas::shortcuts(&mut self.views[i], ctx, self.scroll_speed);
self.copy_key(i, ctx);
self.image_pick_keys(i, ctx);
+47 -1
View File
@@ -7,7 +7,7 @@
use egui::{Color32, CornerRadius, Pos2, Rect, Stroke, vec2};
use pdfcraft_engine::form_scripts::{CalcOp, Calculate, DATE_PRESETS, Format, TIME_PRESETS, Validate, format_value};
use pdfcraft_engine::{BorderStyle, Edit, FieldFont, FieldLook, FieldProps, FormField, FormFieldKind, NewField};
use pdfcraft_engine::{BorderStyle, Document, Edit, FieldFont, FieldLook, FieldProps, FormField, FormFieldKind, NewField};
use pdfcraft_render::DocInfo;
use crate::canvas::{DocView, PageXform};
@@ -565,6 +565,52 @@ pub(crate) fn delete_selected(view: &mut DocView) -> Option<Edit> {
Some(Edit::Batch { label, edits: names.into_iter().map(|name| Edit::DeleteField { name }).collect() })
}
/// The arrow keys move the selected field a point, Shift and an arrow ten points, as in Acrobat,
/// as the object nudge does (`object_ui::nudge_keys`) and as dragging it does (#2608). The keys
/// are consumed before the view turns the page with them; one press is one undo step.
pub(crate) fn nudge_keys(ctx: &egui::Context, view: &mut DocView, doc: &Document) {
if ctx.egui_wants_keyboard_input() || view.prepare.grab.is_some() {
return;
}
let Some((name, wi)) = view.prepare.selected.clone() else { return };
let Some(w) = doc.form.iter().find(|f| f.name == name).and_then(|f| f.widgets.get(wi)) else { return };
let Some(page) = w.page else { return };
let Some(xf) = view.page_xform(page) else { return };
use egui::{Key, Modifiers};
let mut step = egui::Vec2::ZERO;
ctx.input_mut(|i| {
for (key, direction) in [
(Key::ArrowLeft, egui::vec2(-1.0, 0.0)),
(Key::ArrowRight, egui::vec2(1.0, 0.0)),
(Key::ArrowUp, egui::vec2(0.0, -1.0)),
(Key::ArrowDown, egui::vec2(0.0, 1.0)),
] {
// Shift first: a plain key pattern also matches a Shift press.
if i.consume_key(Modifiers::SHIFT, key) {
step += direction * 10.0;
} else if i.consume_key(Modifiers::NONE, key) {
step += direction;
}
}
});
if step == egui::Vec2::ZERO {
return;
}
// A point in view space whatever the zoom and whatever way the view is turned; the page's own
// rotation turns it into the user-space move the widget's rect is stored in.
let c = xf.rect.center();
let unit = |d: egui::Vec2| {
let (a, b) = (xf.screen_to_view(c), xf.screen_to_view(c + d));
let v = egui::vec2(b.0 - a.0, b.1 - a.1);
if v.length() > 0.0 { v / v.length() } else { egui::Vec2::ZERO }
};
let delta = unit(egui::vec2(1.0, 0.0)) * step.x + unit(egui::vec2(0.0, 1.0)) * step.y;
let Ok(offset) = doc.object_move_offset(page, [f64::from(delta.x), f64::from(delta.y)]) else { return };
let r = w.rect;
let rect = [r[0] + offset[0], r[1] + offset[1], r[2] + offset[0], r[3] + offset[1]];
view.pending_edit = Some(Edit::SetFieldProps { name, props: Box::new(FieldProps { rect: Some((wi, rect)), ..Default::default() }) });
}
/// Delete removes the selected fields; Escape clears the selection.
pub(crate) fn select_all(view: &mut DocView, form: &[FormField]) {
let mut selected =
+40
View File
@@ -116,6 +116,46 @@ fn placing_moving_editing_and_deleting_a_field() {
assert_eq!((r[2] - r[0], r[3] - r[1]), (14.0, 14.0));
}
/// The arrow keys nudge the selected field a point, Shift and an arrow ten points (#2608), as they
/// do for a selected object or image and as dragging it does. One press is one undo step, the page
/// does not turn with them, and the field stays selected for the next press.
#[test]
fn arrow_keys_nudge_the_selected_field_instead_of_turning_the_page() {
let mut h = harness();
h.state_mut().execute("form.add.text");
h.run_steps(1);
// A click places a default-size text field and selects it.
let p = at(&h, 40.0, 250.0);
h.hover_at(p);
h.run_steps(1);
h.drag_at(p);
h.run_steps(1);
h.drop_at(p);
h.run_steps(4);
let id = h.state().views[0].id;
let name = h.state().views[0].prepare.selected.clone().expect("the placed field is selected").0;
let near = |a: f64, b: f64| (a - b).abs() < 0.01;
let before = rect_of(&h, &name);
h.key_press(egui::Key::ArrowRight);
h.run_steps(2);
let right = rect_of(&h, &name);
assert!(near(right[0], before[0] + 1.0) && near(right[2], before[2] + 1.0), "{before:?} → {right:?}");
assert!(near(right[2] - right[0], before[2] - before[0]), "moved, not resized");
assert_eq!(h.state().session.get(id).unwrap().can_undo(), Some("Change field properties"));
h.key_press_modifiers(egui::Modifiers::SHIFT, egui::Key::ArrowUp);
h.run_steps(2);
let up = rect_of(&h, &name);
assert!(near(up[1], right[1] + 10.0) && near(up[3], right[3] + 10.0), "{right:?} → {up:?}");
h.key_press(egui::Key::ArrowDown);
h.run_steps(2);
h.key_press(egui::Key::ArrowLeft);
h.run_steps(2);
let back = rect_of(&h, &name);
assert!(near(back[0], up[0] - 1.0) && near(back[1], up[1] - 1.0), "{up:?} → {back:?}");
assert!(h.state().views[0].prepare.selected.is_some(), "it stays selected for the next nudge");
assert_eq!(h.state().views[0].current, 0, "the arrows moved the field, not the page");
}
#[test]
fn format_validate_and_calculate_tabs() {
use pdfcraft_engine::form_scripts::{Calculate, Format, Validate};
+3 -2
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (column select #740, multi-object move #844, Shift-⌘-G #810 fixed) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (the selected field nudges with the arrow keys in Prepare a form, #2608) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
How PdfCraft's tools, panels, shortcuts and feel compare with Acrobat Pro's current interface (the
2023+ "new Acrobat": All tools pane on the left, quick-action toolbar, Home view). Behaviour is
@@ -52,7 +52,7 @@ No dump of Acrobat's menu tree has been diffed against the registry yet (gap 24)
| Rulers, grids, snap to grid, guides | yes | no | planned |
| Cursor coordinates, line weights | yes | no | planned |
| Measuring snap to paths, endpoints, midpoints, intersections | yes | yes | shipped |
| Nudge selected objects with arrow keys (Shift for larger steps) | yes | yes in Edit text: a point, Shift ten points, for selected objects, images and an open paragraph's box (#1520) | shipped |
| Nudge selected objects with arrow keys (Shift for larger steps) | yes | yes in Edit text: a point, Shift ten points, for selected objects, images and an open paragraph's box (#1520); and in Prepare a form, for the selected field (#2608) | shipped |
| Single-key tool accelerators (H, V, Z, …) | yes, opt-in | no | planned |
| Context menus on pages, thumbnails, comments, fields | yes | thumbnails, comments, fields | partial |
| Comment pop-ups as movable windows, connector lines | yes | inline in the panel | planned |
@@ -81,5 +81,6 @@ No dump of Acrobat's menu tree has been diffed against the registry yet (gap 24)
| Date | Change | Summary |
|---|---|---|
| 2026-10-15 | minor | A selected field nudges with the arrow keys in Prepare a form: a point, Shift ten points, one undo step per press (#2608) |
| 2026-10-11 | minor | Column select shipped (#740, PR #765); several objects move together on one page (#844, PR #907); Shift-⌘-G goes back (#810, PR #835) |
| 2026-10-10 | major | Created from the command registry, automation tools, the View area of the checklist and open UI issues |
+2 -2
View File
@@ -4740,8 +4740,8 @@ milestone = "M6"
status = "shipped"
commands = ["form.prepare"]
tools = ["form_add_field", "form_set_props", "form_delete_field"]
evidence = ["crates/ui-egui/tests/prepare.rs::placing_moving_editing_and_deleting_a_field", "crates/ui-egui/src/prepare.rs::corner_drags_stretch_freely_and_keep_proportions_with_shift", "crates/ui-egui/tests/prepare.rs::deleting_several_selected_fields_and_detecting_fields_from_the_panel", "crates/engine/src/tests.rs::preparing_a_form_adds_renames_and_deletes_fields", "crates/ui-egui/tests/ui_group/bulk_fields.rs::shared_properties_preserve_mixed_values_and_undo_the_whole_change", "crates/ui-egui/tests/ui_group/bulk_fields.rs::select_all_in_prepare_a_form_selects_only_the_current_pages_fields", "crates/ui-egui/tests/ui_group/bulk_fields.rs::dragging_an_empty_page_area_selects_fields_without_moving_them"]
notes = "fields shown with blue fill and name tags; select, move, resize by corners (Shift keeps the field's proportions, #2199), Delete (every selected field, one undo step); field tools return to Select after one field. Shared General, Appearance and applicable Options edits are atomic and opt-in; Ctrl/Command+A selects the current page, rectangle and panel selection are supported. Names, positions, choices/defaults and script tabs remain individual GUI edits"
evidence = ["crates/ui-egui/tests/prepare.rs::placing_moving_editing_and_deleting_a_field", "crates/ui-egui/tests/prepare.rs::arrow_keys_nudge_the_selected_field_instead_of_turning_the_page", "crates/ui-egui/src/prepare.rs::corner_drags_stretch_freely_and_keep_proportions_with_shift", "crates/ui-egui/tests/prepare.rs::deleting_several_selected_fields_and_detecting_fields_from_the_panel", "crates/engine/src/tests.rs::preparing_a_form_adds_renames_and_deletes_fields", "crates/ui-egui/tests/ui_group/bulk_fields.rs::shared_properties_preserve_mixed_values_and_undo_the_whole_change", "crates/ui-egui/tests/ui_group/bulk_fields.rs::select_all_in_prepare_a_form_selects_only_the_current_pages_fields", "crates/ui-egui/tests/ui_group/bulk_fields.rs::dragging_an_empty_page_area_selects_fields_without_moving_them"]
notes = "fields shown with blue fill and name tags; select, move (drag, or the arrow keys: a point, Shift ten points, #2608), resize by corners (Shift keeps the field's proportions, #2199), Delete (every selected field, one undo step); field tools return to Select after one field. Shared General, Appearance and applicable Options edits are atomic and opt-in; Ctrl/Command+A selects the current page, rectangle and panel selection are supported. Names, positions, choices/defaults and script tabs remain individual GUI edits"
[[feature]]
id = "form.add-text-field"