mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 12:32:52 +00:00
refactor(ui): a press with Shift or ⌘ in Edit text is the objects', by their hit
`object_ui` took a Shift- or ⌘-press before anyone else, whoever owned it,
through its own inventory test at the press point, and declined a plain
press where that test found an object the router didn't (just outside a
picture's edge), so such a press started nothing at all.
- The page view reads the press's modifiers from `PagePointer`
(`modifiers`: those held at the frame's first primary press). With
Edit text, a press that adds (Shift, Ctrl or ⌘) is the objects' whole:
`object_ui::modified_hit` gives `Objects { Toggle(target) }` for the
object under it, or none (an area selection that adds), `Held` while
text being typed is open, and `Refused` on a page whose objects can't
be selected together. Only the press asks: hover and cursors are as
before.
- `object_ui` is told what the press is to its page (`PagePress`) and acts
on its own parts; a press nobody holds starts the area selection. The
inventory test and its decline go, and so does `blocked_page`: the held
press keeps the editor open until the button is up through its capture.
- A plain press beside a picture, within the 2 px the objects are found
in but outside the picture, now starts an area selection.
- The guard: `object_ui::input` no longer reads the router.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
6 files changed
+234
-112
No files matched your search
@@ -91,7 +91,15 @@ pub(super) fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut
|
||||
clip: ui.clip_rect(),
|
||||
};
|
||||
if let Some(o) = ptr.pressed().filter(|o| xf.rect.contains(*o)) {
|
||||
let at = hits.at(view, o);
|
||||
// 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
|
||||
&& 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);
|
||||
}
|
||||
// What this page has to do with the press: its owner's, the active tool's, or another page's.
|
||||
@@ -99,7 +107,7 @@ pub(super) fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut
|
||||
// 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;
|
||||
let under = pointer.filter(|p| wants_under && xf.rect.contains(*p)).map(|p| hits.at(view, p)).unwrap_or_default();
|
||||
let under = pointer.filter(|p| wants_under && xf.rect.contains(*p)).map(|p| hits.at(view, p, None)).unwrap_or_default();
|
||||
let released_over = match press {
|
||||
PagePress::Mine(owner) if resp.clicked() => under.iter().any(|h| h.owner == owner),
|
||||
_ => false,
|
||||
@@ -135,7 +143,7 @@ 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)
|
||||
};
|
||||
let on_objects = tool == QuickTool::EditText && can_modify && crate::object_ui::page_input(cx, view);
|
||||
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
|
||||
@@ -217,7 +225,7 @@ pub(super) fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut
|
||||
/// for hover painting. With `claim` (nothing drawn over the pages, such as the palette), its cursor
|
||||
/// and tip too, after the tools claimed theirs (the last claim on a layer stands).
|
||||
fn claim_under(hits: &PageHits<'_>, view: &mut DocView, p: Pos2, claim: bool) -> Option<crate::page_router::Hit> {
|
||||
let under = hits.at(view, p);
|
||||
let under = hits.at(view, p, None);
|
||||
if claim {
|
||||
for h in &under {
|
||||
view.cursor.claim_with_tip(h.layer, h.icon, h.tip.clone());
|
||||
@@ -411,9 +419,10 @@ struct PageHits<'a> {
|
||||
}
|
||||
|
||||
impl PageHits<'_> {
|
||||
/// The hits at `p`, bottom up ([`Self::hits`]).
|
||||
fn at(&self, view: &DocView, p: Pos2) -> Vec<crate::page_router::Hit> {
|
||||
self.hits(view, p, false)
|
||||
/// The hits at `p`, bottom up ([`Self::hits`]). For a press with Shift or ⌘ in Edit text, the
|
||||
/// page's `objects` ([`crate::object_ui::ObjectSelection::inventory`]), which take it whole.
|
||||
fn at(&self, view: &DocView, p: Pos2, objects: Option<&Result<Vec<pdfcraft_engine::EditableObject>, String>>) -> Vec<crate::page_router::Hit> {
|
||||
self.hits(view, p, false, objects)
|
||||
}
|
||||
|
||||
/// What a right-click at `p` is on: what lies on top there, as for a press, but with every
|
||||
@@ -429,17 +438,24 @@ impl PageHits<'_> {
|
||||
{
|
||||
return Some(press.owner);
|
||||
}
|
||||
top(self.hits(view, p, true), QuickTool::Select).map(|h| h.owner)
|
||||
top(self.hits(view, p, true, None), QuickTool::Select).map(|h| h.owner)
|
||||
}
|
||||
|
||||
/// The hits at `p`, bottom up: links, comments; Edit text's pictures under its paragraphs (text
|
||||
/// on top of artwork takes the press), added content, the objects selected together and the
|
||||
/// open paragraph's frame, the selected picture's grips over them all; the camera tool's corners;
|
||||
/// then form fields (Prepare's, being moved, over the comments; filled ones under added content
|
||||
/// and the comments that move). In crop mode, only the picture being cropped.
|
||||
/// and the comments that move). For a press with Shift or ⌘ in Edit text, only the objects
|
||||
/// (`objects`, the page's); in crop mode, only the picture being cropped.
|
||||
/// For a right-click (`context`), comments by [`comments::context_hit`], and no camera corner (it
|
||||
/// has no menu).
|
||||
fn hits(&self, view: &DocView, p: Pos2, context: bool) -> Vec<crate::page_router::Hit> {
|
||||
fn hits(
|
||||
&self,
|
||||
view: &DocView,
|
||||
p: Pos2,
|
||||
context: bool,
|
||||
objects: Option<&Result<Vec<pdfcraft_engine::EditableObject>, String>>,
|
||||
) -> Vec<crate::page_router::Hit> {
|
||||
let PageHits {
|
||||
page,
|
||||
xf,
|
||||
@@ -459,6 +475,10 @@ impl PageHits<'_> {
|
||||
images,
|
||||
clip,
|
||||
} = *self;
|
||||
// A press with Shift or ⌘ is the objects', crop mode's page included (`object_ui::modified_hit`).
|
||||
if let Some(objects) = objects {
|
||||
return crate::object_ui::modified_hit(view, xf, info, page, objects, clip, p).into_iter().collect();
|
||||
}
|
||||
// Crop mode holds every press on its page (`edit_text_ui::crop_hit`).
|
||||
if tool == QuickTool::EditText && can_modify && view.image_selection.as_ref().is_some_and(|s| s.page == page && s.crop.is_some()) {
|
||||
return crate::edit_text_ui::crop_hit(xf, page, info, images, view, p).into_iter().collect();
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! Dialogs are drawn after the pages, so on the frame one opens egui has not met its layer yet:
|
||||
//! the reading also takes whether one is open.
|
||||
|
||||
use egui::{Event, PointerButton, Pos2, Rect};
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2, Rect};
|
||||
|
||||
/// A primary-button press or release, and where it happened.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
@@ -46,13 +46,16 @@ pub(crate) struct PagePointer {
|
||||
pub primary_down: bool,
|
||||
/// Some button is down, wherever it was pressed.
|
||||
pub any_down: bool,
|
||||
/// The modifier keys held at this frame's first primary press, or else now: a press with Shift
|
||||
/// or ⌘ adds to a selection.
|
||||
pub modifiers: Modifiers,
|
||||
}
|
||||
|
||||
impl PagePointer {
|
||||
/// The pointer for `resp`, the content widget that holds every page, allocated in `ui`, while
|
||||
/// a dialog is open over the window or not (`modal`).
|
||||
pub(crate) fn read(ui: &egui::Ui, resp: &egui::Response, modal: bool) -> Self {
|
||||
let (at, origin, edges, released, primary_down, any_down) = ui.input(|i| {
|
||||
let (at, origin, edges, released, primary_down, any_down, modifiers) = ui.input(|i| {
|
||||
(
|
||||
i.pointer.interact_pos(),
|
||||
i.pointer.press_origin(),
|
||||
@@ -60,6 +63,7 @@ impl PagePointer {
|
||||
last_release(&i.events),
|
||||
i.pointer.primary_down(),
|
||||
i.pointer.any_down(),
|
||||
press_modifiers(&i.events).unwrap_or(i.modifiers),
|
||||
)
|
||||
});
|
||||
// egui's own answer to "do the pages hold a press?": from the press on until the frame
|
||||
@@ -75,9 +79,9 @@ impl PagePointer {
|
||||
resp.dragged() || resp.drag_stopped() || (!modal && (resp.interact_pointer_pos().is_some() || press_origin.is_some_and(|p| on_top(&p))));
|
||||
let pos = at.filter(|p| held || on_top(p));
|
||||
if !held {
|
||||
return Self { pos, origin: None, edges: Vec::new(), released: None, primary_down, any_down };
|
||||
return Self { pos, origin: None, edges: Vec::new(), released: None, primary_down, any_down, modifiers };
|
||||
}
|
||||
Self { pos, origin: press_origin, edges, released, primary_down, any_down }
|
||||
Self { pos, origin: press_origin, edges, released, primary_down, any_down, modifiers }
|
||||
}
|
||||
|
||||
/// Where the pointer is, when it is inside `r`.
|
||||
@@ -108,6 +112,14 @@ fn first_press(edges: &[Edge]) -> Option<Pos2> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The modifier keys held at the first primary press among `events`.
|
||||
fn press_modifiers(events: &[Event]) -> Option<Modifiers> {
|
||||
events.iter().find_map(|e| match e {
|
||||
Event::PointerButton { button: PointerButton::Primary, pressed: true, modifiers, .. } => Some(*modifiers),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Where the last primary release among `events` was, however many came before it.
|
||||
fn last_release(events: &[Event]) -> Option<Pos2> {
|
||||
events.iter().rev().find_map(|e| match e {
|
||||
|
||||
@@ -573,7 +573,7 @@ const ROUTER_READS: [&str; 6] =
|
||||
/// 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); 8] = [
|
||||
const ROUTER_READERS: [(&str, &str, &str); 7] = [
|
||||
(
|
||||
"canvas/page_input.rs",
|
||||
"page_tool_input",
|
||||
@@ -585,7 +585,6 @@ const ROUTER_READERS: [(&str, &str, &str); 8] = [
|
||||
("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)"),
|
||||
("tools/object_ui.rs", "input", "the objects selected together, until they are told (4b)"),
|
||||
];
|
||||
|
||||
/// What ignores the press the pages hold until the button is up.
|
||||
@@ -597,7 +596,7 @@ const PRESS_IGNORERS: [(&str, &str, &str); 1] =
|
||||
|
||||
/// The module hit tests, called by path: a press handler calls none of them (HIT_TESTERS says who
|
||||
/// may).
|
||||
const PAGE_HIT_CALLS: [&str; 12] = [
|
||||
const PAGE_HIT_CALLS: [&str; 13] = [
|
||||
"link_ui::hit(",
|
||||
"prepare::hit(",
|
||||
"forms_ui::hit(",
|
||||
@@ -609,6 +608,7 @@ const PAGE_HIT_CALLS: [&str; 12] = [
|
||||
"edit_text_ui::picture_handle_hit(",
|
||||
"edit_text_ui::crop_hit(",
|
||||
"object_ui::hit(",
|
||||
"object_ui::modified_hit(",
|
||||
"content_ui::hit(",
|
||||
];
|
||||
|
||||
@@ -695,12 +695,9 @@ fn page_modules_act_on_a_press_only_through_the_router() {
|
||||
"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"),
|
||||
("tools/object_ui.rs", "input"),
|
||||
] {
|
||||
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) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
//! the active tool's, or none of its business.
|
||||
|
||||
use egui::CursorIcon;
|
||||
use pdfcraft_engine::ObjectTarget;
|
||||
|
||||
use crate::QuickTool;
|
||||
use crate::page_cursor::Layer;
|
||||
@@ -73,6 +74,15 @@ pub(crate) enum ObjectsPart {
|
||||
Selection,
|
||||
/// The frame drawn around the paragraph open for typing: a click selects the box, a drag moves it.
|
||||
Frame,
|
||||
/// A press with Shift or ⌘ (Ctrl): it adds the object under it to the objects selected
|
||||
/// together, or takes it out of them, or on none starts an area selection that adds to them.
|
||||
Toggle(Option<ObjectTarget>),
|
||||
/// A press with Shift or ⌘ while text being typed is open (a new box, or a paragraph with
|
||||
/// changes), which must be finished or discarded first: it is held until the button is up, and
|
||||
/// selects nothing.
|
||||
Held,
|
||||
/// A press with Shift or ⌘ on a page whose objects can't be selected together: it says so.
|
||||
Refused,
|
||||
}
|
||||
|
||||
/// The part of a paragraph a hit is on.
|
||||
|
||||
@@ -9,7 +9,7 @@ use pdfcraft_render::DocInfo;
|
||||
|
||||
use crate::canvas::{DocView, PageToolContext, PageXform};
|
||||
use crate::page_cursor::Layer;
|
||||
use crate::page_router::{Hit, ObjectsPart, Owner};
|
||||
use crate::page_router::{Hit, ObjectsPart, Owner, PagePress};
|
||||
|
||||
const BLUE: Color32 = Color32::from_rgb(0x14, 0x73, 0xE6);
|
||||
|
||||
@@ -24,7 +24,6 @@ pub(crate) struct ObjectSelection {
|
||||
pending: Option<Vec<EditableObject>>,
|
||||
pub notice: Option<String>,
|
||||
pub hold_editor: bool,
|
||||
blocked_page: Option<usize>,
|
||||
input_blocked: bool,
|
||||
/// A paragraph (page, block) to select once the document is at hand: Esc in the inline
|
||||
/// editor asks for it, and the next `prepare` selects it.
|
||||
@@ -40,10 +39,9 @@ struct Gesture {
|
||||
}
|
||||
|
||||
impl ObjectSelection {
|
||||
/// Clear any object-selection gesture and the page it was blocking.
|
||||
/// Clear any object-selection gesture.
|
||||
pub(crate) fn cancel_pointer_gesture(&mut self) {
|
||||
self.gesture = None;
|
||||
self.blocked_page = None;
|
||||
}
|
||||
|
||||
pub fn count(&self) -> usize {
|
||||
@@ -56,7 +54,6 @@ impl ObjectSelection {
|
||||
self.input_blocked = blocked;
|
||||
if blocked {
|
||||
self.gesture = None;
|
||||
self.blocked_page = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +62,6 @@ impl ObjectSelection {
|
||||
self.selected.clear();
|
||||
self.gesture = None;
|
||||
self.pending = None;
|
||||
self.blocked_page = None;
|
||||
self.select_paragraph = None;
|
||||
}
|
||||
|
||||
@@ -139,7 +135,9 @@ impl ObjectSelection {
|
||||
Edit::MoveObjects { page, objects: self.selected.iter().map(|o| o.target).collect(), offset }
|
||||
}
|
||||
|
||||
fn inventory(&mut self, doc: &Document, page: usize) -> Result<Vec<EditableObject>, String> {
|
||||
/// The objects of `page` that can be selected together, as `doc.editable_objects` lists them, or
|
||||
/// why they can't be.
|
||||
pub(crate) fn inventory(&mut self, doc: &Document, page: usize) -> Result<Vec<EditableObject>, String> {
|
||||
if self.cache.len() >= 8 && !self.cache.contains_key(&page) {
|
||||
self.cache.clear();
|
||||
}
|
||||
@@ -151,18 +149,9 @@ fn screen(xf: &PageXform, info: &DocInfo, page: usize, object: &EditableObject)
|
||||
xf.user_rect(info, page, object.rect.map(|v| v as f32))
|
||||
}
|
||||
|
||||
fn modified(ui: &egui::Ui) -> bool {
|
||||
ui.input(|i| {
|
||||
let modifiers = i
|
||||
.events
|
||||
.iter()
|
||||
.find_map(|e| match e {
|
||||
egui::Event::PointerButton { button: egui::PointerButton::Primary, pressed: true, modifiers, .. } => Some(*modifiers),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(i.modifiers);
|
||||
modifiers.shift || modifiers.ctrl || modifiers.command
|
||||
})
|
||||
/// Whether `modifiers` make a press add to the objects selected together, or take from them.
|
||||
pub(crate) fn adds(modifiers: egui::Modifiers) -> bool {
|
||||
modifiers.shift || modifiers.ctrl || modifiers.command
|
||||
}
|
||||
|
||||
fn single(view: &DocView, page: usize) -> Option<ObjectTarget> {
|
||||
@@ -195,36 +184,84 @@ pub(crate) fn hit(view: &DocView, xf: &PageXform, info: &DocInfo, page: usize, o
|
||||
open.filter(|r| on_frame(r.shrink(2.0), p)).map(|_| Hit::new(Owner::Objects { part: ObjectsPart::Frame }, Layer::Handle, egui::CursorIcon::Move))
|
||||
}
|
||||
|
||||
/// Returns true while group selection owns the interaction. Ordinary clicks still reach the
|
||||
/// single-object text/resize tools. A text draft is finished or discarded through its editor.
|
||||
pub(crate) fn page_input(cx: PageToolContext<'_>, view: &mut DocView) -> bool {
|
||||
let PageToolContext { ui, resp, xf, ptr, page, .. } = cx;
|
||||
/// The object under `p` among the page's `objects`: added content over text over pictures, so a
|
||||
/// page-sized background picture does not hide editable labels or added notes, and within each
|
||||
/// kind the last, as the single-item tools take it.
|
||||
fn object_at(xf: &PageXform, info: &DocInfo, page: usize, objects: &[EditableObject], p: Pos2) -> Option<ObjectTarget> {
|
||||
[ObjectKind::Added, ObjectKind::Text, ObjectKind::Image]
|
||||
.into_iter()
|
||||
.find_map(|kind| objects.iter().rev().find(|o| o.target.kind == kind && screen(xf, info, page, o).expand(2.0).contains(p)))
|
||||
.map(|o| o.target)
|
||||
}
|
||||
|
||||
/// What a press with Shift or ⌘ at `p` takes on `page` in Edit text, whatever lies there: the
|
||||
/// object under it among the page's `objects` ([`ObjectSelection::inventory`]), which it adds to
|
||||
/// the objects selected together or takes out of them, or none, where it starts an area selection
|
||||
/// that adds to them. Held while text being typed is open, and refused on a page whose objects
|
||||
/// can't be selected together. `None` off the page and outside the pages' visible area (`clip`).
|
||||
pub(crate) fn modified_hit(
|
||||
view: &DocView,
|
||||
xf: &PageXform,
|
||||
info: &DocInfo,
|
||||
page: usize,
|
||||
objects: &Result<Vec<EditableObject>, String>,
|
||||
clip: Rect,
|
||||
p: Pos2,
|
||||
) -> Option<Hit> {
|
||||
if !xf.rect.contains(p) || !clip.contains(p) {
|
||||
return None;
|
||||
}
|
||||
let part = if view.content.draft.is_some() || view.line_editor.as_ref().is_some_and(|e| e.has_changes()) {
|
||||
ObjectsPart::Held
|
||||
} else {
|
||||
match objects {
|
||||
Ok(objects) => ObjectsPart::Toggle(object_at(xf, info, page, objects, p)),
|
||||
Err(_) => ObjectsPart::Refused,
|
||||
}
|
||||
};
|
||||
Some(Hit::new(Owner::Objects { part }, Layer::Editor, egui::CursorIcon::Default))
|
||||
}
|
||||
|
||||
/// The objects selected together, on one page, told what the press is to it (`press`). Returns
|
||||
/// true while they own the interaction: while input is blocked, while a press with Shift or ⌘ is
|
||||
/// held ([`modified_hit`]), and while objects on this page are selected or a gesture of theirs
|
||||
/// runs. Ordinary presses on a paragraph, a picture or added content still reach their own tools.
|
||||
/// A text draft is finished or discarded through its editor.
|
||||
pub(crate) fn page_input(cx: PageToolContext<'_>, press: PagePress, view: &mut DocView) -> bool {
|
||||
let PageToolContext { ui, resp, ptr, .. } = cx;
|
||||
if view.objects.input_blocked {
|
||||
return true;
|
||||
}
|
||||
if resp.context_menu_opened() || egui::Popup::is_any_open(ui.ctx()) {
|
||||
return false;
|
||||
}
|
||||
if view.content.draft.is_some() || view.line_editor.as_ref().is_some_and(|e| e.has_changes()) {
|
||||
let blocked = view.objects.blocked_page == Some(page);
|
||||
let modifier_press = ptr.pressed().is_some() && modified(ui) && ptr.pos.is_some_and(|p| xf.rect.contains(p) && ui.clip_rect().contains(p));
|
||||
if blocked || modifier_press {
|
||||
match press {
|
||||
// Text being typed keeps the keyboard, and its editor stays open, until the button is up.
|
||||
PagePress::Mine(Owner::Objects { part: ObjectsPart::Held }) => {
|
||||
view.objects.hold_editor = true;
|
||||
view.objects.blocked_page = ptr.primary_down.then_some(page);
|
||||
if modifier_press {
|
||||
if ptr.pressed().is_some() {
|
||||
view.objects.notice = Some(tl!("Finish or discard the text being edited before selecting several objects.").into());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
PagePress::Mine(Owner::Objects { part: ObjectsPart::Refused }) => {
|
||||
if ptr.pressed().is_some() {
|
||||
view.objects.notice = Some(tl!("This page contains objects that cannot be selected together.").into());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if view.content.draft.is_some() || view.line_editor.as_ref().is_some_and(|e| e.has_changes()) {
|
||||
return false;
|
||||
}
|
||||
let mut selection = std::mem::take(&mut view.objects);
|
||||
let result = input(&mut selection, cx, view);
|
||||
let result = input(&mut selection, cx, press, view);
|
||||
view.objects = selection;
|
||||
result
|
||||
}
|
||||
|
||||
fn input(s: &mut ObjectSelection, cx: PageToolContext<'_>, view: &mut DocView) -> bool {
|
||||
fn input(s: &mut ObjectSelection, cx: PageToolContext<'_>, press: PagePress, view: &mut DocView) -> bool {
|
||||
let PageToolContext { ui, resp, xf, ptr, page, doc, .. } = cx;
|
||||
let pointer = ptr.pos;
|
||||
let (pressed, down) = (ptr.pressed().is_some(), ptr.primary_down);
|
||||
@@ -285,70 +322,63 @@ fn input(s: &mut ObjectSelection, cx: PageToolContext<'_>, view: &mut DocView) -
|
||||
if !pressed {
|
||||
return s.page == Some(page) && !s.selected.is_empty();
|
||||
}
|
||||
let objects = match s.inventory(doc, page) {
|
||||
Ok(objects) => objects,
|
||||
Err(_) => {
|
||||
if modified(ui) {
|
||||
s.notice = Some(tl!("This page contains objects that cannot be selected together.").into());
|
||||
let Ok(objects) = s.inventory(doc, page) else { return false };
|
||||
// What the press was on ([`hit`], [`modified_hit`]).
|
||||
match press {
|
||||
// With Shift or ⌘: the object it was on joins the selection or leaves it, and on none an
|
||||
// area selection adds to it.
|
||||
PagePress::Mine(Owner::Objects { part: ObjectsPart::Toggle(target) }) => {
|
||||
if s.page != Some(page) {
|
||||
s.clear();
|
||||
s.page = Some(page);
|
||||
if let Some(single) = single(view, page)
|
||||
&& let Some(object) = objects.iter().find(|o| o.target == single)
|
||||
{
|
||||
s.selected.push(object.clone());
|
||||
}
|
||||
}
|
||||
match target.and_then(|t| objects.iter().find(|o| o.target == t)) {
|
||||
Some(object) => {
|
||||
if let Some(index) = s.selected.iter().position(|o| o.target == object.target) {
|
||||
s.selected.remove(index);
|
||||
} else if s.selected.len() < MAX_MOVE_OBJECTS {
|
||||
s.selected.push(object.clone());
|
||||
} else {
|
||||
s.notice = Some(tl!("Select at most 1000 objects at a time.").into());
|
||||
}
|
||||
}
|
||||
None => s.gesture = Some(Gesture { start: p, now: p, marquee: true, additive: true }),
|
||||
}
|
||||
leave_single(view);
|
||||
view.current = page;
|
||||
return true;
|
||||
}
|
||||
// The selection moves.
|
||||
PagePress::Mine(Owner::Objects { part: ObjectsPart::Selection }) if !resp.double_clicked() => {
|
||||
s.gesture = Some(Gesture { start: p, now: p, marquee: false, additive: false });
|
||||
return true;
|
||||
}
|
||||
// The frame of the paragraph open for typing selects its box (for the arrow keys) and
|
||||
// moves it.
|
||||
PagePress::Mine(Owner::Objects { part: ObjectsPart::Frame }) => {
|
||||
if let Some(block) = view.line_editor.as_ref().filter(|e| e.page == page && !e.has_changes()).map(|e| e.block)
|
||||
&& let Some(object) = objects.iter().find(|o| o.target == ObjectTarget { kind: ObjectKind::Text, index: block })
|
||||
{
|
||||
s.clear();
|
||||
s.page = Some(page);
|
||||
s.selected.push(object.clone());
|
||||
s.gesture = Some(Gesture { start: p, now: p, marquee: false, additive: false });
|
||||
leave_single(view);
|
||||
view.current = page;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let additive = modified(ui);
|
||||
// A page-sized background image must not hide editable labels or added notes.
|
||||
// Within each kind, prefer the last object as the ordinary single-item tools do.
|
||||
let hit = [ObjectKind::Added, ObjectKind::Text, ObjectKind::Image]
|
||||
.into_iter()
|
||||
.find_map(|kind| objects.iter().rev().find(|o| o.target.kind == kind && screen(xf, info, page, o).expand(2.0).contains(p)));
|
||||
if additive {
|
||||
if s.page != Some(page) {
|
||||
s.clear();
|
||||
s.page = Some(page);
|
||||
if let Some(target) = single(view, page)
|
||||
&& let Some(object) = objects.iter().find(|o| o.target == target)
|
||||
{
|
||||
s.selected.push(object.clone());
|
||||
}
|
||||
}
|
||||
if let Some(object) = hit {
|
||||
if let Some(index) = s.selected.iter().position(|o| o.target == object.target) {
|
||||
s.selected.remove(index);
|
||||
} else if s.selected.len() < MAX_MOVE_OBJECTS {
|
||||
s.selected.push(object.clone());
|
||||
} else {
|
||||
s.notice = Some(tl!("Select at most 1000 objects at a time.").into());
|
||||
}
|
||||
} else {
|
||||
s.gesture = Some(Gesture { start: p, now: p, marquee: true, additive: true });
|
||||
}
|
||||
leave_single(view);
|
||||
view.current = page;
|
||||
return true;
|
||||
}
|
||||
// What the press was on ([`hit`]): the selection moves; the frame of the paragraph open for
|
||||
// typing selects its box (for the arrow keys) and moves it.
|
||||
let owner = view.router.owner(page);
|
||||
if owner == Some(Owner::Objects { part: ObjectsPart::Selection }) && !resp.double_clicked() {
|
||||
s.gesture = Some(Gesture { start: p, now: p, marquee: false, additive: false });
|
||||
return true;
|
||||
}
|
||||
if owner == Some(Owner::Objects { part: ObjectsPart::Frame })
|
||||
&& let Some(block) = view.line_editor.as_ref().filter(|e| e.page == page && !e.has_changes()).map(|e| e.block)
|
||||
&& let Some(object) = objects.iter().find(|o| o.target == ObjectTarget { kind: ObjectKind::Text, index: block })
|
||||
{
|
||||
s.clear();
|
||||
s.page = Some(page);
|
||||
s.selected.push(object.clone());
|
||||
s.gesture = Some(Gesture { start: p, now: p, marquee: false, additive: false });
|
||||
leave_single(view);
|
||||
view.current = page;
|
||||
return true;
|
||||
_ => {}
|
||||
}
|
||||
s.clear();
|
||||
// A press on something (a paragraph, its edge reaching past its box #2276, a picture, added
|
||||
// content) is its own; only one on the empty page starts an area selection.
|
||||
if hit.is_some() || view.router.owner(page).is_some() {
|
||||
// content) is its own; only one nobody holds starts an area selection.
|
||||
if press != PagePress::Free {
|
||||
return false;
|
||||
}
|
||||
leave_single(view);
|
||||
|
||||
@@ -385,3 +385,56 @@ fn modifier_selection_picks_foreground_text_and_added_items_over_background_artw
|
||||
assert!(new.rect.iter().enumerate().all(|(i, value)| (value - old.rect[i] - shift[i % 2]).abs() < 0.01), "{old:?} -> {new:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A press with Shift while a paragraph with changes is open is held until the button is up: it
|
||||
/// says why, the paragraph keeps its text, and nothing under the press is moved or selected, not
|
||||
/// even once the press turns into a drag.
|
||||
#[test]
|
||||
fn a_shift_click_with_a_changed_paragraph_open_shows_the_notice_and_moves_nothing() {
|
||||
let mut h = harness();
|
||||
let before = objects(&h);
|
||||
let p = at(&h, 55.0, 70.0);
|
||||
click(&mut h, p, Modifiers::NONE);
|
||||
h.state_mut().views[0].line_editor.as_mut().unwrap().text = "A draft still being edited".into();
|
||||
let (a, b) = (at(&h, 60.0, 145.0), at(&h, 120.0, 200.0));
|
||||
h.hover_at(a);
|
||||
h.run_steps(2);
|
||||
h.event_modifiers(
|
||||
egui::Event::PointerButton { pos: a, button: egui::PointerButton::Primary, pressed: true, modifiers: Modifiers::SHIFT },
|
||||
Modifiers::SHIFT,
|
||||
);
|
||||
h.run_steps(2);
|
||||
let notice = "Finish or discard the text being edited before selecting several objects.";
|
||||
assert!(h.state().toast.as_ref().is_some_and(|t| t.0.contains(notice)), "the notice says why");
|
||||
for k in 1..=3 {
|
||||
h.hover_at(a + (b - a) * (k as f32 / 3.0));
|
||||
h.run_steps(2);
|
||||
}
|
||||
h.drop_at(b);
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().views[0].line_editor.as_ref().map(|e| e.text.as_str()), Some("A draft still being edited"));
|
||||
assert_eq!(objects(&h), before, "nothing moved");
|
||||
assert!(h.state().views[0].image_selection.is_none() && h.state().views[0].content.selected.is_none(), "nothing selected");
|
||||
assert!(
|
||||
h.query_all_by_label_contains("object selected").next().is_none() && h.query_all_by_label_contains("objects selected").next().is_none(),
|
||||
"no objects selected"
|
||||
);
|
||||
}
|
||||
|
||||
/// A press beside a picture, within the reach the objects selected together are found in but
|
||||
/// outside the picture itself, is nobody's: it starts an area selection. It used to start nothing.
|
||||
#[test]
|
||||
fn a_press_just_outside_a_picture_starts_an_area_selection() {
|
||||
let mut h = harness();
|
||||
// The figure is x 40..100, y 130..160 from the page's top; the added note x 130..230, y 130..144.
|
||||
let (a, b) = (at(&h, 38.5, 140.0), at(&h, 235.0, 175.0));
|
||||
h.hover_at(a);
|
||||
h.run_steps(2);
|
||||
h.drag_at(a);
|
||||
h.run_steps(2);
|
||||
h.hover_at(b);
|
||||
h.run_steps(2);
|
||||
h.drop_at(b);
|
||||
h.run_steps(4);
|
||||
h.get_by_label("2 objects selected");
|
||||
}
|
||||
Reference in new issue
Block a user