mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 12:32:52 +00:00
fix(ui): Edit text's presses that 4b's routing dropped reach their owners
Found in review, each against `main`'s behaviour: - A double-click on one of the objects selected together only left the group: the second press was the group's. It is the object's under it, so added text opens for typing and a picture is selected, as before; a paragraph opens too (the user's ruling). - On a page whose objects can't be selected together, a click on the empty page left the picture selected: no area selection starts there to drop it, and the picture's own test that did is gone. A click nobody holds on the page unselects it. - Crop mode's grips stopped at the page's edge, so those of a picture filling its page could be taken from their inner half only. They reach past it, cursor, press and right-click: no page took a right-click there, which left crop mode and opened the page menu (#5900 keeps crop mode there). The box and the picture stop at the page's edge, as a clipped artwork's whole frame can reach over the next pages. - A press past a page's edge was the page's whenever it had any hit there: the objects selected together, or the open paragraph's frame, took presses on the next page and did nothing with them. Past the edge only the selected picture's knob and grips (crop mode's too) take a press, and the objects' hit stays on its page. - A drag from a hole in the selected picture (a clip with a hole) fell through, started an area selection and unselected it. The selected picture's frame is its own, as before. - Image pick: a drag or double-click from a link let the picked picture go before ruling 5, and does again; the link's click still picks none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
8 files changed
+285
-55
No files matched your search
@@ -1242,11 +1242,10 @@ struct PageFrame<'a> {
|
||||
}
|
||||
|
||||
impl PageFrame<'_> {
|
||||
/// Where this frame's right-click is, on the page at `rect`, for the first page it is on: pages
|
||||
/// that touch (the gaps hidden) can share a row of pixels, and a right-click there is one
|
||||
/// page's, as a press is.
|
||||
fn right_click_on(&mut self, rect: Rect) -> Option<Pos2> {
|
||||
let p = self.pointer.filter(|p| !self.right_clicked && self.resp.secondary_clicked() && rect.contains(*p))?;
|
||||
/// Where this frame's right-click is, for the first page it is `on`: pages that touch (the gaps
|
||||
/// hidden) can share a row of pixels, and a right-click there is one page's, as a press is.
|
||||
fn right_click_on(&mut self, on: impl FnOnce(Pos2) -> bool) -> Option<Pos2> {
|
||||
let p = self.pointer.filter(|p| !self.right_clicked && self.resp.secondary_clicked() && on(*p))?;
|
||||
self.right_clicked = true;
|
||||
Some(p)
|
||||
}
|
||||
@@ -1545,7 +1544,7 @@ fn draw_page(ui: &mut egui::Ui, view: &mut DocView, frame: &mut PageFrame<'_>, i
|
||||
// A cancelled press takes nothing until the button is up, though the page's menu still opens
|
||||
// where a right-click is.
|
||||
let input = if view.press_cancelled {
|
||||
if let Some(p) = frame.right_click_on(xf.rect) {
|
||||
if let Some(p) = frame.right_click_on(|p| xf.rect.contains(p)) {
|
||||
comments::anchor(&pcx, p, view);
|
||||
}
|
||||
PageInput {
|
||||
|
||||
@@ -22,8 +22,10 @@ pub struct PickedImage {
|
||||
const BLUE: Color32 = Color32::from_rgb(0x3A, 0x7B, 0xF0);
|
||||
|
||||
/// A click on page `page` (Select tool, not taken by anything else) selects the picture under
|
||||
/// it, even with text drawn over it (scans with an OCR layer, captions); dragging or a double
|
||||
/// click still selects text, and clears the picture.
|
||||
/// it, even with text drawn over it (scans with an OCR layer, captions), with `pick` (a press
|
||||
/// nobody holds: a link's click follows the link only); dragging or a double click still selects
|
||||
/// text, and clears the picture.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn page_input(
|
||||
resp: &egui::Response,
|
||||
xf: &PageXform,
|
||||
@@ -31,6 +33,7 @@ pub(crate) fn page_input(
|
||||
info: &DocInfo,
|
||||
doc: &pdfcraft_engine::Document,
|
||||
ptr: &crate::page_pointer::PagePointer,
|
||||
pick: bool,
|
||||
view: &mut DocView,
|
||||
) {
|
||||
let Some(p) = ptr.pos.filter(|p| xf.rect.contains(*p)) else { return };
|
||||
@@ -38,7 +41,7 @@ pub(crate) fn page_input(
|
||||
view.picked_image = None;
|
||||
return;
|
||||
}
|
||||
if !resp.clicked() {
|
||||
if !resp.clicked() || !pick {
|
||||
return;
|
||||
}
|
||||
let Some(info_page) = info.pages.get(page) else { return };
|
||||
|
||||
@@ -108,9 +108,13 @@ pub(super) fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut
|
||||
&& !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());
|
||||
// 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() {
|
||||
// A press is the page's it lies on, or past its edge the one whose selected picture has its
|
||||
// rotate knob or a grip there (crop mode's grips too).
|
||||
let past_edge = |h: &crate::page_router::Hit| {
|
||||
use crate::page_router::PicturePart;
|
||||
matches!(h.owner, Owner::Picture { part: PicturePart::Knob | PicturePart::Corner(_) | PicturePart::Crop(_), .. })
|
||||
};
|
||||
if xf.rect.contains(o) || at.iter().any(past_edge) {
|
||||
view.router.press(i, at, tool);
|
||||
}
|
||||
}
|
||||
@@ -126,6 +130,16 @@ pub(super) fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut
|
||||
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);
|
||||
// A double-click on the objects selected together is the object's under it: the double-click
|
||||
// leaves the group (`object_ui`), and added text opens for typing, a picture or a paragraph is
|
||||
// the one taken (the user's ruling).
|
||||
let press = match press {
|
||||
PagePress::Mine(Owner::Objects { part: crate::page_router::ObjectsPart::Selection }) if resp.double_clicked() => {
|
||||
top(under.iter().filter(|h| !matches!(h.owner, Owner::Objects { .. })).cloned(), tool)
|
||||
.map_or(PagePress::Free, |h| PagePress::Mine(h.owner))
|
||||
}
|
||||
press => press,
|
||||
};
|
||||
let released_over = match press {
|
||||
PagePress::Mine(owner) if resp.clicked() => under.iter().any(|h| h.owner == owner),
|
||||
_ => false,
|
||||
@@ -135,7 +149,12 @@ 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 right = frame.right_click_on(xf.rect);
|
||||
// A right-click is the page's it lies on, or past its edge the one whose crop grip lies there,
|
||||
// as a press is (#5835): what a right-click there would go to is asked only off the page.
|
||||
let right = frame.right_click_on(|p| {
|
||||
use crate::page_router::PicturePart;
|
||||
xf.rect.contains(p) || matches!(hits.context_at(view, p, &[]), Some(Owner::Picture { part: PicturePart::Crop(_), .. }))
|
||||
});
|
||||
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.
|
||||
@@ -794,9 +813,9 @@ pub(super) fn page_overlays(ui: &mut egui::Ui, view: &mut DocView, frame: &mut P
|
||||
let tool_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 };
|
||||
// A press nobody holds picks the picture under it: a link's click follows the link only (ruling
|
||||
// 5).
|
||||
if tool == QuickTool::Select && input.press == PagePress::Free && !input.consumed {
|
||||
crate::image_pick::page_input(resp, &xf, i, info, doc, ptr, view);
|
||||
// 5), though its drag or double-click still lets the picked picture go.
|
||||
if tool == QuickTool::Select && matches!(input.press, PagePress::Free | PagePress::Mine(Owner::Link { .. })) && !input.consumed {
|
||||
crate::image_pick::page_input(resp, &xf, i, info, doc, ptr, input.press == PagePress::Free, view);
|
||||
}
|
||||
crate::image_pick::drop_stale(doc, view);
|
||||
crate::image_pick::paint(painter, &xf, i, info, view);
|
||||
|
||||
@@ -571,40 +571,41 @@ 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, 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.
|
||||
/// [`CROP_GRIP_REACH`], past the page's edge too), 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,
|
||||
/// its grips and its controls: the box and the picture stop at the page's edge, as a clipped
|
||||
/// artwork's whole frame can reach over the next pages.
|
||||
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()?;
|
||||
let control = mode.controls.iter().any(|r| r.contains(p));
|
||||
if !control && !xf.rect.contains(p) {
|
||||
return None;
|
||||
}
|
||||
let crop = |part, icon| Some(Hit::new(Owner::Picture { index: s.index, part: PicturePart::Crop(part) }, Layer::Editor, icon));
|
||||
if control {
|
||||
if mode.controls.iter().any(|r| r.contains(p)) {
|
||||
return crop(CropPart::Page, egui::CursorIcon::PointingHand);
|
||||
}
|
||||
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);
|
||||
}
|
||||
let image = images.get(s.index);
|
||||
let kept = image.and_then(|image| crop_frames(xf, info, page, image, mode.keep)).map(|(_, k)| k);
|
||||
// The grips reach past the page's edge, where those of a picture filling its page lie half.
|
||||
if let Some(g) = kept.and_then(|k| 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 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);
|
||||
if !xf.rect.contains(p) {
|
||||
return None;
|
||||
}
|
||||
crop(CropPart::Page, egui::CursorIcon::Default)
|
||||
if kept.is_some_and(|k| k.contains(p)) {
|
||||
return crop(CropPart::Keep, egui::CursorIcon::Grab);
|
||||
}
|
||||
let on_picture = image.is_some_and(|image| {
|
||||
let shown = ImageObb::from_image(image, xf, info, page);
|
||||
let whole = ImageObb::from_matrix(image.matrix, xf, info, page);
|
||||
quad_contains(shown.corners(), p) || quad_contains(whole.corners(), p)
|
||||
});
|
||||
crop(if on_picture { CropPart::Cut } else { CropPart::Page }, egui::CursorIcon::Default)
|
||||
}
|
||||
|
||||
/// The picture under `p` (its shape, not only its box): a click selects it, a drag moves the
|
||||
/// selected one. On the text layer, asked before the paragraphs: editable text on top of a
|
||||
/// picture takes the press.
|
||||
/// The picture under `p` (its shape, not only its box; for the selected one, its frame, a hole in it
|
||||
/// included): a click selects it, a drag moves the selected one. On the text layer, asked before
|
||||
/// the paragraphs: editable text on top of a picture takes the press.
|
||||
pub(crate) fn body_hit(xf: &PageXform, page: usize, info: &DocInfo, images: &[pdfcraft_engine::PageImage], view: &DocView, p: Pos2) -> Option<Hit> {
|
||||
if !xf.rect.contains(p) {
|
||||
return None;
|
||||
@@ -613,7 +614,12 @@ pub(crate) fn body_hit(xf: &PageXform, page: usize, info: &DocInfo, images: &[pd
|
||||
let user = info.pages.get(page)?.view_to_user(x, y).map(f64::from);
|
||||
let index = images
|
||||
.iter()
|
||||
.rposition(|im| im.visible_rect.is_some_and(|r| xf.user_rect(info, page, r.map(|v| v as f32)).contains(p)) && im.contains(user))?;
|
||||
.rposition(|im| im.visible_rect.is_some_and(|r| xf.user_rect(info, page, r.map(|v| v as f32)).contains(p)) && im.contains(user))
|
||||
.or_else(|| {
|
||||
let s = view.image_selection.as_ref().filter(|s| s.page == page && s.crop.is_none())?;
|
||||
let image = images.get(s.index)?;
|
||||
quad_contains(ImageObb::from_image(image, xf, info, page).corners(), p).then_some(s.index)
|
||||
})?;
|
||||
let selected = view.image_selection.as_ref().is_some_and(|s| s.page == page && s.index == index);
|
||||
let icon = if selected { egui::CursorIcon::Move } else { egui::CursorIcon::PointingHand };
|
||||
Some(Hit::new(Owner::Picture { index, part: PicturePart::Body }, Layer::Text, icon))
|
||||
@@ -966,16 +972,19 @@ pub(crate) fn image_input(cx: &ImageCx<'_>, images: &[pdfcraft_engine::PageImage
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// A click selects the picture its press was on; its cursor is the hit's.
|
||||
// A click selects the picture its press was on; its cursor is the hit's. One nobody holds on this
|
||||
// page leaves it unselected (where the page's objects can be selected together, the area
|
||||
// selection the press starts has done so already).
|
||||
let owner = match press {
|
||||
PagePress::Mine(Owner::Picture { index, .. }) => Some(index),
|
||||
_ => None,
|
||||
};
|
||||
if gate
|
||||
&& resp.clicked()
|
||||
&& let Some(index) = owner
|
||||
{
|
||||
view.image_selection = Some(ImageSelection::at(page, index));
|
||||
if gate && resp.clicked() {
|
||||
match owner {
|
||||
Some(index) => view.image_selection = Some(ImageSelection::at(page, index)),
|
||||
None if press == PagePress::Free && ptr.released.is_some_and(|p| xf.rect.contains(p)) => view.image_selection = None,
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
owner.is_some()
|
||||
}
|
||||
|
||||
@@ -187,6 +187,10 @@ fn leave_single(view: &mut DocView) {
|
||||
/// takes hold of the box as in Acrobat: a click selects it, a drag moves it, its text keeps the
|
||||
/// caret.
|
||||
pub(crate) fn hit(view: &DocView, xf: &PageXform, info: &DocInfo, page: usize, open: Option<Rect>, p: Pos2) -> Option<Hit> {
|
||||
// Past the page's edge is no object's: the next page lies there.
|
||||
if !xf.rect.contains(p) {
|
||||
return None;
|
||||
}
|
||||
let s = &view.objects;
|
||||
if s.page == Some(page) && !s.selected.is_empty() && s.selected.iter().fold(Rect::NOTHING, |r, o| r.union(screen(xf, info, page, o))).contains(p)
|
||||
{
|
||||
|
||||
@@ -6503,6 +6503,15 @@ fn clipped_artwork_with_labels() -> Vec<u8> {
|
||||
|
||||
/// `own_clip` draws the image alone in its clip (`q re W n cm Do Q`), which moves with it (#1049).
|
||||
fn clipped_artwork_with_labels_drawn(own_clip: bool) -> Vec<u8> {
|
||||
artwork_drawn(if own_clip {
|
||||
&b"q 30 170 130 80 re W* n 800 0 0 900 -300 -400 cm /Im0 Do Q BT /F1 12 Tf 30 130 Td (Editable caption) Tj ET BT /F1 12 Tf 40 220 Td (Overlay) Tj ET BT /F1 9 Tf 154 249 Td (Corner label) Tj ET"[..]
|
||||
} else {
|
||||
&b"q 30 170 130 80 re W* n q 800 0 0 900 -300 -400 cm /Im0 Do Q Q BT /F1 12 Tf 30 130 Td (Editable caption) Tj ET BT /F1 12 Tf 40 220 Td (Overlay) Tj ET BT /F1 9 Tf 154 249 Td (Corner label) Tj ET"[..]
|
||||
})
|
||||
}
|
||||
|
||||
/// [`fixture`]'s two pages, each drawn by `content`, with a one-pixel picture as `/Im0`.
|
||||
fn artwork_drawn(content: &[u8]) -> Vec<u8> {
|
||||
use pdfcraft_cos::{Dict, Document, Object, Stream, write_incremental};
|
||||
let mut doc = Document::open(std::sync::Arc::new(fixture(2))).unwrap();
|
||||
let mut image = Dict::new();
|
||||
@@ -6514,12 +6523,7 @@ fn clipped_artwork_with_labels_drawn(own_clip: bool) -> Vec<u8> {
|
||||
image.set(b"ColorSpace".to_vec(), Object::name("DeviceRGB"));
|
||||
let image = doc.add(Object::Stream(Stream::flate(image, &[220, 235, 245])));
|
||||
for page in pdfcraft_model::pages(&doc) {
|
||||
let contents = doc.add(Object::Stream(Stream::flate(Dict::new(),
|
||||
if own_clip {
|
||||
&b"q 30 170 130 80 re W* n 800 0 0 900 -300 -400 cm /Im0 Do Q BT /F1 12 Tf 30 130 Td (Editable caption) Tj ET BT /F1 12 Tf 40 220 Td (Overlay) Tj ET BT /F1 9 Tf 154 249 Td (Corner label) Tj ET"[..]
|
||||
} else {
|
||||
&b"q 30 170 130 80 re W* n q 800 0 0 900 -300 -400 cm /Im0 Do Q Q BT /F1 12 Tf 30 130 Td (Editable caption) Tj ET BT /F1 12 Tf 40 220 Td (Overlay) Tj ET BT /F1 9 Tf 154 249 Td (Corner label) Tj ET"[..]
|
||||
})));
|
||||
let contents = doc.add(Object::Stream(Stream::flate(Dict::new(), content)));
|
||||
let mut resources = doc.resolve(page.dict.get(b"Resources").unwrap()).as_dict().unwrap().clone();
|
||||
let mut xo = Dict::new();
|
||||
xo.set(b"Im0".to_vec(), Object::Ref(image));
|
||||
@@ -7233,10 +7237,15 @@ fn cropping_a_picture_with_its_handles() {
|
||||
/// Both pages of [`clipped_artwork_with_labels`] at 100% in Edit text, the picture on page 0
|
||||
/// selected (and being cropped, with `crop`).
|
||||
fn picture_pages(crop: bool) -> Harness<'static, PdfCraftApp> {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 1100.0)).build_eframe(|_cc| {
|
||||
picture_pages_of(clipped_artwork_with_labels(), crop)
|
||||
}
|
||||
|
||||
/// [`picture_pages`], of `pdf`.
|
||||
fn picture_pages_of(pdf: Vec<u8>, crop: bool) -> Harness<'static, PdfCraftApp> {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 1100.0)).build_eframe(move |_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_bytes("clipped.pdf", None, clipped_artwork_with_labels()).unwrap();
|
||||
app.open_bytes("clipped.pdf", None, pdf).unwrap();
|
||||
app.set_option("left", "closed").unwrap();
|
||||
app.set_option("zoom", "100").unwrap();
|
||||
app
|
||||
@@ -7355,6 +7364,99 @@ fn in_crop_mode_a_right_click_on_another_picture_selects_it() {
|
||||
assert!(h.query_by_label("Flip horizontal").is_some(), "its menu opens");
|
||||
}
|
||||
|
||||
/// A drag from a hole in the selected picture (drawn through a clip with a hole in it) moves the
|
||||
/// picture: its frame is its own. The press fell through the hole, started an area selection and
|
||||
/// unselected it.
|
||||
#[test]
|
||||
fn a_drag_from_a_hole_in_the_selected_picture_moves_it() {
|
||||
// The picture shows at x 30..160, y 170..250, around a hole at x 70..120, y 190..230.
|
||||
let mut h = picture_pages_of(artwork_drawn(b"q 30 170 130 80 re 70 190 50 40 re W* n 800 0 0 900 -300 -400 cm /Im0 Do Q"), false);
|
||||
let id = h.state().views[0].id;
|
||||
let before = h.state().session.get(id).unwrap().page_images(0)[0].rect;
|
||||
let hole = page_at(&h, 0, 95.0, 210.0);
|
||||
h.hover_at(hole);
|
||||
h.run_steps(1);
|
||||
h.drag_at(hole);
|
||||
h.run_steps(1);
|
||||
for k in 1..=4 {
|
||||
h.hover_at(hole + egui::vec2(5.0 * k as f32, 0.0));
|
||||
h.run_steps(1);
|
||||
}
|
||||
h.drop_at(hole + egui::vec2(20.0, 0.0));
|
||||
h.run_steps(4);
|
||||
let after = h.state().session.get(id).unwrap().page_images(0)[0].rect;
|
||||
assert!(after[0] - before[0] > 5.0, "the picture moved right: {before:?} → {after:?}");
|
||||
}
|
||||
|
||||
/// A right-click on a crop grip past the page's edge, where those of a picture filling its page lie
|
||||
/// half, is the crop's, as on the page: crop mode stays and no menu opens (#5835). No page took it,
|
||||
/// which left crop mode.
|
||||
#[test]
|
||||
fn a_right_click_on_a_crop_grip_past_the_page_edge_keeps_crop_mode() {
|
||||
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);
|
||||
let id = h.state().views[0].id;
|
||||
let keep = h.state().session.get(id).unwrap().page_images(0)[0].visible_rect.unwrap();
|
||||
let mut selection = pdfcraft_ui_egui::ImageSelection::at(0, 0);
|
||||
selection.crop = Some(pdfcraft_ui_egui::CropMode::new(keep));
|
||||
h.state_mut().views[0].image_selection = Some(selection);
|
||||
h.run_steps(3);
|
||||
let r = h.state().views[0].page_screen_rect(0).expect("on screen");
|
||||
crop_secondary_click(&mut h, r.right_bottom() + egui::vec2(4.0, 4.0));
|
||||
assert!(h.state().views[0].image_selection.as_ref().is_some_and(|s| s.crop.is_some()), "crop mode stays");
|
||||
for label in ["Flip horizontal", "Rotate view clockwise"] {
|
||||
assert!(h.query_all_by_label(label).next().is_none(), "no menu: {label}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Crop mode's grips reach past the page's edge, where those of a picture filling its page lie
|
||||
/// half: a drag from just outside the page's corner trims the picture. Only the half on the page took
|
||||
/// the press.
|
||||
#[test]
|
||||
fn a_crop_grip_past_the_page_edge_drags() {
|
||||
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);
|
||||
let id = h.state().views[0].id;
|
||||
let before = h.state().session.get(id).unwrap().page_images(0)[0].visible_rect.unwrap();
|
||||
let mut selection = pdfcraft_ui_egui::ImageSelection::at(0, 0);
|
||||
selection.crop = Some(pdfcraft_ui_egui::CropMode::new(before));
|
||||
h.state_mut().views[0].image_selection = Some(selection);
|
||||
h.run_steps(3);
|
||||
let r = h.state().views[0].page_screen_rect(0).expect("on screen");
|
||||
let outside = r.right_bottom() + egui::vec2(4.0, 4.0);
|
||||
let to = r.right_bottom() - egui::vec2(r.width() / 4.0, r.height() / 4.0);
|
||||
h.hover_at(outside);
|
||||
h.run_steps(1);
|
||||
h.drag_at(outside);
|
||||
h.run_steps(1);
|
||||
for k in 1..=4 {
|
||||
h.hover_at(outside + (to - outside) * (k as f32 / 4.0));
|
||||
h.run_steps(1);
|
||||
}
|
||||
h.drop_at(to);
|
||||
h.run_steps(2);
|
||||
let keep = h.state().views[0].image_selection.as_ref().and_then(|s| s.crop.as_ref()).map(|c| c.keep);
|
||||
assert!(keep.is_some_and(|k| k[2] < before[2] - 1.0 && k[1] > before[1] + 1.0), "trimmed from the corner: {keep:?} of {before:?}");
|
||||
}
|
||||
|
||||
/// Delete removes the selected picture wherever the pointer is, on a paragraph too: the key is the
|
||||
/// app's, not the pointer's.
|
||||
#[test]
|
||||
|
||||
@@ -438,3 +438,16 @@ fn a_press_just_outside_a_picture_starts_an_area_selection() {
|
||||
h.run_steps(4);
|
||||
h.get_by_label("2 objects selected");
|
||||
}
|
||||
|
||||
/// A double-click on one of the objects selected together leaves the group and reaches that one:
|
||||
/// added text opens for typing. The double-click only left the group.
|
||||
#[test]
|
||||
fn a_double_click_on_a_grouped_object_reaches_it() {
|
||||
let mut h = harness();
|
||||
marquee(&mut h);
|
||||
// The added note is x 130..230, y 130..144 from the page's top.
|
||||
let note = at(&h, 180.0, 137.0);
|
||||
crate::support::input::double_click(&mut h, note);
|
||||
h.run_steps(2);
|
||||
assert!(h.state().views[0].content.draft.as_ref().is_some_and(|d| d.index == Some(0)), "the added note opens for typing");
|
||||
}
|
||||
@@ -81,6 +81,31 @@ fn linked_picture_pdf() -> Vec<u8> {
|
||||
])
|
||||
}
|
||||
|
||||
/// Two 300×400 pages, the first with a line of text at its bottom edge, at y 4, the second with one
|
||||
/// near its top, at y 380.
|
||||
fn bottom_line_pages_pdf() -> Vec<u8> {
|
||||
let bottom = stream("BT /F1 12 Tf 40 4 Td (Bottom line) Tj ET");
|
||||
let top = stream("BT /F1 12 Tf 40 380 Td (Top line) Tj ET");
|
||||
pdf(&[
|
||||
CATALOG,
|
||||
"<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 /MediaBox [0 0 300 400] >>",
|
||||
"<< /Type /Page /Parent 2 0 R /Contents 5 0 R /Resources << /Font << /F1 7 0 R >> >> >>",
|
||||
"<< /Type /Page /Parent 2 0 R /Contents 6 0 R /Resources << /Font << /F1 7 0 R >> >> >>",
|
||||
bottom.as_str(),
|
||||
top.as_str(),
|
||||
HELVETICA,
|
||||
])
|
||||
}
|
||||
|
||||
/// A 300×400 page with a picture at x 50..250, y 250..350, and graphics states nested deeper than
|
||||
/// the objects selected together are read through: they can't be selected together on this page.
|
||||
fn ungroupable_picture_pdf() -> Vec<u8> {
|
||||
let picture =
|
||||
"<< /Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Length 1 >>\nstream\nA\nendstream";
|
||||
let content = stream(&format!("q 200 0 0 100 50 250 cm /Im0 Do Q {}{}", "q ".repeat(1030), "Q ".repeat(1030)));
|
||||
pdf(&[CATALOG, PAGES, "<< /Type /Page /Parent 2 0 R /Contents 4 0 R /Resources << /XObject << /Im0 5 0 R >> >> >>", content.as_str(), picture])
|
||||
}
|
||||
|
||||
/// A 300×400 page with a line of text at y≈300 and a rectangle comment over it.
|
||||
fn comment_on_text_pdf() -> Vec<u8> {
|
||||
let text = stream(LINE);
|
||||
@@ -1867,6 +1892,62 @@ fn with_edit_text_a_right_click_on_a_picture_under_a_link_selects_the_picture()
|
||||
assert_eq!(selected, Some((0, 0)), "the picture is selected for its menu");
|
||||
}
|
||||
|
||||
/// With the gaps between pages hidden, a drag from the top of page 2 under page 1's open paragraph is
|
||||
/// page 2's: its area selection takes the line there. The paragraph's frame, which reaches past
|
||||
/// page 1's edge, took the press and did nothing with it.
|
||||
#[test]
|
||||
fn a_drag_past_a_pages_edge_under_its_open_paragraph_is_the_next_pages() {
|
||||
let mut h = open(bottom_line_pages_pdf(), "100", false);
|
||||
h.state_mut().views[0].show_page_gaps = false;
|
||||
h.state_mut().quick_tool = QuickTool::EditText;
|
||||
h.run_steps(4);
|
||||
let (one, two) = {
|
||||
let v = &h.state().views[0];
|
||||
(v.page_screen_rect(0).expect("page 1 on screen"), v.page_screen_rect(1).expect("page 2 on screen"))
|
||||
};
|
||||
let line = egui::pos2(one.min.x + 60.0, one.max.y - 8.0);
|
||||
mouse_click(&mut h, line);
|
||||
assert!(h.state().views[0].line_editor.is_some(), "the bottom line is open");
|
||||
let below = egui::pos2(one.min.x + 60.0, two.min.y + 3.0);
|
||||
mouse_drag(&mut h, below, egui::vec2(200.0, 30.0));
|
||||
// What the area selection took, Delete removes.
|
||||
h.key_press(egui::Key::Delete);
|
||||
h.run_steps(4);
|
||||
let s = h.state();
|
||||
assert!(s.session.get(s.views[0].id).unwrap().text_blocks(1).is_empty(), "page 2's area selection took its line");
|
||||
}
|
||||
|
||||
/// With Edit text, a click on the empty page leaves the selected picture unselected, on a page whose
|
||||
/// objects can't be selected together too, where no area selection starts to do it. It stayed.
|
||||
#[test]
|
||||
fn a_click_beside_the_picture_unselects_it_where_objects_cant_be_grouped() {
|
||||
let mut h = open(ungroupable_picture_pdf(), "150", false);
|
||||
h.state_mut().quick_tool = QuickTool::EditText;
|
||||
h.run_steps(4);
|
||||
let s = h.state();
|
||||
let doc = s.session.get(s.views[0].id).unwrap();
|
||||
assert_eq!(doc.page_images(0).len(), 1, "the picture");
|
||||
assert!(doc.editable_objects(0).is_err(), "and no objects to group");
|
||||
h.state_mut().views[0].image_selection = Some(pdfcraft_ui_egui::ImageSelection::at(0, 0));
|
||||
h.run_steps(2);
|
||||
let blank = at(&h, 150.0, 100.0);
|
||||
mouse_click(&mut h, blank);
|
||||
assert!(h.state().views[0].image_selection.is_none(), "the picture is unselected");
|
||||
}
|
||||
|
||||
/// With the Select tool a drag that starts on a link lets the picked picture go, as a drag elsewhere
|
||||
/// does, though the link's click picks none.
|
||||
#[test]
|
||||
fn a_drag_from_a_link_lets_the_picked_picture_go() {
|
||||
let mut h = open(linked_picture_pdf(), "150", false);
|
||||
let beside = at(&h, 70.0, 300.0);
|
||||
mouse_click(&mut h, beside);
|
||||
assert!(h.state().views[0].picked_image.is_some(), "the picture is picked");
|
||||
let on_link = at(&h, 150.0, 300.0);
|
||||
mouse_drag(&mut h, on_link, egui::vec2(30.0, 0.0));
|
||||
assert!(h.state().views[0].picked_image.is_none(), "the drag lets it go");
|
||||
}
|
||||
|
||||
/// With the Edit panel open and the Note tool, a click on added text is the added text's: no note is
|
||||
/// started under it, as before.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user