mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 12:32:52 +00:00
refactor(ui): tell Edit text, Add text, pictures and added content whether the press is theirs
The Edit text group still asked the router for itself and passed answers from one module to the next: `others_own_pointer`, `text_used`, `over_source`, `source_blocked` and the pictures' `allow_interaction` decided who acted, so what the pointer happened to be over at the release, or an earlier module's answer, could take a click from the module its press was on. - Paragraphs (`ParagraphCx`), pictures (`ImageCx`) and added content (`ContentCx`) are told the press (`PagePress`) and Edit text's gate (its input blocked by a dialog, the palette, another window or tab, as before), and act on their own presses; the paragraph outline comes from the top hit under the pointer (`hovered`), hidden while the objects selected together or added content being moved hold the page. The flags go, and the three modules no longer read the router. - Add text acts on the paragraph its press was on, or the one under the pointer; its click on a paragraph refuses a blocked draft and finishes the one being typed in the paragraph's own act. - Clicks act where they were pressed (the user's ruling): a paragraph opens though the button came up just past its box, added text is selected, and opened for typing, by the item its press was on, and Add text's new box starts where it was pressed (a press beside a paragraph released inside it starts a box; it placed nothing). - The picture's grips and rotate knob lose their egui widgets: a drag takes what the press was on. A press past a page's edge is the page's that has something under it there (the knob of a picture filling its page), so the knob still turns the picture there. - Edit text's right-click goes to what lies on top under it, as every tool's does (ruling C): a picture is selected for its menu through `edit_text_ui::picture_context`, and a comment over a paragraph or a picture gets its own menu. The page view's own picture scan goes. - #5900's part of that scan (a right-click on the picture being cropped, its box or its grips keeps crop mode, over a picture on top of it too) is the crop hit's: `CropPart::Cut`, the picture outside the box kept, shown or cut away, joins the grips and the box, so a right-click there keeps crop mode. Its menu check stays for now (the menu commit takes it). Test: `a_right_click_on_the_part_a_crop_cuts_away_keeps_crop_mode`, seen failing without `Cut`. - With the Edit panel open, the tools that don't pick added content up still select added text by its own test at the pointer, as before: a press-time hit would take the press from the stamps and the other tools that place on any press. - `main`'s press-handler check guards nothing now, and goes; rule (e) (`ROUTER_READERS`) keeps the modules from reading the router again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
10 files changed
+454
-342
No files matched your search
@@ -1604,8 +1604,6 @@ fn draw_pages(
|
||||
prefs,
|
||||
preparing,
|
||||
selection_wheel,
|
||||
ppp,
|
||||
rects,
|
||||
single_paging,
|
||||
tool,
|
||||
unobstructed,
|
||||
@@ -1674,54 +1672,9 @@ fn draw_pages(
|
||||
view.links.context = None;
|
||||
// Set again below by the page right-clicked: a right-click off the pages (between them,
|
||||
// beside one) anchors no page's menu, which then acts on the current page. It deselects the
|
||||
// comment too (below), as one on a page's empty part does. A selected Prepare field keeps
|
||||
// its menu, off the pages as on them.
|
||||
// comment, and Edit text's picture, too (below), as one on a page's empty part does. A
|
||||
// selected Prepare field keeps its menu, off the pages as on them.
|
||||
view.comments.context_at = None;
|
||||
let mut over_image = false;
|
||||
if tool == QuickTool::EditText {
|
||||
let pointer_pos = ptr.pos;
|
||||
if let Some(p) = pointer_pos {
|
||||
for &page_idx in visible_pages {
|
||||
let r = snap_to_pixels(rects[page_idx].translate(origin.to_vec2()), ppp);
|
||||
if !r.intersects(visible) {
|
||||
continue;
|
||||
}
|
||||
let xf =
|
||||
PageXform { rect: r, rot: view.rotation, pw: info.pages[page_idx].width.max(1.0), ph: info.pages[page_idx].height.max(1.0) };
|
||||
let images = cached_edit_images(view, doc, page_idx).to_vec();
|
||||
// Crop controls own the click even when another image overlaps them.
|
||||
if view.image_selection.as_ref().is_some_and(|s| {
|
||||
s.page == page_idx
|
||||
&& s.crop
|
||||
.as_ref()
|
||||
.zip(images.get(s.index))
|
||||
.is_some_and(|(crop, img)| crop_contains_point(img, crop, &xf, info, page_idx, p))
|
||||
}) {
|
||||
over_image = true;
|
||||
break;
|
||||
}
|
||||
for (idx, img) in images.iter().enumerate().rev() {
|
||||
let obb = ImageObb::from_image(img, &xf, info, page_idx);
|
||||
let b = image_screen_rect(img, &xf, info, page_idx);
|
||||
let selected_crop = view.image_selection.as_ref().is_some_and(|s| s.page == page_idx && s.index == idx && s.crop.is_some());
|
||||
let hit = if selected_crop { crate::edit_text_ui::quad_contains(obb.corners(), p) } else { obb.contains(b, p) };
|
||||
if hit {
|
||||
over_image = true;
|
||||
if view.image_selection.as_ref().is_none_or(|s| s.page != page_idx || s.index != idx) {
|
||||
view.image_selection = Some(crate::edit_text_ui::ImageSelection::at(page_idx, idx));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if over_image {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !over_image && tool == QuickTool::EditText {
|
||||
view.image_selection = None;
|
||||
}
|
||||
}
|
||||
let mut frame = PageFrame {
|
||||
env: *env,
|
||||
@@ -1759,9 +1712,13 @@ fn draw_pages(
|
||||
right_clicked,
|
||||
..
|
||||
} = frame;
|
||||
// A right-click no page took deselects the comment, so the page's menu opens, not the comment's.
|
||||
// A right-click no page took deselects the comment, so the page's menu opens, not the comment's,
|
||||
// and with Edit text the picture (one on a page went to that page, `page_input`).
|
||||
if resp.secondary_clicked() && !right_clicked {
|
||||
view.comments.selected = None;
|
||||
if tool == QuickTool::EditText {
|
||||
view.image_selection = None;
|
||||
}
|
||||
}
|
||||
let PageOut {
|
||||
mut canvas_action,
|
||||
|
||||
@@ -37,8 +37,9 @@ pub(crate) struct PageToolContext<'a> {
|
||||
|
||||
/// The tools' input on one page. The hits at the press point decide who holds the press, and the
|
||||
/// modules are told what the press is to this page (`PagePress`): their own, nobody's (the active
|
||||
/// tool's: `tool_input`, and the tools still called here), or another page's. The Edit text group
|
||||
/// still asks the router, until it is told too. Text selection runs after, on what nobody took.
|
||||
/// tool's: `tool_input`, and the tools still called here), or another page's. Edit text's group
|
||||
/// paints from what lies under the pointer too (`hovered`). Text selection runs after, on what
|
||||
/// nobody took.
|
||||
pub(super) fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut PageFrame<'_>, slot: &PageSlot) -> PageInput {
|
||||
let PageEnv {
|
||||
added,
|
||||
@@ -90,24 +91,31 @@ pub(super) fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut
|
||||
images: &images,
|
||||
clip: ui.clip_rect(),
|
||||
};
|
||||
if let Some(o) = ptr.pressed().filter(|o| xf.rect.contains(*o)) {
|
||||
if let Some(o) = ptr.pressed() {
|
||||
// A press with Shift or ⌘ in Edit text is the objects', whatever lies under it
|
||||
// (`object_ui::modified_hit`): the page's objects are read for it alone.
|
||||
let adds = tool == QuickTool::EditText
|
||||
&& can_modify
|
||||
&& xf.rect.contains(o)
|
||||
&& crate::object_ui::adds(ptr.modifiers)
|
||||
&& !resp.context_menu_opened()
|
||||
&& !egui::Popup::is_any_open(ui.ctx());
|
||||
let objects = adds.then(|| view.objects.inventory(doc, i));
|
||||
let at = hits.at(view, o, objects.as_ref());
|
||||
view.router.press(i, at, tool);
|
||||
// A press is the page's it lies on, or the one with something under it past its edge (the
|
||||
// selected picture's rotate knob and grips).
|
||||
if xf.rect.contains(o) || !at.is_empty() {
|
||||
view.router.press(i, at, tool);
|
||||
}
|
||||
}
|
||||
// What this page has to do with the press: its owner's, the active tool's, or another page's.
|
||||
let press = view.router.press_for(i);
|
||||
// What lies under the pointer before the tools act, on the frames that need it: for a click's
|
||||
// release, a right-click's tool checks, and Fill & Sign's preview.
|
||||
let wants_under = resp.clicked() || resp.secondary_clicked() || matches!(tool, QuickTool::Fill(_)) && !ptr.any_down;
|
||||
// release, a right-click's tool checks, Fill & Sign's preview, and Edit text's hover outlines.
|
||||
let editing = matches!(tool, QuickTool::AddText | QuickTool::EditText) && can_modify;
|
||||
let wants_under = editing || resp.clicked() || resp.secondary_clicked() || matches!(tool, QuickTool::Fill(_)) && !ptr.any_down;
|
||||
let under = pointer.filter(|p| wants_under && xf.rect.contains(*p)).map(|p| hits.at(view, p, None)).unwrap_or_default();
|
||||
let hovered = top(under.iter().cloned(), tool);
|
||||
let released_over = match press {
|
||||
PagePress::Mine(owner) if resp.clicked() => under.iter().any(|h| h.owner == owner),
|
||||
_ => false,
|
||||
@@ -117,7 +125,17 @@ pub(super) fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut
|
||||
// released, the right-click's own frame included, so the button is what tells. A touch's long
|
||||
// press, which egui reports as a right-click with the finger still down, opens its menu.
|
||||
let press_held = ptr.primary_down && !resp.long_touched();
|
||||
let context = frame.right_click_on(xf.rect).and_then(|p| right_click(&hits, &under, view, p, press_held));
|
||||
let right = frame.right_click_on(xf.rect);
|
||||
let context = right.and_then(|p| right_click(&hits, &under, view, p, press_held));
|
||||
// Edit text: a right-click on a picture selects it for its menu, and one on anything else on the
|
||||
// page unselects it.
|
||||
if right.is_some() && tool == QuickTool::EditText && can_modify && !press_held {
|
||||
let picture = match context {
|
||||
Some(Owner::Picture { index, part }) if part != crate::page_router::PicturePart::Crop(crate::page_router::CropPart::Page) => Some(index),
|
||||
_ => None,
|
||||
};
|
||||
crate::edit_text_ui::picture_context(ui.ctx(), i, picture, view);
|
||||
}
|
||||
// A mark already placed takes its own press (comments, below), as does one being moved on this
|
||||
// page until its move ends; with no button down the Fill & Sign tool's preview hides where a
|
||||
// press would be taken (a mark, its handles, a Prepare field).
|
||||
@@ -143,58 +161,38 @@ pub(super) fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut
|
||||
};
|
||||
crate::redact_ui::page_input(cx, over_text, press == PagePress::Free, view)
|
||||
};
|
||||
// Edit text's group: the objects selected together, added content, the paragraphs and the
|
||||
// pictures, each told what the press is to this page. Edit text ignores input while a dialog,
|
||||
// the palette, another window or another tab has it (`gate`), as before.
|
||||
let gate = !(tool == QuickTool::EditText && view.objects.input_blocked());
|
||||
let on_objects = tool == QuickTool::EditText && can_modify && crate::object_ui::page_input(cx, press, view);
|
||||
// Add text acts on the page's own paragraphs over them, and on the one its press was on until
|
||||
// the button is up: a drag that leaves the box still ends, and no new box takes it over.
|
||||
let over_source = tool == QuickTool::AddText
|
||||
&& (matches!(view.router.owner(i), Some(crate::page_router::Owner::Paragraph { .. }))
|
||||
|| pointer.is_some_and(|p| {
|
||||
!crate::content_ui::hits_added(&xf, info, i, added, p) && crate::edit_text_ui::over_paragraph(&xf, info, i, &lines, p)
|
||||
}));
|
||||
let source_blocked = over_source && view.content.hold_blocked();
|
||||
if over_source && resp.clicked() && !source_blocked {
|
||||
if let Some(edit) = view.content.finish_draft(added) {
|
||||
view.pending_edit = Some(edit);
|
||||
}
|
||||
view.content.selected = None;
|
||||
}
|
||||
let on_content = !on_objects && editing_content && can_modify && {
|
||||
source_blocked
|
||||
|| crate::content_ui::page_input(
|
||||
cx,
|
||||
added,
|
||||
tool == QuickTool::AddText && !over_source,
|
||||
matches!(tool, QuickTool::AddText | QuickTool::EditText),
|
||||
text_style,
|
||||
view,
|
||||
)
|
||||
|| tool == QuickTool::AddText && !over_source
|
||||
let picks_up = matches!(tool, QuickTool::Select | QuickTool::AddText | QuickTool::EditText);
|
||||
let ccx = crate::content_ui::ContentCx {
|
||||
added,
|
||||
press,
|
||||
gate,
|
||||
picks_up,
|
||||
adding_text: tool == QuickTool::AddText,
|
||||
edit_on_click: matches!(tool, QuickTool::AddText | QuickTool::EditText),
|
||||
style: text_style,
|
||||
};
|
||||
// Added content owns its gesture; its source text must not queue a second edit.
|
||||
// Edit text & images boxes every paragraph and image whenever the tool is on. Only the
|
||||
// input waits while selected objects or added content own the pointer, or while page
|
||||
// input is blocked (another tab, a dialog, the window unfocused): the boxes stay drawn
|
||||
// instead of vanishing until the pointer comes back.
|
||||
let others_own_pointer = on_objects || on_content;
|
||||
let on_edit_text = (tool == QuickTool::EditText || over_source) && can_modify && {
|
||||
// Editable text takes precedence over background/overlapping artwork. Both
|
||||
// inventories are painted every frame, even when text owns the pointer.
|
||||
let text_used = crate::edit_text_ui::page_input(cx, &lines, view, !others_own_pointer);
|
||||
let image_cx = crate::edit_text_ui::ImageCx {
|
||||
ui,
|
||||
resp,
|
||||
xf: &xf,
|
||||
ptr,
|
||||
page: i,
|
||||
info,
|
||||
press,
|
||||
allow_interaction: !others_own_pointer && (matches!(view.router.owner(i), Some(crate::page_router::Owner::Picture { .. })) || !text_used),
|
||||
};
|
||||
let on_content = editing_content && can_modify && crate::content_ui::page_input(cx, ccx, view);
|
||||
// Add text acts on the page's own paragraphs: the one its press was on, until the button is up,
|
||||
// and the one under the pointer. Edit text boxes every paragraph and picture.
|
||||
let paragraph = |o: Option<Owner>| matches!(o, Some(Owner::Paragraph { .. }));
|
||||
let source = tool == QuickTool::AddText && (paragraph(press.owner()) || paragraph(hovered.as_ref().map(|h| h.owner)));
|
||||
let on_edit_text = (tool == QuickTool::EditText || source) && can_modify && {
|
||||
let adding = (tool == QuickTool::AddText).then_some(added.as_slice());
|
||||
let pcx = crate::edit_text_ui::ParagraphCx { press, gate, hover: hovered.as_ref(), adding };
|
||||
let text_used = crate::edit_text_ui::page_input(cx, &lines, pcx, view);
|
||||
let image_cx = crate::edit_text_ui::ImageCx { ui, resp, xf: &xf, ptr, page: i, info, press, gate };
|
||||
let image_used = crate::edit_text_ui::image_input(&image_cx, &images, view);
|
||||
!others_own_pointer && (text_used || image_used)
|
||||
text_used || image_used
|
||||
};
|
||||
if tool == QuickTool::EditText {
|
||||
if others_own_pointer {
|
||||
// A press the objects or added content took leaves the picture unselected; so does one
|
||||
// while Edit text's input is blocked. Otherwise added content follows the picture selected.
|
||||
if on_objects || on_content {
|
||||
if ptr.pressed().is_some() || resp.clicked() || resp.secondary_clicked() {
|
||||
view.image_selection = None;
|
||||
}
|
||||
@@ -340,7 +338,8 @@ fn tool_input(
|
||||
|
||||
/// A right-click at `p`: the page's menu opens there, and it goes to what lies on top there
|
||||
/// ([`PageHits::context_at`]), which the menu then acts on. A comment there is selected, and none
|
||||
/// elsewhere; a Prepare field is selected; links take theirs from what this returns.
|
||||
/// elsewhere; a Prepare field is selected; links and Edit text's pictures take theirs from what this
|
||||
/// returns.
|
||||
/// `under`: the hits at `p` before the tools act. `press_held`: the primary button is held, and the
|
||||
/// comment and Prepare selections its press acts on are left alone until its release; links still
|
||||
/// take theirs, as before.
|
||||
@@ -351,13 +350,10 @@ fn right_click(hits: &PageHits<'_>, under: &[crate::page_router::Hit], view: &mu
|
||||
if press_held {
|
||||
return context;
|
||||
}
|
||||
// Add text takes every right-click on a page it can change, Edit text those on its own
|
||||
// paragraphs and pictures (the objects selected together, and crop mode's picture, among them),
|
||||
// the Link tool those on a link, and a Prepare field tool the whole page, as before: comments
|
||||
// see none of them.
|
||||
// Add text takes every right-click on a page it can change, the Link tool those on a link, and a
|
||||
// Prepare field tool the whole page, as before: comments see none of them.
|
||||
let tool_takes_it = match hits.tool {
|
||||
QuickTool::AddText => hits.can_modify,
|
||||
QuickTool::EditText => under.iter().any(|h| matches!(h.owner, Owner::Paragraph { .. } | Owner::Picture { .. } | Owner::Objects { .. })),
|
||||
QuickTool::Link => hits.can_modify && matches!(context, Some(Owner::Link { .. })),
|
||||
_ => hits.field_tool.is_some(),
|
||||
};
|
||||
|
||||
@@ -570,21 +570,13 @@ const ROUTER_READS: [&str; 6] =
|
||||
["router.press_for(", "router.owner(", "router.keeps_drag(", "PageRouter::press_for(", "PageRouter::owner(", "PageRouter::keeps_drag("];
|
||||
|
||||
/// The functions that read who holds the press: (file, function, why). The page view asks once per
|
||||
/// page and tells each module what the press is to its page (`PagePress`); the Edit text group, the
|
||||
/// text layer and link following still ask, until they are told too (PR 4b), and an unused entry
|
||||
/// fails the test.
|
||||
const ROUTER_READERS: [(&str, &str, &str); 7] = [
|
||||
(
|
||||
"canvas/page_input.rs",
|
||||
"page_tool_input",
|
||||
"the page view: what the press is to each page (`press_for`); Add text's paragraph and the pictures until 4b",
|
||||
),
|
||||
/// page and tells each module what the press is to its page (`PagePress`); the text layer and link
|
||||
/// following still ask, until they are told too (PR 4b), and an unused entry fails the test.
|
||||
const ROUTER_READERS: [(&str, &str, &str); 4] = [
|
||||
("canvas/page_input.rs", "page_tool_input", "the page view: what the press is to each page (`press_for`)"),
|
||||
("canvas/page_input.rs", "update_selection_on_press", "the text layer selects no text under a press held by what keeps its drag (4b)"),
|
||||
("canvas/page_input.rs", "finish_text_pointer_gesture", "a click held by what keeps its drag leaves the text selection alone (4b)"),
|
||||
("canvas/page_input.rs", "page_overlays", "a click follows the link that holds its press (4b)"),
|
||||
("tools/content_ui.rs", "page_input", "added text and pictures, until they are told (4b)"),
|
||||
("tools/edit_text_ui.rs", "page_input", "Edit text's paragraphs, until they are told (4b)"),
|
||||
("tools/edit_text_ui/image.rs", "image_input", "Edit text's pictures, until they are told (4b)"),
|
||||
];
|
||||
|
||||
/// What ignores the press the pages hold until the button is up.
|
||||
@@ -594,8 +586,8 @@ const PRESS_IGNORES: [&str; 1] = ["press_cancelled = true"];
|
||||
const PRESS_IGNORERS: [(&str, &str, &str); 1] =
|
||||
[("canvas/page_input.rs", "cancel_on_escape", "Escape abandons the gesture the press drives, whichever tool's")];
|
||||
|
||||
/// The module hit tests, called by path: a press handler calls none of them (HIT_TESTERS says who
|
||||
/// may).
|
||||
/// The module hit tests, called by path (HIT_TESTERS says who may): an entry no page module calls
|
||||
/// any more fails the test.
|
||||
const PAGE_HIT_CALLS: [&str; 13] = [
|
||||
"link_ui::hit(",
|
||||
"prepare::hit(",
|
||||
@@ -612,17 +604,6 @@ const PAGE_HIT_CALLS: [&str; 13] = [
|
||||
"content_ui::hit(",
|
||||
];
|
||||
|
||||
/// Whether a press handler's `code` acts on the owner the router captured, and calls no hit test
|
||||
/// of its own to choose one.
|
||||
fn routed_press_handler(code: &str) -> bool {
|
||||
let code = code.lines().filter(|line| !line.trim_start().starts_with("//")).collect::<Vec<_>>().join("\n");
|
||||
code.contains("router.owner(")
|
||||
&& !PAGE_HIT_CALLS.iter().any(|call| code.contains(call))
|
||||
&& !uses(&code, "hit(")
|
||||
&& !uses(&code, "body_hit(")
|
||||
&& !uses(&code, "handle_hit(")
|
||||
}
|
||||
|
||||
/// Page modules act on a press only when the router says it is theirs. The hits at the press
|
||||
/// point decide its owner once, in one place, and a `Hit` comes only from a module's hit test,
|
||||
/// so what the cursor promised is what the press does. Where each module tested the press
|
||||
@@ -694,16 +675,6 @@ fn page_modules_act_on_a_press_only_through_the_router() {
|
||||
`DocView::cancel_pointer_gestures` instead.",
|
||||
"no longer ignores a press: remove it from PRESS_IGNORERS.",
|
||||
));
|
||||
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
|
||||
for (file, function) in
|
||||
[("tools/content_ui.rs", "page_input"), ("tools/edit_text_ui.rs", "page_input"), ("tools/edit_text_ui/image.rs", "image_input")]
|
||||
{
|
||||
let source = std::fs::read_to_string(src.join(file)).expect("page input module");
|
||||
let body = source.lines().skip_while(|line| declares(line) != Some(function)).take_while(|line| *line != "}").collect::<Vec<_>>().join("\n");
|
||||
if !routed_press_handler(&body) {
|
||||
problems.push(format!("src/{file}: {function} must consume the captured router owner without repeating a hit test"));
|
||||
}
|
||||
}
|
||||
// A hit test is a function named `hit` or `…_hit`: one answer to "what of mine is here?",
|
||||
// which gives the module's cursor, its tip and its press.
|
||||
problems.extend(misuses_in(
|
||||
@@ -774,13 +745,3 @@ fn the_page_guards_read_hit_tests_split_calls_and_aliases() {
|
||||
let aliases: Vec<(String, String)> = [("hit", "corner_under"), ("picture_hit", "under")].map(|(a, b)| (a.to_owned(), b.to_owned())).to_vec();
|
||||
assert_eq!(aliased_hit_tests(source), aliases);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_page_press_guard_rejects_missing_owners_and_second_hit_tests() {
|
||||
assert!(routed_press_handler("let owner = view.router.owner(page);"));
|
||||
assert!(!routed_press_handler("// view.router.owner(page)\nlet owner = None;"));
|
||||
assert!(!routed_press_handler("let owner = view.router.owner(page);\nlet again = cx.hit(origin);"));
|
||||
for call in PAGE_HIT_CALLS {
|
||||
assert!(!routed_press_handler(&format!("let owner = view.router.owner(page);\n{call}origin);")), "{call}");
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,9 @@ pub(crate) enum CropPart {
|
||||
Grip(usize),
|
||||
/// The box kept: a drag moves it over the picture.
|
||||
Keep,
|
||||
/// The picture outside the box kept, shown or cut away: a press there moves nothing, and a
|
||||
/// right-click is the crop's, as on the box and its grips (#5835).
|
||||
Cut,
|
||||
/// The rest of the page, its Done and Cancel controls among it: a press there is crop mode's,
|
||||
/// and moves nothing.
|
||||
Page,
|
||||
@@ -146,7 +149,8 @@ pub(crate) enum PagePress {
|
||||
/// up (`Elsewhere` instead when Escape came in the press's own frame; page input skips a
|
||||
/// cancelled press either way).
|
||||
Free,
|
||||
/// The press began on another page, or off the pages (between them, or beside one).
|
||||
/// The press began on another page, or off the pages (between them, or beside one) where no
|
||||
/// page has anything under it.
|
||||
Elsewhere,
|
||||
}
|
||||
|
||||
@@ -183,8 +187,9 @@ impl PageRouter {
|
||||
self.began = Some(None);
|
||||
}
|
||||
|
||||
/// A press on `page`: `hits` at the press point decide who it belongs to. A press nobody's
|
||||
/// hit claims is held by nobody (the page's text, or a tool that acts anywhere on the page).
|
||||
/// A press on `page`, or past its edge where `hits` are under it (the selected picture's rotate
|
||||
/// knob): `hits` at the press point decide who it belongs to. A press nobody's hit claims is held
|
||||
/// by nobody (the page's text, or a tool that acts anywhere on the page).
|
||||
/// Only a press just begun ([`Self::begin`]) is claimed, by the first page that does: pages
|
||||
/// that touch (the gaps hidden) can share a row of pixels, and a press there is one page's.
|
||||
pub(crate) fn press(&mut self, page: usize, hits: impl IntoIterator<Item = Hit>, tool: QuickTool) {
|
||||
|
||||
@@ -11,7 +11,7 @@ use pdfcraft_render::DocInfo;
|
||||
|
||||
use crate::canvas::{DocView, PageToolContext, PageXform};
|
||||
use crate::page_cursor::Layer;
|
||||
use crate::page_router::{Hit, Owner};
|
||||
use crate::page_router::{Hit, Owner, PagePress};
|
||||
use crate::theme::Tokens;
|
||||
use crate::widgets;
|
||||
|
||||
@@ -108,6 +108,8 @@ pub struct ContentView {
|
||||
/// Select the item an add creates (the page's item count before it).
|
||||
pub select_added: Option<(usize, usize)>,
|
||||
grab: Option<(usize, Grab, Pos2)>,
|
||||
/// Where the press nobody holds began, on which page: Add text's click starts its new box there.
|
||||
pressed_at: Option<(usize, Pos2)>,
|
||||
undrawable: UndrawableMemo,
|
||||
layout: Option<(AddedText, f32, f64, [f64; 4])>,
|
||||
}
|
||||
@@ -116,6 +118,12 @@ impl ContentView {
|
||||
/// Forget an in-progress pointer grab for content editing.
|
||||
pub(crate) fn cancel_pointer_gesture(&mut self) {
|
||||
self.grab = None;
|
||||
self.pressed_at = None;
|
||||
}
|
||||
|
||||
/// An added item is being moved or resized on `page`.
|
||||
pub(crate) fn moving_on(&self, page: usize) -> bool {
|
||||
self.grab.is_some_and(|(p, ..)| p == page)
|
||||
}
|
||||
|
||||
pub(crate) fn finish_draft(&mut self, added: &[Added]) -> Option<Edit> {
|
||||
@@ -372,17 +380,33 @@ pub(crate) fn hit(xf: &PageXform, info: &DocInfo, page: usize, added: &[Added],
|
||||
Some(Hit::new(Owner::Added { index: *index, corner: None }, Layer::Placed, egui::CursorIcon::Move))
|
||||
}
|
||||
|
||||
/// Pointer input on one page in Edit mode (`adding_text` with the Text tool). `true` when it
|
||||
/// used the input.
|
||||
pub(crate) fn page_input(
|
||||
cx: PageToolContext<'_>,
|
||||
added: &[Added],
|
||||
adding_text: bool,
|
||||
edit_on_click: bool,
|
||||
style: &AddedText,
|
||||
view: &mut DocView,
|
||||
) -> bool {
|
||||
/// What added content is told on one page.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct ContentCx<'a> {
|
||||
pub added: &'a [Added],
|
||||
/// What the press the pages hold is to this page. With the Edit panel open, a press on added
|
||||
/// text is its own with every tool ([`hit`]).
|
||||
pub press: PagePress,
|
||||
/// Edit text's input is open: no dialog, palette, other window or other tab has it.
|
||||
pub gate: bool,
|
||||
/// The tool picks added content up (Select, Add text, Edit text): a drag moves it or resizes it.
|
||||
pub picks_up: bool,
|
||||
/// The Add text tool: a click nobody holds starts a new box where it was pressed.
|
||||
pub adding_text: bool,
|
||||
/// A click on added text opens it for typing (Add text, Edit text); otherwise a double-click does.
|
||||
pub edit_on_click: bool,
|
||||
/// The style of new text.
|
||||
pub style: &'a AddedText,
|
||||
}
|
||||
|
||||
/// Pointer input on one page in Edit mode, on the presses that are added content's ([`hit`]) and,
|
||||
/// with Add text, the clicks nobody holds. `true` when it used the input.
|
||||
pub(crate) fn page_input(cx: PageToolContext<'_>, ccx: ContentCx<'_>, view: &mut DocView) -> bool {
|
||||
let PageToolContext { resp, xf, ptr, page, info, .. } = cx;
|
||||
let ContentCx { added, press, gate, picks_up, adding_text, edit_on_click, style } = ccx;
|
||||
if !gate {
|
||||
return false;
|
||||
}
|
||||
let mut consumed = false;
|
||||
let pointer = ptr.pos;
|
||||
let cv = &mut view.content;
|
||||
@@ -412,19 +436,28 @@ pub(crate) fn page_input(
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
// Where a press nobody holds begins on this page: Add text's click starts its box there.
|
||||
if let Some(o) = ptr.pressed().filter(|_| press != PagePress::Elsewhere) {
|
||||
cv.pressed_at = (press == PagePress::Free).then_some((page, o));
|
||||
}
|
||||
let Some(p) = pointer.filter(|p| xf.rect.contains(*p)) else { return consumed };
|
||||
let items: Vec<(usize, &Added)> = on_page(added, page).into_iter().filter(|(_, a)| matches!(a.content, AddedContent::Text(_))).collect();
|
||||
let hit = items.iter().rev().find(|(_, a)| screen_rect(xf, info, page, a.content.rect()).expand(2.0).contains(p)).map(|(i, a)| (*i, *a));
|
||||
let selected_rect = cv
|
||||
.selected
|
||||
.filter(|s| s.0 == page)
|
||||
.and_then(|(_, si)| items.iter().find(|(i, _)| *i == si))
|
||||
.map(|(_, a)| screen_rect(xf, info, page, a.content.rect()));
|
||||
let corner = selected_rect.and_then(|r| handles(r).into_iter().find(|(c, ..)| c.distance(p) <= 6.0));
|
||||
if adding_text && hit.is_none() {
|
||||
// What the press was on ([`hit`]): an item, or a corner of the selected one.
|
||||
let owned = match press {
|
||||
PagePress::Mine(Owner::Added { index, corner }) => Some((index, corner)),
|
||||
_ => None,
|
||||
};
|
||||
// The tools that don't pick added content up have no hit on it: with the Edit panel open their
|
||||
// clicks on added text still select it and their presses there are its, by this test at the
|
||||
// pointer, as before.
|
||||
let under = (!picks_up)
|
||||
.then(|| items.iter().rev().find(|(_, a)| screen_rect(xf, info, page, a.content.rect()).expand(2.0).contains(p)).map(|(i, _)| *i))
|
||||
.flatten();
|
||||
let target = if picks_up { owned } else { under.map(|i| (i, None)) };
|
||||
if adding_text {
|
||||
view.cursor.claim(Layer::ToolArea, egui::CursorIcon::Text);
|
||||
// A new box where nothing drawn over the page holds the press.
|
||||
if resp.clicked() && view.router.owner(page).is_none() {
|
||||
if resp.clicked() && press == PagePress::Free {
|
||||
// Text that can't be drawn stays where it is being typed (#125).
|
||||
if cv.hold_blocked() {
|
||||
return true;
|
||||
@@ -434,7 +467,9 @@ pub(crate) fn page_input(
|
||||
let typed = cv.draft.take().and_then(|d| finish(cv, d, added));
|
||||
// The Format panel styles the new text, not the box just kept.
|
||||
(cv.selected, cv.select_added) = (None, None);
|
||||
let at = to_display(xf, info, page, p);
|
||||
// Where the click was pressed, as every owner acts where its press was.
|
||||
let start = cv.pressed_at.take().filter(|(pg, _)| *pg == page).map_or(p, |(_, o)| o);
|
||||
let at = to_display(xf, info, page, start);
|
||||
let mut style = style.clone();
|
||||
style.text.clear();
|
||||
let page_width = info.pages.get(page).map_or(f64::INFINITY, |p| p.width as f64);
|
||||
@@ -445,29 +480,31 @@ pub(crate) fn page_input(
|
||||
if typed.is_some() {
|
||||
view.pending_edit = typed;
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
// A drag on what the press was on ([`hit`]): a corner resizes the selected item, the item
|
||||
// itself moves.
|
||||
// A drag on what the press was on: a corner resizes the selected item, the item itself moves,
|
||||
// with the tools that pick added content up.
|
||||
if resp.drag_started() {
|
||||
let origin = ptr.origin.unwrap_or(p);
|
||||
match view.router.owner(page) {
|
||||
Some(Owner::Added { corner: Some((left, top)), .. }) => {
|
||||
match owned.filter(|_| picks_up) {
|
||||
Some((_, Some((left, top)))) => {
|
||||
cv.grab = Some((page, Grab::Corner(left, top), origin));
|
||||
consumed = true;
|
||||
}
|
||||
Some(Owner::Added { index, corner: None }) => {
|
||||
Some((index, None)) => {
|
||||
cv.selected = Some((page, index));
|
||||
cv.grab = Some((page, Grab::Move, origin));
|
||||
consumed = true;
|
||||
}
|
||||
_ => {}
|
||||
None => {}
|
||||
}
|
||||
return consumed;
|
||||
return consumed || owned.is_some();
|
||||
}
|
||||
if (resp.double_clicked() || edit_on_click && resp.clicked() && corner.is_none())
|
||||
&& let Some((i, a)) = hit
|
||||
// A double-click, or with `edit_on_click` a click, opens the text the press was on for typing.
|
||||
if (resp.double_clicked() || edit_on_click && resp.clicked())
|
||||
&& let Some((i, None)) = target
|
||||
&& let Some((_, a)) = items.iter().find(|(index, _)| *index == i)
|
||||
&& let AddedContent::Text(t) = &a.content
|
||||
{
|
||||
if cv.hold_blocked() {
|
||||
@@ -493,17 +530,21 @@ pub(crate) fn page_input(
|
||||
consumed = true;
|
||||
return consumed;
|
||||
}
|
||||
// A click selects the item its press was on, keeps the selected one when on a corner of it, and
|
||||
// on anything else on this page (but the objects selected together, which take the selection
|
||||
// themselves) leaves it unselected.
|
||||
if resp.clicked() {
|
||||
match hit {
|
||||
Some((i, _)) => {
|
||||
match (target, press) {
|
||||
(Some((i, None)), _) => {
|
||||
cv.selected = Some((page, i));
|
||||
consumed = true;
|
||||
}
|
||||
None if corner.is_none() => cv.selected = None,
|
||||
None => consumed = true,
|
||||
(Some((_, Some(_))), _) => consumed = true,
|
||||
(None, PagePress::Elsewhere | PagePress::Mine(Owner::Objects { .. })) => {}
|
||||
(None, _) => cv.selected = None,
|
||||
}
|
||||
}
|
||||
consumed || hit.is_some() && resp.is_pointer_button_down_on()
|
||||
consumed || owned.is_some() || under.is_some() && resp.is_pointer_button_down_on()
|
||||
}
|
||||
|
||||
/// Outlines of added items (hover and selection with handles) and the box being dragged, the
|
||||
|
||||
@@ -14,7 +14,7 @@ use pdfcraft_render::DocInfo;
|
||||
use crate::canvas::{DocView, PageToolContext, PageXform};
|
||||
use crate::page_cursor::Layer;
|
||||
use crate::page_pointer::PagePointer;
|
||||
use crate::page_router::{Hit, Owner, ParagraphPart};
|
||||
use crate::page_router::{Hit, Owner, PagePress, ParagraphPart};
|
||||
|
||||
mod background;
|
||||
mod image;
|
||||
@@ -24,7 +24,7 @@ pub(crate) use background::TextBackground;
|
||||
pub use image::{CropMode, DragTarget, ImageAction, ImageDrag, ImageHandle, ImageSelection, image_menu_id};
|
||||
pub(crate) use image::{
|
||||
ImageCx, Matrix, body_hit as picture_hit, crop_controls_contain, crop_hit, handle_hit as picture_handle_hit, image_context_menu, image_input,
|
||||
picture_keys, quad_contains, user_to_screen,
|
||||
picture_context, picture_keys, quad_contains, user_to_screen,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use image::{rotate_knob, turn_degrees, turn_degrees_delta};
|
||||
@@ -659,10 +659,26 @@ fn open_editor(cx: PageToolContext<'_>, lines: &[pdfcraft_engine::TextBlock], bo
|
||||
});
|
||||
}
|
||||
|
||||
/// Lines and their screen boxes for a page; hover outlines, click opens the editor. Returns
|
||||
/// `true` when the pointer was used.
|
||||
pub(crate) fn page_input(cx: PageToolContext<'_>, lines: &[pdfcraft_engine::TextBlock], view: &mut DocView, allow_interaction: bool) -> bool {
|
||||
/// What Edit text's paragraphs are told on one page.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct ParagraphCx<'a> {
|
||||
/// What the press the pages hold is to this page.
|
||||
pub press: PagePress,
|
||||
/// Edit text's input is open: no dialog, palette, other window or other tab has it.
|
||||
pub gate: bool,
|
||||
/// What a press under the pointer would take (the top hit there): the paragraph outlined.
|
||||
pub hover: Option<&'a Hit>,
|
||||
/// With Add text, the page's added content: a click on a paragraph finishes the text being
|
||||
/// typed before it opens the paragraph.
|
||||
pub adding: Option<&'a [pdfcraft_engine::Added]>,
|
||||
}
|
||||
|
||||
/// Lines and their screen boxes for a page: the paragraph under the pointer is outlined, and the
|
||||
/// press that is a paragraph's (`press`) moves it, rewraps it or, a click, opens it in the editor.
|
||||
/// Returns `true` when the pointer was used.
|
||||
pub(crate) fn page_input(cx: PageToolContext<'_>, lines: &[pdfcraft_engine::TextBlock], pcx: ParagraphCx<'_>, view: &mut DocView) -> bool {
|
||||
let PageToolContext { ui, resp, xf, ptr, page, doc, .. } = cx;
|
||||
let ParagraphCx { press, gate, hover, adding } = pcx;
|
||||
let info = &doc.info;
|
||||
if view.block_drag.is_some_and(|d| d.generation != doc.edit_generation()) {
|
||||
view.block_drag = None;
|
||||
@@ -679,7 +695,7 @@ pub(crate) fn page_input(cx: PageToolContext<'_>, lines: &[pdfcraft_engine::Text
|
||||
}
|
||||
// A box a switching click asked for opens now, once the edit queued with that click has
|
||||
// applied and `lines`/`boxes` are the document's new inventory (#2022).
|
||||
if allow_interaction
|
||||
if gate
|
||||
&& active.is_none()
|
||||
&& let Some((open_page, key)) = view.pending_text_open.clone()
|
||||
&& open_page == page
|
||||
@@ -691,7 +707,7 @@ pub(crate) fn page_input(cx: PageToolContext<'_>, lines: &[pdfcraft_engine::Text
|
||||
None => view.pending_text_open = None,
|
||||
}
|
||||
}
|
||||
if !allow_interaction {
|
||||
if !gate {
|
||||
return false;
|
||||
}
|
||||
let upright = has_edges(info, page);
|
||||
@@ -750,8 +766,8 @@ pub(crate) fn page_input(cx: PageToolContext<'_>, lines: &[pdfcraft_engine::Text
|
||||
// What it grabs is what the press was on ([`hit`]), not what is under the pointer now: egui
|
||||
// only calls it a drag once the pointer has moved a few pixels (or been held a moment), and a
|
||||
// quick flick has left the edge by then.
|
||||
let owner = match view.router.owner(page) {
|
||||
Some(Owner::Paragraph { block, part }) => Some((block, part)),
|
||||
let owner = match press {
|
||||
PagePress::Mine(Owner::Paragraph { block, part }) => Some((block, part)),
|
||||
_ => None,
|
||||
};
|
||||
if resp.drag_started()
|
||||
@@ -767,18 +783,35 @@ pub(crate) fn page_input(cx: PageToolContext<'_>, lines: &[pdfcraft_engine::Text
|
||||
view.block_drag = Some(BlockDrag { page, block, generation: doc.edit_generation(), start: o, grip });
|
||||
return true;
|
||||
}
|
||||
let Some(p) = ptr.within(xf.rect) else { return false };
|
||||
// The paragraph under the pointer: outlined, with its width handles when closed.
|
||||
let Some((hovered, _)) = grab_at(&boxes, p, upright) else { return false };
|
||||
if active != Some(hovered) {
|
||||
painter.rect_stroke(boxes[hovered], CornerRadius::same(2), Stroke::new(1.5, ACCENT), egui::StrokeKind::Outside);
|
||||
}
|
||||
if upright && active.is_none() {
|
||||
draw_handles(boxes[hovered]);
|
||||
// The paragraph under the pointer, where a press would take it: outlined, with its width
|
||||
// handles when closed. Not while the objects selected together or added content being moved
|
||||
// hold the page.
|
||||
let hovered = match hover.map(|h| h.owner) {
|
||||
Some(Owner::Paragraph { block, .. }) => Some(block),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(b) = hovered.filter(|_| !view.objects.holds(page) && !view.content.moving_on(page)).and_then(|h| boxes.get(h).map(|b| (h, *b))) {
|
||||
if active != Some(b.0) {
|
||||
painter.rect_stroke(b.1, CornerRadius::same(2), Stroke::new(1.5, ACCENT), egui::StrokeKind::Outside);
|
||||
}
|
||||
if upright && active.is_none() {
|
||||
draw_handles(b.1);
|
||||
}
|
||||
}
|
||||
// A click opens the paragraph the press was on.
|
||||
let Some((hit, _)) = owner else { return true };
|
||||
let Some((hit, _)) = owner else { return hovered.is_some() };
|
||||
if resp.clicked() {
|
||||
// Add text: text that can't be drawn stays where it is being typed (#125), and text being
|
||||
// typed is finished first.
|
||||
if let Some(added) = adding {
|
||||
if view.content.hold_blocked() {
|
||||
return true;
|
||||
}
|
||||
if let Some(edit) = view.content.finish_draft(added) {
|
||||
view.pending_edit = Some(edit);
|
||||
}
|
||||
view.content.selected = None;
|
||||
}
|
||||
view.image_selection = None;
|
||||
// A click that moves the editor to another paragraph applies the open one first, so its
|
||||
// typed text is not dropped with the switched-away editor (#2022). A click on the open
|
||||
|
||||
@@ -15,7 +15,8 @@ pub(crate) struct ImageCx<'a> {
|
||||
pub info: &'a DocInfo,
|
||||
/// What the press the pages hold is to this page.
|
||||
pub press: PagePress,
|
||||
pub allow_interaction: bool,
|
||||
/// Edit text's input is open: no dialog, palette, other window or other tab has it.
|
||||
pub gate: bool,
|
||||
}
|
||||
|
||||
/// The 8 resize handles on an image (4 corners + 4 edge centers).
|
||||
@@ -281,7 +282,7 @@ pub(crate) fn crop_controls_contain(
|
||||
/// (the pointer is the crop's while it lasts; its keys are read by [`picture_keys`]).
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn crop_input(cx: &ImageCx<'_>, view: &mut DocView, i: usize, image: &pdfcraft_engine::PageImage) -> bool {
|
||||
let ImageCx { ui, resp, xf, ptr, page, info, press, allow_interaction } = *cx;
|
||||
let ImageCx { ui, resp, xf, ptr, page, info, press, gate } = *cx;
|
||||
let painter = ui.painter();
|
||||
let Some(mode) = view.image_selection.as_mut().and_then(|s| s.crop.as_mut()) else { return false };
|
||||
let hover_pos = ptr.pos;
|
||||
@@ -292,7 +293,7 @@ fn crop_input(cx: &ImageCx<'_>, view: &mut DocView, i: usize, image: &pdfcraft_e
|
||||
let keep_screen = |keep: [f64; 4]| xf.user_rect(info, page, keep.map(|v| v as f32)).intersect(b);
|
||||
|
||||
// A drag takes what its press was on ([`crop_hit`]).
|
||||
if allow_interaction
|
||||
if gate
|
||||
&& resp.drag_started()
|
||||
&& let Some(o) = ptr.origin
|
||||
&& let PagePress::Mine(Owner::Picture { part: PicturePart::Crop(part), .. }) = press
|
||||
@@ -300,7 +301,7 @@ fn crop_input(cx: &ImageCx<'_>, view: &mut DocView, i: usize, image: &pdfcraft_e
|
||||
let grip = match part {
|
||||
CropPart::Grip(g) => Some(g),
|
||||
CropPart::Keep => Some(8),
|
||||
CropPart::Page => None,
|
||||
CropPart::Cut | CropPart::Page => None,
|
||||
};
|
||||
if let Some(g) = grip {
|
||||
mode.drag = Some((g, o, mode.keep));
|
||||
@@ -406,10 +407,10 @@ fn crop_input(cx: &ImageCx<'_>, view: &mut DocView, i: usize, image: &pdfcraft_e
|
||||
let done_resp = ui.interact(done, done_id, egui::Sense::click());
|
||||
let cancel_resp = ui.interact(cancel, cancel_id, egui::Sense::click());
|
||||
let reset_resp = reset.map(|r| ui.interact(r, reset_id, egui::Sense::click()));
|
||||
done_resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, allow_interaction, tl!("Done")));
|
||||
cancel_resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, allow_interaction, tl!("Cancel")));
|
||||
done_resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, gate, tl!("Done")));
|
||||
cancel_resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, gate, tl!("Cancel")));
|
||||
if let Some(response) = &reset_resp {
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, allow_interaction, tl!("Remove Crop")));
|
||||
response.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, gate, tl!("Remove Crop")));
|
||||
}
|
||||
|
||||
let t = crate::theme::Tokens::get(ui.ctx());
|
||||
@@ -448,9 +449,9 @@ fn crop_input(cx: &ImageCx<'_>, view: &mut DocView, i: usize, image: &pdfcraft_e
|
||||
view.cursor.claim(Layer::Drag, GRIPS.get(grip).map_or(egui::CursorIcon::Grabbing, |g| g.cursor_icon()));
|
||||
}
|
||||
|
||||
let done_clicked = allow_interaction && (done_resp.clicked() || (resp.clicked() && hover_pos.is_some_and(|p| done.contains(p))));
|
||||
let cancel_clicked = allow_interaction && (cancel_resp.clicked() || (resp.clicked() && hover_pos.is_some_and(|p| cancel.contains(p))));
|
||||
let reset_clicked = allow_interaction
|
||||
let done_clicked = gate && (done_resp.clicked() || (resp.clicked() && hover_pos.is_some_and(|p| done.contains(p))));
|
||||
let cancel_clicked = gate && (cancel_resp.clicked() || (resp.clicked() && hover_pos.is_some_and(|p| cancel.contains(p))));
|
||||
let reset_clicked = gate
|
||||
&& reset
|
||||
.is_some_and(|r| reset_resp.as_ref().is_some_and(|res| res.clicked()) || (resp.clicked() && hover_pos.is_some_and(|p| r.contains(p))));
|
||||
|
||||
@@ -575,9 +576,9 @@ pub(crate) fn handle_hit(
|
||||
/// The picture being cropped on `page`, under `p`: crop mode holds every press on its page, the
|
||||
/// controls beside its frame included, so the paragraphs under it and the empty page's marquee
|
||||
/// never take a crop button's press before the picture does. A grip of the box kept (within
|
||||
/// [`CROP_GRIP_REACH`]), the box itself, or the rest of the page; and the controls past the page's
|
||||
/// edge, where they show the hand. `None` when no picture on `page` is being cropped, or `p` is off
|
||||
/// the page and its controls.
|
||||
/// [`CROP_GRIP_REACH`]), the box itself, the picture outside it (its frame shown or its whole frame),
|
||||
/// or the rest of the page; and the controls past the page's edge, where they show the hand. `None`
|
||||
/// when no picture on `page` is being cropped, or `p` is off the page and its controls.
|
||||
pub(crate) fn crop_hit(xf: &PageXform, page: usize, info: &DocInfo, images: &[pdfcraft_engine::PageImage], view: &DocView, p: Pos2) -> Option<Hit> {
|
||||
let s = view.image_selection.as_ref().filter(|s| s.page == page)?;
|
||||
let mode = s.crop.as_ref()?;
|
||||
@@ -589,14 +590,19 @@ pub(crate) fn crop_hit(xf: &PageXform, page: usize, info: &DocInfo, images: &[pd
|
||||
if control {
|
||||
return crop(CropPart::Page, egui::CursorIcon::PointingHand);
|
||||
}
|
||||
let Some((_, k)) = images.get(s.index).and_then(|image| crop_frames(xf, info, page, image, mode.keep)) else {
|
||||
return crop(CropPart::Page, egui::CursorIcon::Default);
|
||||
};
|
||||
if let Some(g) = crop_handles(k).iter().position(|h| h.distance(p) < CROP_GRIP_REACH) {
|
||||
return crop(CropPart::Grip(g), GRIPS.get(g).map_or(egui::CursorIcon::Default, |grip| grip.cursor_icon()));
|
||||
let Some(image) = images.get(s.index) else { return crop(CropPart::Page, egui::CursorIcon::Default) };
|
||||
if let Some((_, k)) = crop_frames(xf, info, page, image, mode.keep) {
|
||||
if let Some(g) = crop_handles(k).iter().position(|h| h.distance(p) < CROP_GRIP_REACH) {
|
||||
return crop(CropPart::Grip(g), GRIPS.get(g).map_or(egui::CursorIcon::Default, |grip| grip.cursor_icon()));
|
||||
}
|
||||
if k.contains(p) {
|
||||
return crop(CropPart::Keep, egui::CursorIcon::Grab);
|
||||
}
|
||||
}
|
||||
if k.contains(p) {
|
||||
return crop(CropPart::Keep, egui::CursorIcon::Grab);
|
||||
let shown = ImageObb::from_image(image, xf, info, page);
|
||||
let whole = ImageObb::from_matrix(image.matrix, xf, info, page);
|
||||
if quad_contains(shown.corners(), p) || quad_contains(whole.corners(), p) {
|
||||
return crop(CropPart::Cut, egui::CursorIcon::Default);
|
||||
}
|
||||
crop(CropPart::Page, egui::CursorIcon::Default)
|
||||
}
|
||||
@@ -677,10 +683,12 @@ pub fn image_menu_id() -> egui::Id {
|
||||
egui::Id::unique("image-menu")
|
||||
}
|
||||
|
||||
/// Images on a page: select, move, resize, right-click. Returns `true` when the pointer was used.
|
||||
/// Images on a page: the press that is a picture's (`press`, [`body_hit`] and [`handle_hit`])
|
||||
/// selects it, a drag of the selected one moves, resizes or turns it. Returns `true` when the
|
||||
/// pointer was used.
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub(crate) fn image_input(cx: &ImageCx<'_>, images: &[pdfcraft_engine::PageImage], view: &mut DocView) -> bool {
|
||||
let ImageCx { ui, resp, xf, ptr, page, info, allow_interaction, .. } = *cx;
|
||||
let ImageCx { ui, resp, xf, ptr, page, info, press, gate } = *cx;
|
||||
let boxes: Vec<Rect> = images.iter().map(|im| crate::canvas::image_screen_rect(im, xf, info, page)).collect();
|
||||
let painter = ui.painter();
|
||||
let hover_pos = ptr.pos;
|
||||
@@ -776,56 +784,22 @@ pub(crate) fn image_input(cx: &ImageCx<'_>, images: &[pdfcraft_engine::PageImage
|
||||
}
|
||||
}
|
||||
|
||||
let mut handle_drag = None;
|
||||
for (h, h_pos) in obb.handles() {
|
||||
let handle_rect = Rect::from_center_size(h_pos, egui::vec2(18.0, 18.0));
|
||||
let handle_resp = ui.interact(handle_rect, ui.scope_id().with("img_handle").with(h), egui::Sense::drag());
|
||||
if allow_interaction && handle_resp.drag_started() {
|
||||
handle_drag = Some(h);
|
||||
}
|
||||
}
|
||||
let rot_id = ui.scope_id().with("img_handle").with(8);
|
||||
let rot_rect = Rect::from_center_size(shown_knob, egui::vec2(12.0, 12.0));
|
||||
let rot_resp = ui.interact(rot_rect, rot_id, egui::Sense::drag());
|
||||
let rot_drag = allow_interaction && rot_resp.drag_started();
|
||||
|
||||
if allow_interaction && let Some(p) = hover_pos {
|
||||
if shown_knob.distance(p) < 8.0 {
|
||||
view.cursor.claim(Layer::Handle, egui::CursorIcon::Grab);
|
||||
} else if let Some((h, _)) = obb.handles().iter().find(|(_, pos)| pos.distance(p) < 8.0) {
|
||||
let icon = obb.handle_cursor(*h);
|
||||
view.cursor.claim(Layer::Handle, icon);
|
||||
}
|
||||
}
|
||||
|
||||
let origin = ptr.origin;
|
||||
if allow_interaction {
|
||||
if let Some(h) = handle_drag {
|
||||
if let Some(s) = view.image_selection.as_mut() {
|
||||
let o = origin.unwrap_or_else(|| obb.handle_pos(h));
|
||||
s.drag = Some(ImageDrag {
|
||||
start: o,
|
||||
target: DragTarget::Handle(h),
|
||||
initial_rect: b,
|
||||
initial_matrix: resize_frame(&obb, xf, info, page).0,
|
||||
});
|
||||
}
|
||||
} else if rot_drag {
|
||||
if let Some(s) = view.image_selection.as_mut() {
|
||||
let o = origin.unwrap_or(shown_knob);
|
||||
// A drag of the selected picture takes what its press was on ([`body_hit`], [`handle_hit`]):
|
||||
// the rotate knob turns it, a grip resizes it, the picture itself moves.
|
||||
if gate
|
||||
&& resp.drag_started()
|
||||
&& let Some(o) = ptr.origin
|
||||
&& let PagePress::Mine(Owner::Picture { index, part }) = press
|
||||
&& index == i
|
||||
&& let Some(s) = view.image_selection.as_mut()
|
||||
{
|
||||
match part {
|
||||
PicturePart::Knob => {
|
||||
s.drag = Some(ImageDrag { start: o, target: DragTarget::Rotate, initial_rect: b, initial_matrix: image.matrix });
|
||||
s.turning = true;
|
||||
}
|
||||
} else if resp.drag_started()
|
||||
&& let Some(o) = origin
|
||||
{
|
||||
if shown_knob.distance(o) < 8.0 {
|
||||
if let Some(s) = view.image_selection.as_mut() {
|
||||
s.drag = Some(ImageDrag { start: o, target: DragTarget::Rotate, initial_rect: b, initial_matrix: image.matrix });
|
||||
s.turning = true;
|
||||
}
|
||||
} else if let Some((h, _)) = obb.handles().iter().find(|(_, pos)| pos.distance(o) < 8.0) {
|
||||
if let Some(s) = view.image_selection.as_mut() {
|
||||
PicturePart::Corner(grip) => {
|
||||
if let Some((h, _)) = obb.handles().get(grip) {
|
||||
s.drag = Some(ImageDrag {
|
||||
start: o,
|
||||
target: DragTarget::Handle(*h),
|
||||
@@ -833,11 +807,11 @@ pub(crate) fn image_input(cx: &ImageCx<'_>, images: &[pdfcraft_engine::PageImage
|
||||
initial_matrix: resize_frame(&obb, xf, info, page).0,
|
||||
});
|
||||
}
|
||||
} else if (image_contains(i, o) || quad_contains(obb.corners(), o))
|
||||
&& let Some(s) = view.image_selection.as_mut()
|
||||
{
|
||||
}
|
||||
PicturePart::Body => {
|
||||
s.drag = Some(ImageDrag { start: o, target: DragTarget::Move, initial_rect: b, initial_matrix: image.matrix });
|
||||
}
|
||||
PicturePart::Crop(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1002,50 +976,36 @@ pub(crate) fn image_input(cx: &ImageCx<'_>, images: &[pdfcraft_engine::PageImage
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if !allow_interaction {
|
||||
return false;
|
||||
}
|
||||
let menu_key = image_menu_id();
|
||||
let Some(p) = hover_pos.filter(|p| xf.rect.contains(*p)) else { return false };
|
||||
let Some(hit) = boxes.iter().enumerate().rposition(|(i, b)| {
|
||||
b.is_positive() && images.get(i).is_some_and(|img| ImageObb::from_image(img, xf, info, page).contains(*b, p)) && image_contains(i, p)
|
||||
}) else {
|
||||
if resp.secondary_clicked() || resp.clicked() {
|
||||
view.image_selection = None;
|
||||
ui.ctx().data_mut(|d| d.insert_temp(menu_key, false));
|
||||
}
|
||||
return false;
|
||||
// A click selects the picture its press was on; its cursor is the hit's.
|
||||
let owner = match press {
|
||||
PagePress::Mine(Owner::Picture { index, .. }) => Some(index),
|
||||
_ => None,
|
||||
};
|
||||
let Some(hit_img) = images.get(hit) else { return false };
|
||||
let hit_obb = ImageObb::from_image(hit_img, xf, info, page);
|
||||
if selected != Some(hit) {
|
||||
if hit_obb.is_rotated {
|
||||
let pts = hit_obb.corners();
|
||||
for k in 0..4 {
|
||||
painter.line_segment([pts[k], pts[(k + 1) % 4]], Stroke::new(1.5, ACCENT.gamma_multiply(0.7)));
|
||||
}
|
||||
} else if let Some(b) = boxes.get(hit).copied() {
|
||||
painter.rect_stroke(b, CornerRadius::ZERO, Stroke::new(1.5, ACCENT.gamma_multiply(0.7)), egui::StrokeKind::Outside);
|
||||
}
|
||||
}
|
||||
let grip = hit_obb.handles().into_iter().find(|(_, point)| point.distance(p) < 10.0);
|
||||
let cursor = if selected == Some(hit) {
|
||||
grip.map_or(egui::CursorIcon::Grab, |(handle, _)| hit_obb.handle_cursor(handle))
|
||||
} else {
|
||||
egui::CursorIcon::PointingHand
|
||||
};
|
||||
view.cursor.claim(Layer::Text, cursor);
|
||||
let clicked = match view.router.owner(page) {
|
||||
Some(Owner::Picture { index, .. }) if resp.clicked() => Some(index),
|
||||
_ => (resp.clicked() || resp.secondary_clicked()).then_some(hit),
|
||||
};
|
||||
if let Some(index) = clicked {
|
||||
if gate
|
||||
&& resp.clicked()
|
||||
&& let Some(index) = owner
|
||||
{
|
||||
view.image_selection = Some(ImageSelection::at(page, index));
|
||||
}
|
||||
if resp.secondary_clicked() {
|
||||
ui.ctx().data_mut(|d| d.insert_temp(menu_key, true));
|
||||
owner.is_some()
|
||||
}
|
||||
|
||||
/// A right-click on `page` that went to its picture `index` (on it, a grip of it, or the box kept
|
||||
/// while it is cropped) selects it, keeping it as it is if it was selected, and opens the picture's
|
||||
/// menu; one that went to anything else (`None`) unselects the picture selected and closes the menu.
|
||||
pub(crate) fn picture_context(ctx: &egui::Context, page: usize, index: Option<usize>, view: &mut DocView) {
|
||||
match index {
|
||||
Some(index) => {
|
||||
if view.image_selection.as_ref().is_none_or(|s| (s.page, s.index) != (page, index)) {
|
||||
view.image_selection = Some(ImageSelection::at(page, index));
|
||||
}
|
||||
ctx.data_mut(|d| d.insert_temp(image_menu_id(), true));
|
||||
}
|
||||
None => {
|
||||
view.image_selection = None;
|
||||
ctx.data_mut(|d| d.insert_temp(image_menu_id(), false));
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// The right-click context menu for a selected image.
|
||||
|
||||
@@ -48,6 +48,17 @@ impl ObjectSelection {
|
||||
self.selected.len()
|
||||
}
|
||||
|
||||
/// Edit text's input is blocked: a dialog, the palette, another window or another tab has it.
|
||||
pub(crate) fn input_blocked(&self) -> bool {
|
||||
self.input_blocked
|
||||
}
|
||||
|
||||
/// The objects selected together are on `page`, or a gesture of theirs runs there: the hover
|
||||
/// outlines of what lies under them wait.
|
||||
pub(crate) fn holds(&self, page: usize) -> bool {
|
||||
self.page == Some(page) && (!self.selected.is_empty() || self.gesture.is_some())
|
||||
}
|
||||
|
||||
/// A preview belongs to the active, focused document. Returning from another tab or a
|
||||
/// modal must not turn a release that happened elsewhere into a document edit.
|
||||
pub fn set_input_blocked(&mut self, blocked: bool) {
|
||||
|
||||
@@ -7347,6 +7347,117 @@ fn a_dialog_keeps_its_delete_while_a_picture_is_selected() {
|
||||
assert_eq!(h.state().dialog, Some(Dialog::Preferences));
|
||||
}
|
||||
|
||||
/// A click that slips a few pixels between its press and its release, within egui's click distance.
|
||||
fn slipping_click(h: &mut Harness<'static, PdfCraftApp>, press: egui::Pos2, release: egui::Pos2) {
|
||||
h.hover_at(press);
|
||||
h.run_steps(1);
|
||||
h.drag_at(press);
|
||||
h.run_steps(1);
|
||||
h.drop_at(release);
|
||||
h.run_steps(4);
|
||||
}
|
||||
|
||||
/// [`harness`]'s page 1 in Edit text (`tool`), and the screen point at PDF (x, y) on it.
|
||||
fn edit_page(tool: pdfcraft_ui_egui::QuickTool) -> Harness<'static, PdfCraftApp> {
|
||||
let mut h = harness(2, |_| {});
|
||||
h.state_mut().quick_tool = tool;
|
||||
h.run_steps(3);
|
||||
h
|
||||
}
|
||||
|
||||
fn point_on(h: &Harness<'static, PdfCraftApp>, x: f64, y: f64) -> egui::Pos2 {
|
||||
let r = h.state().views[0].page_screen_rect(0).expect("on screen");
|
||||
let k = r.width() / 200.0;
|
||||
egui::pos2(r.left() + x as f32 * k, r.top() + (300.0 - y as f32) * k)
|
||||
}
|
||||
|
||||
/// A click on a paragraph opens it where it was pressed, though the button came up just below its
|
||||
/// box: an owner acts where its press was. It opened nothing.
|
||||
#[test]
|
||||
fn a_click_on_a_paragraph_released_just_below_it_opens_it() {
|
||||
let mut h = edit_page(pdfcraft_ui_egui::QuickTool::EditText);
|
||||
let id = h.state().views[0].id;
|
||||
let block = h.state().session.get(id).unwrap().text_blocks(0)[0].clone();
|
||||
// The box reaches 2 px below the text: pressed 1 px inside it, released 3 px below it.
|
||||
let bottom = point_on(&h, (block.rect[0] + block.rect[2]) / 2.0, block.rect[1]) + egui::vec2(0.0, 2.0);
|
||||
slipping_click(&mut h, bottom - egui::vec2(0.0, 1.0), bottom + egui::vec2(0.0, 3.0));
|
||||
assert!(h.state().views[0].line_editor.is_some(), "the paragraph pressed is open for typing");
|
||||
}
|
||||
|
||||
/// With the Select tool and the Edit panel open, a click on added text selects it where it was
|
||||
/// pressed, though the button came up just beside it. It left nothing selected.
|
||||
#[test]
|
||||
fn a_click_on_added_text_released_just_beside_it_selects_it() {
|
||||
let mut h = edit_page(pdfcraft_ui_egui::QuickTool::Select);
|
||||
assert!(h.state_mut().apply_edit(pdfcraft_engine::Edit::AddText {
|
||||
page: 0,
|
||||
text: pdfcraft_engine::AddedText { rect: [40.0, 250.0, 140.0, 265.0], text: "Added".into(), ..Default::default() },
|
||||
}));
|
||||
assert!(h.state_mut().execute("edit.text"));
|
||||
h.run_steps(3);
|
||||
h.state_mut().quick_tool = pdfcraft_ui_egui::QuickTool::Select;
|
||||
h.run_steps(3);
|
||||
// The item takes presses 2 px past its box: pressed 1 px inside that, released 3 px past it.
|
||||
let edge = point_on(&h, 140.0, 258.0) + egui::vec2(2.0, 0.0);
|
||||
slipping_click(&mut h, edge - egui::vec2(1.0, 0.0), edge + egui::vec2(3.0, 0.0));
|
||||
assert_eq!(h.state().views[0].content.selected, Some((0, 0)), "the added text pressed is selected");
|
||||
}
|
||||
|
||||
/// With Add text, a click pressed beside a paragraph and released inside its box starts a new box
|
||||
/// where it was pressed. It finished the text being typed and placed nothing.
|
||||
#[test]
|
||||
fn an_add_text_click_pressed_beside_a_paragraph_starts_a_box_there() {
|
||||
let mut h = edit_page(pdfcraft_ui_egui::QuickTool::AddText);
|
||||
let id = h.state().views[0].id;
|
||||
let block = h.state().session.get(id).unwrap().text_blocks(0)[0].clone();
|
||||
// The box reaches 2 px past the text: pressed 2 px left of it, released 2 px inside it.
|
||||
let left = point_on(&h, block.rect[0], (block.rect[1] + block.rect[3]) / 2.0) - egui::vec2(2.0, 0.0);
|
||||
slipping_click(&mut h, left - egui::vec2(2.0, 0.0), left + egui::vec2(2.0, 0.0));
|
||||
let draft = h.state().views[0].content.draft.as_ref().map(|d| d.rect);
|
||||
let x = draft.map(|r| r[0]);
|
||||
assert!(x.is_some_and(|x| x < block.rect[0]), "a new box at the press, left of the paragraph: {x:?} of {:?}", block.rect);
|
||||
assert!(h.state().views[0].line_editor.is_none(), "the paragraph stays closed");
|
||||
}
|
||||
|
||||
/// The rotate knob of a picture that fills its page lies past the page's edge, and a drag there
|
||||
/// still turns the picture: the press is the page's whose knob it is.
|
||||
#[test]
|
||||
fn dragging_the_rotate_knob_past_the_page_edge_turns_the_picture() {
|
||||
let mut png = Vec::new();
|
||||
image::RgbImage::from_pixel(80, 40, image::Rgb([200, 40, 40])).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_bytes("picture.png", None, png.clone()).expect("opens");
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
assert!(h.state_mut().execute("edit.edit_text"));
|
||||
h.run_steps(2);
|
||||
h.state_mut().views[0].image_selection = Some(pdfcraft_ui_egui::ImageSelection::at(0, 0));
|
||||
h.run_steps(3);
|
||||
let r = h.state().views[0].page_screen_rect(0).expect("on screen");
|
||||
// Above the top edge, or below the bottom one when the view has no room above.
|
||||
let knob = [egui::pos2(r.center().x, r.top() - 22.0), egui::pos2(r.center().x, r.bottom() + 22.0)]
|
||||
.into_iter()
|
||||
.find(|p| cursor_over(&mut h, *p) == egui::CursorIcon::Grab)
|
||||
.expect("the knob past the page's edge");
|
||||
assert!(!r.contains(knob));
|
||||
let to = egui::pos2(r.right() + 60.0, r.center().y);
|
||||
h.hover_at(knob);
|
||||
h.run_steps(1);
|
||||
h.drag_at(knob);
|
||||
h.run_steps(1);
|
||||
for k in 1..=4 {
|
||||
h.hover_at(knob + (to - knob) * (k as f32 / 4.0));
|
||||
h.run_steps(1);
|
||||
}
|
||||
h.drop_at(to);
|
||||
h.run_steps(4);
|
||||
let id = h.state().views[0].id;
|
||||
assert_eq!(h.state().session.get(id).unwrap().can_undo(), Some("Rotate image"));
|
||||
}
|
||||
|
||||
/// Organize pages has Acrobat's panel whenever the grid shows, however it was opened, and the
|
||||
/// panel's Close ends Organize: the grid closes and All tools comes back.
|
||||
#[test]
|
||||
@@ -8138,6 +8249,34 @@ fn test_image_context_menu_crop_overlapping_image_and_handle() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A right-click on the part of a picture its crop cuts away is the crop's, as one on the box kept:
|
||||
/// crop mode stays and no menu opens (#5835).
|
||||
#[test]
|
||||
fn a_right_click_on_the_part_a_crop_cuts_away_keeps_crop_mode() {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 1400.0)).build_eframe(|_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_bytes("cut.pdf", None, two_image_fixture()).unwrap();
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
let id = h.state().views[0].id;
|
||||
let rect = h.state().session.get(id).unwrap().page_images(0)[0].rect;
|
||||
assert!(h.state_mut().execute("edit.edit_text"));
|
||||
h.run_steps(4);
|
||||
// Keep the picture's left half.
|
||||
let mut selection = pdfcraft_ui_egui::ImageSelection::at(0, 0);
|
||||
selection.crop = Some(pdfcraft_ui_egui::CropMode::new([rect[0], rect[1], (rect[0] + rect[2]) / 2.0, rect[3]]));
|
||||
h.state_mut().views[0].image_selection = Some(selection);
|
||||
h.run_steps(2);
|
||||
let xf = h.state().views[0].page_xform(0).unwrap();
|
||||
let doc = h.state().session.get(id).unwrap();
|
||||
let b = pdfcraft_ui_egui::canvas::image_screen_rect(&doc.page_images(0)[0], &xf, &doc.info, 0);
|
||||
assert!(b.width() > 60.0, "the picture is wide enough for a point clear of the grips: {b:?}");
|
||||
crop_secondary_click(&mut h, b.lerp_inside(egui::vec2(0.8, 0.5)));
|
||||
assert_crop_context_preserved(&h);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_image_context_menu_crop_page_edge_pills() {
|
||||
for rotation in [0, 90, 180, 270] {
|
||||
|
||||
@@ -1418,19 +1418,25 @@ fn the_link_tools_hand_stops_at_the_pages_edge() {
|
||||
assert_ne!(cursor_at(&mut h, beside), egui::CursorIcon::PointingHand, "just off the page");
|
||||
}
|
||||
|
||||
/// With Edit text a right-click on a paragraph is Edit text's, as before, though a comment lies
|
||||
/// over it: the comment is not selected. The page's menu is anchored there all the same (Paste on
|
||||
/// the picture menu, Delete page); it used to stay where the last right-click comments saw was.
|
||||
/// With Edit text a right-click goes to what lies on top under it, as with every tool: on a
|
||||
/// paragraph under a comment, the comment, selected for its menu; on the paragraph beside it, the
|
||||
/// paragraph, and the comment is unselected. The page's menu is anchored there either way (Paste on
|
||||
/// the picture menu, Delete page). Edit text used to keep the right-clicks on its paragraphs, and
|
||||
/// left the comment over them unselected.
|
||||
#[test]
|
||||
fn a_right_click_with_edit_text_on_a_paragraph_under_a_comment_selects_no_comment() {
|
||||
fn a_right_click_with_edit_text_on_a_paragraph_under_a_comment_selects_the_comment() {
|
||||
let mut h = open(comment_on_text_pdf(), "150", false);
|
||||
h.state_mut().quick_tool = QuickTool::EditText;
|
||||
h.run_steps(4);
|
||||
let on_text = at(&h, 100.0, 303.0);
|
||||
wait_for_cursor(&mut h, on_text, |c| c != egui::CursorIcon::Default);
|
||||
right_click(&mut h, on_text);
|
||||
assert_eq!(h.state().views[0].comments.selected, None);
|
||||
assert_eq!(h.state().views[0].comments.selected, Some((0, 0)), "the comment over the paragraph is selected");
|
||||
assert_eq!(h.state().views[0].comments.context_at.map(|(page, _)| page), Some(0), "the menu is anchored on the page");
|
||||
// The rectangle ends at x 200; the line goes on.
|
||||
let beside = at(&h, 230.0, 303.0);
|
||||
right_click(&mut h, beside);
|
||||
assert_eq!(h.state().views[0].comments.selected, None, "the paragraph beside the comment leaves it unselected");
|
||||
}
|
||||
|
||||
/// With a Prepare field tool a right-click is the tool's, as before: a comment under it is not
|
||||
@@ -1591,8 +1597,8 @@ fn a_right_click_after_escape_cancelled_the_press_anchors_the_menu_on_its_page()
|
||||
assert_eq!(menu_page_after_a_right_click_with_the_button_held(true), Some(1), "the menu is page 2's");
|
||||
}
|
||||
|
||||
/// With Edit text a right-click on added text reaches comments, as before (Edit text keeps those on
|
||||
/// its own paragraphs and pictures): the selected comment is unselected.
|
||||
/// With Edit text a right-click on added text reaches comments, as before: the selected comment is
|
||||
/// unselected.
|
||||
#[test]
|
||||
fn a_right_click_with_edit_text_on_added_text_unselects_the_comment() {
|
||||
let mut h = open(comment_on_text_pdf(), "150", false);
|
||||
@@ -1757,11 +1763,12 @@ fn a_right_click_between_the_pages_opens_the_page_menu_not_the_selected_comments
|
||||
assert!(h.query_by_label("Reply").is_none(), "not the comment's");
|
||||
}
|
||||
|
||||
/// Where pages touch, a right-click on the row they share is page 1's, and a picture on page 2 there
|
||||
/// opens its menu, whose Paste then places the image as Ctrl+V does. It used to centre the image on
|
||||
/// page 1's point, which on page 2 lies at its foot.
|
||||
/// Where pages touch, a right-click on the row they share is page 1's, whatever lies there on page 2:
|
||||
/// page 2's picture across its top edge opens no menu, and page 1's menu pastes on page 1, where it
|
||||
/// opened. The page view's own picture scan used to open page 2's picture's menu there, whose Paste
|
||||
/// centred the image on page 1's point, which on page 2 lies at its foot.
|
||||
#[test]
|
||||
fn paste_from_a_picture_menu_where_two_pages_touch_keeps_off_the_first_pages_point() {
|
||||
fn paste_from_a_right_click_where_two_pages_touch_keeps_to_the_first_page() {
|
||||
let (mut h, seam) = touching_pages(|app| {
|
||||
app.system_clipboard = false;
|
||||
app.quick_tool = QuickTool::EditText;
|
||||
@@ -1774,16 +1781,18 @@ fn paste_from_a_picture_menu_where_two_pages_touch_keeps_off_the_first_pages_poi
|
||||
h.run_steps(4);
|
||||
right_click(&mut h, seam);
|
||||
assert_eq!(h.state().views[0].comments.context_at.map(|(page, _)| page), Some(0), "page 1 takes the right-click");
|
||||
assert_eq!(h.state().views[0].image_selection.as_ref().map(|s| s.page), Some(1), "page 2's picture opens its menu");
|
||||
h.get_by_label("Paste").click();
|
||||
assert!(h.state().views[0].image_selection.is_none(), "page 2's picture is not selected for its menu");
|
||||
h.get_by_label_contains("Paste").click();
|
||||
h.run_steps(3);
|
||||
let pasted = {
|
||||
let images = |page| {
|
||||
let s = h.state();
|
||||
s.session.get(s.views[0].id).unwrap().page_images(1)
|
||||
s.session.get(s.views[0].id).unwrap().page_images(page)
|
||||
};
|
||||
assert_eq!(pasted.len(), 2, "the image is pasted on page 2");
|
||||
let r = pasted[1].rect;
|
||||
assert!((r[1] + r[3]) / 2.0 > 100.0, "not centred at page 2's foot: {r:?}");
|
||||
assert_eq!(images(1).len(), 1, "nothing is pasted on page 2");
|
||||
let pasted = images(0);
|
||||
assert_eq!(pasted.len(), 1, "the image is pasted on page 1");
|
||||
let r = pasted[0].rect;
|
||||
assert!((r[1] + r[3]) / 2.0 < 100.0, "at page 1's foot, where its menu opened: {r:?}");
|
||||
}
|
||||
|
||||
/// With the camera tool, a click on another page moves the quadrilateral there, as a drag from it
|
||||
|
||||
Reference in new issue
Block a user