Snap: Shift and Command on a move
This commit is contained in:
parent
55ec575521
commit
a2451243ad
|
|
@ -72,6 +72,40 @@ fn tool_gesture_creates_one_undo_step() {
|
|||
assert_eq!(s.doc().unwrap().doc.spreads[0].items[0].bounds().x0, 36.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_move_does_not_take_the_other_axis() {
|
||||
use designcraft_tools::{PointerEvent, PointerKind};
|
||||
let mut s = session();
|
||||
s.execute("frame.create", &json!({"rect": [80.0, 100.0, 140.0, 140.0], "content": "unassigned"})).unwrap();
|
||||
s.execute("guide.add", &json!({"orientation": "horizontal", "position": 102.0, "page": 0})).unwrap();
|
||||
s.set_tool("selection");
|
||||
let v = ViewInfo::at_zoom(1.0);
|
||||
let shift = designcraft_tools::Mods { shift: true, ..Default::default() };
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 120.0).with_mods(shift), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 160.0, 121.0).with_mods(shift), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 160.0, 121.0).with_mods(shift), v).unwrap();
|
||||
let b = s.doc().unwrap().doc.spreads[0].items[0].bounds();
|
||||
assert!((b.y0 - 100.0).abs() < 1e-6, "locked axis jumped to {b:?}");
|
||||
assert!(b.x0 > 80.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_move_does_not_snap() {
|
||||
use designcraft_tools::{PointerEvent, PointerKind};
|
||||
let mut s = session();
|
||||
s.execute("frame.create", &json!({"rect": [80.0, 100.0, 140.0, 140.0], "content": "unassigned"})).unwrap();
|
||||
s.execute("guide.add", &json!({"orientation": "horizontal", "position": 102.0, "page": 0})).unwrap();
|
||||
s.set_tool("selection");
|
||||
let v = ViewInfo::at_zoom(1.0);
|
||||
let cmd = designcraft_tools::Mods { cmd: true, shift: false, ..Default::default() };
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 110.0, 120.0).with_mods(cmd), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 160.0, 121.0).with_mods(cmd), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 160.0, 121.0).with_mods(cmd), v).unwrap();
|
||||
let b = s.doc().unwrap().doc.spreads[0].items[0].bounds();
|
||||
assert!((b.y0 - 101.0).abs() < 1e-6, "cmd move snapped to {b:?}");
|
||||
assert!(b.x0 > 80.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pages_and_layers() {
|
||||
let mut s = session();
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use designcraft_doc::SpreadRef;
|
|||
use designcraft_geom::{Point, Rect, Vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey, rect_json, spread_json};
|
||||
use crate::{Action, Cursor, Gesture, Mods, Overlay, PointerEvent, PointerKind, SnapRequest, Tool, ToolContext, ToolKey, rect_json, spread_json};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Drag {
|
||||
|
|
@ -54,11 +54,13 @@ pub struct SelectionTool {
|
|||
drag: Drag,
|
||||
hover_handle: Option<usize>,
|
||||
guides: Vec<Overlay>,
|
||||
/// Selected item to toggle if this Shift press never becomes a drag.
|
||||
shift_release: Option<u64>,
|
||||
}
|
||||
|
||||
impl SelectionTool {
|
||||
pub fn new(direct: bool) -> Self {
|
||||
Self { direct, drag: Drag::None, hover_handle: None, guides: vec![] }
|
||||
Self { direct, drag: Drag::None, hover_handle: None, guides: vec![], shift_release: None }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -183,6 +185,7 @@ impl Tool for SelectionTool {
|
|||
vec![]
|
||||
}
|
||||
PointerKind::Down => {
|
||||
self.shift_release = None;
|
||||
if self.direct
|
||||
&& let Some((id, si, ai, handle)) = anchor_at(cx, p)
|
||||
{
|
||||
|
|
@ -221,7 +224,12 @@ impl Tool for SelectionTool {
|
|||
let id = if self.direct { id } else { cx.doc.top_level_of(id).unwrap_or(id) };
|
||||
let mut out = vec![];
|
||||
if ev.mods.shift {
|
||||
out.push(Action::Exec("selection.toggle".into(), json!({"id": id.0})));
|
||||
if cx.selection.contains(id) {
|
||||
// Keep it selected so a Shift-drag can constrain. A click still toggles, on release.
|
||||
self.shift_release = Some(id.0);
|
||||
} else {
|
||||
out.push(Action::Exec("selection.toggle".into(), json!({"id": id.0})));
|
||||
}
|
||||
} else if !cx.selection.contains(id) {
|
||||
out.push(Action::Exec("selection.set".into(), json!({"ids": [id.0], "content": self.direct})));
|
||||
}
|
||||
|
|
@ -240,6 +248,7 @@ impl Tool for SelectionTool {
|
|||
if (p - start).hypot() < cx.tol(3.0) {
|
||||
return vec![];
|
||||
}
|
||||
self.shift_release = None;
|
||||
if hit && !cx.selection.items.is_empty() {
|
||||
let sr = cx.selection.items.first().and_then(|i| cx.doc.find(*i)).map(|l| l.spread).unwrap_or(SpreadRef::Doc(0));
|
||||
self.drag = Drag::Move { start, origin_spread: sr, bounds0: cx.selection_bounds() };
|
||||
|
|
@ -254,23 +263,49 @@ impl Tool for SelectionTool {
|
|||
}
|
||||
Drag::Move { start, origin_spread, bounds0 } => {
|
||||
let mut d: Vec2 = p - start;
|
||||
let mut x_edges = [true, true, true];
|
||||
let mut y_edges = [true, true, true];
|
||||
if ev.mods.shift {
|
||||
if d.x.abs() > d.y.abs() {
|
||||
d.y = 0.0;
|
||||
y_edges = [false, false, false];
|
||||
} else {
|
||||
d.x = 0.0;
|
||||
x_edges = [false, false, false];
|
||||
}
|
||||
}
|
||||
self.guides.clear();
|
||||
// The movement in the spread's own coordinates (its view may be turned).
|
||||
let mut ds = cx.layout.delta_to_spread(origin_spread, d);
|
||||
let b0s = bounds0.map(|b| cx.layout.xf(origin_spread).inverse().transform_rect_bbox(b));
|
||||
if cx.snap.any()
|
||||
// Command suspends snapping. A Shift-locked axis is not offered.
|
||||
if !ev.mods.cmd
|
||||
&& cx.snap.any()
|
||||
&& let Some(b0) = b0s
|
||||
{
|
||||
let snap = crate::snap::snap_rect(cx, origin_spread, b0 + ds, &cx.selection.items);
|
||||
ds += snap.delta;
|
||||
self.guides = snap.guides;
|
||||
let hit = crate::snap::snap(
|
||||
cx,
|
||||
SnapRequest {
|
||||
spread: origin_spread,
|
||||
gesture: Gesture::Move,
|
||||
rect: b0 + ds,
|
||||
x_edges,
|
||||
y_edges,
|
||||
exclude: &cx.selection.items,
|
||||
copying: ev.mods.alt,
|
||||
lengths: [None, None],
|
||||
angle: None,
|
||||
radius: 0.0,
|
||||
pointer: cx.layout.to_spread(origin_spread, p),
|
||||
},
|
||||
);
|
||||
if x_edges != [false, false, false] {
|
||||
ds.x += hit.delta.x;
|
||||
}
|
||||
if y_edges != [false, false, false] {
|
||||
ds.y += hit.delta.y;
|
||||
}
|
||||
self.guides = hit.guides;
|
||||
}
|
||||
// Dragging to another spread moves the items there.
|
||||
let target = cx.layout.spread_at(p).map(|(s, _)| s).unwrap_or(origin_spread);
|
||||
|
|
@ -318,9 +353,14 @@ impl Tool for SelectionTool {
|
|||
},
|
||||
PointerKind::Up => {
|
||||
self.guides.clear();
|
||||
let release = self.shift_release.take();
|
||||
let d = std::mem::replace(&mut self.drag, Drag::None);
|
||||
match d {
|
||||
Drag::Move { .. } | Drag::Resize { .. } | Drag::Anchor { .. } | Drag::Rotate { .. } => vec![Action::Commit],
|
||||
Drag::Pending { .. } => match release {
|
||||
Some(id) => vec![Action::Exec("selection.toggle".into(), json!({"id": id}))],
|
||||
None => vec![],
|
||||
},
|
||||
Drag::Marquee { start, cur } => {
|
||||
let r = Rect::from_points(start, cur);
|
||||
// Items whose bounds intersect the marquee.
|
||||
|
|
@ -337,7 +377,7 @@ impl Tool for SelectionTool {
|
|||
}
|
||||
vec![Action::Exec("selection.set".into(), json!({"ids": ids, "add": ev.mods.shift}))]
|
||||
}
|
||||
_ => vec![],
|
||||
Drag::None => vec![],
|
||||
}
|
||||
}
|
||||
PointerKind::DoubleClick => {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
//!
|
||||
//! [`snap`] tries each pass on a free axis and keeps the first hit inside the snap zone.
|
||||
//! Ruler guides, margins, and columns draw nothing. Alignment draws [`Overlay::Guide`] in
|
||||
//! canvas coordinates. [`snap_rect`] is the older closest-target helper the selection tool
|
||||
//! still calls.
|
||||
//! canvas coordinates. [`snap_rect`] is the older closest-target helper.
|
||||
|
||||
use designcraft_doc::{ItemId, Orientation, SpreadRef};
|
||||
use designcraft_geom::{Point, Rect};
|
||||
|
|
@ -263,8 +262,9 @@ fn pass_guides(cx: &ToolContext, req: &SnapRequest<'_>, axis: Axis, tol: f64) ->
|
|||
/// Page and top-level item edges and centers. Draws one [`Overlay::Guide`] per winning axis.
|
||||
///
|
||||
/// Edges match edges only while `align_edges` is on. Centers match centers only while
|
||||
/// `align_centers` is on, and only when the request's middle flag is set. Hidden and excluded
|
||||
/// items are skipped. Parent items and group children are not targets.
|
||||
/// `align_centers` is on, and only when the request's middle flag is set. Hidden items are
|
||||
/// skipped. Excluded items are skipped unless `copying` is set, so an Alt-drag can meet the
|
||||
/// original. Parent items and group children are not targets.
|
||||
fn pass_align(cx: &ToolContext, req: &SnapRequest<'_>, axis: Axis, tol: f64) -> Option<AxisHit> {
|
||||
if !cx.snap.smart_guides || (!cx.snap.align_edges && !cx.snap.align_centers) {
|
||||
return None;
|
||||
|
|
@ -276,7 +276,7 @@ fn pass_align(cx: &ToolContext, req: &SnapRequest<'_>, axis: Axis, tol: f64) ->
|
|||
offer_box(&mut best, req.rect, flags, axis, page.bounds(), tol, cx.snap.align_edges, cx.snap.align_centers);
|
||||
}
|
||||
for item in &sp.items {
|
||||
if req.exclude.contains(&item.id) || item.hidden {
|
||||
if item.hidden || (!req.copying && req.exclude.contains(&item.id)) {
|
||||
continue;
|
||||
}
|
||||
offer_box(&mut best, req.rect, flags, axis, item.visible_bounds(), tol, cx.snap.align_edges, cx.snap.align_centers);
|
||||
|
|
@ -521,4 +521,35 @@ mod tests {
|
|||
);
|
||||
assert_eq!(hit.delta.x, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alt_copy_snaps_to_the_original() {
|
||||
let mut doc = Document::new(&NewDocument::default());
|
||||
let layer = doc.default_layer();
|
||||
let id = ItemId(doc.alloc());
|
||||
let item = Item::new(id, layer, Shape::Rectangle, shapes::rectangle(Rect::new(100.0, 40.0, 160.0, 80.0)));
|
||||
doc.insert_item(SpreadRef::Doc(0), item, None).unwrap();
|
||||
let cache = Cache::new();
|
||||
let layout = CanvasLayout::new(&doc, false);
|
||||
let sel = Selection::default();
|
||||
let mut cx = ctx_on(&doc, &sel, &cache, &layout);
|
||||
cx.snap.snap_to_guides = false;
|
||||
let hit = snap(
|
||||
&cx,
|
||||
SnapRequest {
|
||||
spread: SpreadRef::Doc(0),
|
||||
gesture: Gesture::Move,
|
||||
rect: Rect::new(162.0, 40.0, 222.0, 80.0),
|
||||
x_edges: [true, false, false],
|
||||
y_edges: [false, false, false],
|
||||
exclude: &[id],
|
||||
copying: true,
|
||||
lengths: [None, None],
|
||||
angle: None,
|
||||
radius: 0.0,
|
||||
pointer: Point::new(0.0, 0.0),
|
||||
},
|
||||
);
|
||||
assert!((hit.delta.x - -2.0).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue