mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-29 12:38:58 -06:00
Keep added text selectable over form fields (#1528)
* Keep added text selectable over form fields Generated with Codex for @FredApps. * Fix the content.rs test harness merge: harness_bytes and harness_with_step_dt share one builder Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
b7976ef40e
commit
10ad9b46e5
@@ -2892,7 +2892,10 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
(o.consumed || field_tool.is_some(), false)
|
||||
} else {
|
||||
use crate::forms_ui::FieldPointer;
|
||||
let field = if tool == QuickTool::Select && !comments::select_grabs(ui, &pcx, view) {
|
||||
let field = if tool == QuickTool::Select
|
||||
&& !(editing_content && can_modify && crate::content_ui::select_grabs(ui, &xf, i, info, &added, view))
|
||||
&& !comments::select_grabs(ui, &pcx, view)
|
||||
{
|
||||
crate::forms_ui::page_input(ui, &resp, &xf, i, info, &form, can_fill, view)
|
||||
} else {
|
||||
FieldPointer::Away
|
||||
|
||||
@@ -279,6 +279,22 @@ fn dragged(r: Rect, grab: Grab, d: egui::Vec2, keep_aspect: bool) -> Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// Added text and images keep pointer ownership over a form field while editing content.
|
||||
pub(crate) fn select_grabs(ui: &egui::Ui, xf: &PageXform, page: usize, info: &DocInfo, added: &[Added], view: &DocView) -> bool {
|
||||
let cv = &view.content;
|
||||
if cv.grab.is_some_and(|(grab_page, _, _)| grab_page == page) {
|
||||
return true;
|
||||
}
|
||||
let Some(p) = ui.input(|i| i.pointer.hover_pos().or(i.pointer.interact_pos())).filter(|p| xf.rect.contains(*p)) else {
|
||||
return false;
|
||||
};
|
||||
on_page(added, page).into_iter().any(|(index, item)| {
|
||||
let rect = screen_rect(xf, info, page, item.content.rect());
|
||||
rect.expand(2.0).contains(p)
|
||||
|| cv.selected == Some((page, index)) && handles(rect).into_iter().any(|(corner, _, _)| corner.distance(p) <= 6.0)
|
||||
})
|
||||
}
|
||||
|
||||
/// Pointer input on one page in Edit mode (`adding_text` with the Text tool). `true` when it
|
||||
/// used the input.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
@@ -7,11 +7,19 @@ use egui_kittest::kittest::Queryable;
|
||||
use pdfcraft_ui_egui::{PdfCraftApp, QuickTool};
|
||||
|
||||
fn harness() -> Harness<'static, PdfCraftApp> {
|
||||
harness_bytes(include_bytes!("data/form.pdf").to_vec())
|
||||
harness_with_step_dt(0.25)
|
||||
}
|
||||
|
||||
fn harness_with_step_dt(step_dt: f32) -> Harness<'static, PdfCraftApp> {
|
||||
harness_bytes_with_step_dt(include_bytes!("data/form.pdf").to_vec(), step_dt)
|
||||
}
|
||||
|
||||
fn harness_bytes(bytes: Vec<u8>) -> Harness<'static, PdfCraftApp> {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
|
||||
harness_bytes_with_step_dt(bytes, 0.25)
|
||||
}
|
||||
|
||||
fn harness_bytes_with_step_dt(bytes: Vec<u8>, step_dt: f32) -> Harness<'static, PdfCraftApp> {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(step_dt).build_eframe(move |_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_bytes("form.pdf", None, bytes.clone()).unwrap();
|
||||
@@ -236,6 +244,116 @@ fn typing_thai_text_keeps_marks_and_commits_to_page() {
|
||||
assert_eq!(texts(&h), ["น้ำ กิ่ ผู้ใช้ Hello"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finished_text_over_form_field_keeps_selection_and_drag_ownership() {
|
||||
let mut h = harness();
|
||||
assert!(h.state_mut().execute("edit.text"));
|
||||
h.run_steps(2);
|
||||
let p = at(&h, 60.0, 202.0);
|
||||
click(&mut h, p);
|
||||
h.event(egui::Event::Text("Reviewed".into()));
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Done adding text").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(texts(&h), ["Reviewed"]);
|
||||
let rect = added(&h)[0].content.rect();
|
||||
let mid_x = ((rect[0] + rect[2]) / 2.0) as f32;
|
||||
let mid_y = ((rect[1] + rect[3]) / 2.0) as f32;
|
||||
assert!((20.0..280.0).contains(&mid_x) && (180.0..200.0).contains(&mid_y), "fixture text must overlap city: {rect:?}");
|
||||
let center = at(&h, mid_x, 400.0 - mid_y);
|
||||
click(&mut h, center);
|
||||
assert!(h.state().views[0].forms.focus.is_none(), "selecting finished text must not focus the field underneath");
|
||||
assert_eq!(h.state().views[0].content.selected, Some((0, 0)));
|
||||
h.run_steps(60);
|
||||
let start = at(&h, mid_x, 400.0 - mid_y);
|
||||
let end = at(&h, mid_x, 400.0 - mid_y - 80.0);
|
||||
h.drag_at(start);
|
||||
h.run_steps(1);
|
||||
for step in 1..=40 {
|
||||
h.hover_at(start + (end - start) * (step as f32 / 40.0));
|
||||
h.run_steps(1);
|
||||
}
|
||||
h.drop_at(end);
|
||||
h.run_steps(4);
|
||||
let moved = added(&h)[0].content.rect();
|
||||
for (actual, expected) in moved.into_iter().zip([rect[0], rect[1] + 80.0, rect[2], rect[3] + 80.0]) {
|
||||
assert!((actual - expected).abs() < 1.0, "text should move upward: {rect:?} -> {moved:?}");
|
||||
}
|
||||
assert!(h.state().views[0].forms.focus.is_none());
|
||||
let bare = at(&h, 270.0, 210.0);
|
||||
click(&mut h, bare);
|
||||
assert_eq!(h.state().views[0].forms.focus.as_ref().map(|f| f.name.as_str()), Some("city"));
|
||||
}
|
||||
|
||||
fn text_over_city() -> Harness<'static, PdfCraftApp> {
|
||||
let mut h = harness_with_step_dt(1.0 / 60.0);
|
||||
assert!(h.state_mut().execute("edit.text"));
|
||||
h.run_steps(2);
|
||||
let p = at(&h, 60.0, 202.0);
|
||||
click(&mut h, p);
|
||||
h.event(egui::Event::Text("Reviewed".into()));
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Done adding text").click();
|
||||
h.run_steps(3);
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finished_text_over_field_keeps_move_and_resize_cursors() {
|
||||
let mut h = text_over_city();
|
||||
let rect = added(&h)[0].content.rect();
|
||||
let center = at(&h, ((rect[0] + rect[2]) / 2.0) as f32, (400.0 - (rect[1] + rect[3]) / 2.0) as f32);
|
||||
h.hover_at(center);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::Move);
|
||||
// The upper-right handle overlaps the city field; it retains its resize gesture.
|
||||
let corner = at(&h, rect[2] as f32, (400.0 - rect[3]) as f32);
|
||||
h.hover_at(corner);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::ResizeNeSw);
|
||||
let end = corner + egui::vec2(30.0, -20.0);
|
||||
h.drag_at(corner);
|
||||
h.run_steps(1);
|
||||
for step in 1..=20 {
|
||||
h.hover_at(corner + (end - corner) * (step as f32 / 20.0));
|
||||
h.run_steps(1);
|
||||
}
|
||||
h.drop_at(end);
|
||||
h.run_steps(4);
|
||||
let resized = added(&h)[0].content.rect();
|
||||
assert!(resized[2] > rect[2] + 1.0 && resized[3] > rect[3] + 1.0, "resize must retain ownership: {rect:?} -> {resized:?}");
|
||||
assert!(h.state().views[0].forms.focus.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finished_text_over_field_still_opens_its_editor() {
|
||||
let mut h = text_over_city();
|
||||
let rect = added(&h)[0].content.rect();
|
||||
let center = at(&h, ((rect[0] + rect[2]) / 2.0) as f32, (400.0 - (rect[1] + rect[3]) / 2.0) as f32);
|
||||
h.run_steps(60);
|
||||
h.hover_at(center);
|
||||
h.run_steps(1);
|
||||
for _ in 0..2 {
|
||||
for pressed in [true, false] {
|
||||
h.event(egui::Event::PointerButton { pos: center, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE });
|
||||
}
|
||||
h.run_steps(1);
|
||||
}
|
||||
h.run_steps(2);
|
||||
assert!(h.state().views[0].content.draft.as_ref().is_some_and(|d| d.index == Some(0)), "double click edits the added text");
|
||||
assert!(h.state().views[0].forms.focus.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_edit_panel_leaves_form_filling_available() {
|
||||
let mut h = text_over_city();
|
||||
h.state_mut().set_option("left", "closed").unwrap();
|
||||
h.run_steps(3);
|
||||
let p = at(&h, 100.0, 210.0);
|
||||
click(&mut h, p);
|
||||
assert_eq!(h.state().views[0].forms.focus.as_ref().map(|f| f.name.as_str()), Some("city"));
|
||||
}
|
||||
|
||||
/// The screen rectangle of an added item's display-space box (y up, from the page's bottom).
|
||||
fn item_on_screen(h: &Harness<'static, PdfCraftApp>, r: [f64; 4]) -> egui::Rect {
|
||||
let s = h.state();
|
||||
|
||||
Reference in New Issue
Block a user