diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index cba5fc317..033e1a97c 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -683,6 +683,412 @@ fn positive_pad(v: f64) -> f64 { if v.is_finite() && v > 0.0 { v } else { 0.0 } } +impl SelectionTool { + fn pointer_down(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec { + let p = ev.pos; + + self.shift_release = None; + self.key_release = None; + // Type on a path: a bracket moves where the text starts or ends. + if !ev.mods.shift + && let Some((id, end)) = self.bracket_at(cx, p) + { + self.drag = Drag::Bracket { id, end }; + return vec![Action::Begin("Move Bracket".into())]; + } + if self.direct + && let Some((id, si, ai, handle)) = anchor_at(cx, p) + { + let at = anchor_spread_point(cx, id, si, ai); + self.drag = Drag::Anchor { id, si, ai, handle, start: p, at }; + return vec![Action::Begin(if handle.is_some() { "Move Direction Handle".into() } else { "Move Anchor".into() })]; + } + // A selected placed graphic's own handles resize it inside its frame (the + // frame's anchors, under Direct Selection, come first). + if let Some((id, h)) = content_handle_at(cx, p) + && let (Some(loc), Some((_, b))) = (cx.doc.find(id), selected_graphic(cx)) + { + let turns = cx.layout.slot(loc.spread).map_or(0, |s| s.rotation as usize); + let handle = (h + 8 - 2 * turns) % 8; + let from = cx.layout.xf(loc.spread).inverse().transform_rect_bbox(b); + self.drag = Drag::ContentResize { id, handle, from, spread: loc.spread }; + return vec![Action::Begin("Resize Content".into())]; + } + if let Some((frame, corner)) = self.diamond_at(cx, p) { + if ev.mods.alt { + self.drag = Drag::None; + let opts = frame.cycled(corner, ev.mods.shift); + return vec![Action::Exec("object.cornerOptions".into(), corners::params(frame.id, &opts))]; + } + self.drag = Drag::Corner { frame, corner, start: p }; + return vec![Action::Begin("Corner Options".into())]; + } + if let Some(frame) = self.widget_at(cx, p) { + self.live = Some(frame.id); + self.drag = Drag::None; + return vec![Action::Handled]; + } + if !self.direct + && handle_at(cx, p).is_none() + && let Some(center) = rotate_zone(cx, p) + { + self.drag = Drag::Rotate { center, start_angle: (p - center).atan2(), start_rotation: crate::snap::reference_rotation(cx) }; + return vec![Action::Begin("Rotate".into())]; + } + if let Some(h) = handle_at(cx, p).filter(|_| !self.direct) + && let (Some(b), Some(first)) = (cx.selection_bounds(), cx.selection.items.first()) + && let Some(loc) = cx.doc.find(*first) + { + // On a turned spread the canvas handle is another one of the spread's. + let turns = cx.layout.slot(loc.spread).map_or(0, |s| s.rotation as usize); + let handle = (h + 8 - 2 * turns) % 8; + self.drag = Drag::Resize { handle, start: p, from: cx.layout.xf(loc.spread).inverse().transform_rect_bbox(b), spread: loc.spread }; + return vec![Action::Begin("Resize".into())]; + } + // The content grabber: select the frame's content; a drag moves it. + if !self.direct + && !ev.mods.shift + && let Some(id) = grabber_at(cx, p) + { + self.drag = Drag::Pending { start: p, hit: true, content: true }; + if cx.selection.items == [id] && cx.selection.content { + return vec![]; + } + return vec![Action::Exec("selection.set".into(), json!({"ids": [id.0], "content": true}))]; + } + // Cmd+Shift-click a parent item on a page: override it there and select the copy. + if ev.mods.cmd + && ev.mods.shift + && cx.hit(p).is_none() + && let Some((page, id)) = cx.hit_parent_on_page(p) + { + self.drag = Drag::Pending { start: p, hit: true, content: false }; + return vec![Action::Exec("layout.overrideParentItems".into(), json!({"page": page, "ids": [id.0]}))]; + } + match cx.hit(p) { + Some((sr, id)) => { + let id = if self.direct { id } else { cx.doc.top_level_of(id).unwrap_or(id) }; + let mut out = vec![]; + if ev.mods.shift { + 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) || (self.direct && !cx.selection.content) { + out.push(Action::Exec("selection.set".into(), json!({"ids": [id.0], "content": self.direct}))); + } else if !self.direct && cx.selection.items.len() > 1 { + // InDesign: a click on one of several selected objects makes it the + // key object, on release so that a drag still moves them all. + self.key_release = Some(id.0); + } + let content = self.direct || on_selected_content(cx, p); + self.drag = Drag::Pending { start: p, hit: true, content }; + let _ = sr; + out + } + None => { + self.live = None; + self.drag = Drag::Pending { start: p, hit: false, content: false }; + if ev.mods.shift { vec![] } else { vec![Action::Exec("selection.set".into(), json!({"ids": []}))] } + } + } + } + + fn pointer_drag_move(&mut self, cx: &ToolContext, ev: &PointerEvent, drag: Drag) -> Vec { + let Drag::Move { start, origin_spread, bounds0, exclude, content } = drag else { return vec![] }; + let p = ev.pos; + + let mut d: Vec2 = p - start; + let mut screen_x_locked = false; + let mut screen_y_locked = false; + if ev.mods.shift { + if d.x.abs() > d.y.abs() { + d.y = 0.0; + screen_y_locked = true; + } else { + d.x = 0.0; + screen_x_locked = true; + } + } + // Snap on the spread under the pointer. Shift uses that spread's view rotation. + // Content stays in its frame, on the frame's spread. + let target = if content { origin_spread } else { cx.layout.spread_at(p).map(|(s, _)| s).unwrap_or(origin_spread) }; + // 180 degrees keeps the screen axis. 90 and 270 swap it into the other spread axis. + let odd_turn = cx.layout.slot(target).is_some_and(|slot| slot.rotation % 2 == 1); + let spread_x_locked = if odd_turn { screen_y_locked } else { screen_x_locked }; + let spread_y_locked = if odd_turn { screen_x_locked } else { screen_y_locked }; + let x_edges = if spread_x_locked { [false; 3] } else { [true; 3] }; + let y_edges = if spread_y_locked { [false; 3] } else { [true; 3] }; + self.guides.clear(); + // The movement in the origin spread's 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)); + let q0 = b0s.map_or(Point::ORIGIN, |b| Point::new(b.x0, b.y0)); + // Same corner conversion transform.move already uses when the spread changes. + let mut landed = cx.layout.to_spread(target, cx.layout.to_canvas(origin_spread, q0 + ds)); + // Command suspends snapping. A Shift-locked spread axis is not offered. + if !ev.mods.cmd + && cx.snap.any() + && let Some(b0) = b0s + { + let proposed = if target == origin_spread { + b0 + ds + } else { + let shift = landed - q0; + Rect::new(b0.x0 + shift.x, b0.y0 + shift.y, b0.x1 + shift.x, b0.y1 + shift.y) + }; + let hit = crate::snap::snap( + cx, + SnapRequest { + spread: target, + gesture: Gesture::Move, + rect: proposed, + x_edges, + y_edges, + exclude: &exclude, + exclude_guides: &[], + copying: ev.mods.alt && !content, + lengths: [None, None], + angle: None, + radius: 0.0, + pointer: cx.layout.to_spread(target, p), + }, + ); + if target == origin_spread { + if x_edges != [false, false, false] && hit.delta.x.is_finite() { + ds.x += hit.delta.x; + } + if y_edges != [false, false, false] && hit.delta.y.is_finite() { + ds.y += hit.delta.y; + } + } else { + if x_edges != [false, false, false] && hit.delta.x.is_finite() { + landed.x += hit.delta.x; + } + if y_edges != [false, false, false] && hit.delta.y.is_finite() { + landed.y += hit.delta.y; + } + } + self.guides = hit.guides; + } + // Same spread: dx, dy are the origin spread translation. Crossing: the corner + // conversion, so the reparented item lands on the snapped target point. + let (dx, dy) = if target == origin_spread { + (ds.x, ds.y) + } else { + let dt = landed - q0; + (dt.x, dt.y) + }; + if content { + let ids: Vec = exclude.iter().map(|i| i.0).collect(); + return vec![Action::Preview("transform.move".into(), json!({"dx": dx, "dy": dy, "ids": ids, "content": true}))]; + } + let mut params = json!({"dx": dx, "dy": dy, "copy": ev.mods.alt}); + if target != origin_spread { + params = json!({"dx": dx, "dy": dy, "copy": ev.mods.alt, "toSpread": spread_json(target)}); + } + vec![Action::Preview("transform.move".into(), params)] + } + + fn pointer_drag(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec { + let p = ev.pos; + match self.drag.clone() { + Drag::Bracket { id, end } => { + let Some((warp, xf, _, _)) = path_type_of(cx, id) else { return vec![] }; + if xf.determinant().abs() < 1e-12 { + return vec![]; + } + let s = warp.nearest(xf.inverse() * p); + vec![Action::Preview("type.pathOptions".into(), json!({"id": id.0, if end { "end" } else { "start" }: s}))] + } + Drag::Pending { start, hit, content } => { + if (p - start).hypot() < cx.tol(3.0) { + return vec![]; + } + self.shift_release = None; + self.key_release = None; + if hit && !cx.selection.items.is_empty() { + // Direct Selection on items without content moves (or duplicates) them whole. + let content = content && cx.selection.items.iter().any(|i| cx.doc.item(*i).is_some_and(holds_content)); + let sr = cx.selection.items.first().and_then(|i| cx.doc.find(*i)).map(|l| l.spread).unwrap_or(SpreadRef::Doc(0)); + let exclude = cx.selection.items.clone(); + let bounds0 = if content { content_bounds(cx) } else { cx.selection_visible_bounds() }; + self.drag = Drag::Move { start, origin_spread: sr, bounds0, exclude: exclude.clone(), content }; + let mut v = vec![]; + // Dragging a content selection by its frame moves the frame: select it. + if !content && cx.selection.content { + let ids: Vec = exclude.iter().map(|i| i.0).collect(); + v.push(Action::Exec("selection.set".into(), json!({"ids": ids}))); + } + let label = if content { + "Move Content" + } else if ev.mods.alt { + "Duplicate" + } else { + "Move" + }; + v.push(Action::Begin(label.into())); + v.extend(self.pointer(cx, ev)); + v + } else { + self.drag = Drag::Marquee { start, cur: p }; + vec![] + } + } + movement @ Drag::Move { .. } => self.pointer_drag_move(cx, ev, movement), + Drag::Resize { handle, start, from, spread } => { + let _ = start; + let pointer = cx.layout.to_spread(spread, p); + let raw = resize_edges_rect(from, handle, pointer); + let mut to = resize_rect(from, handle, pointer, ev.mods); + // Space while dragging: Live Distribute (several objects keep their size). + let distribute = ev.mods.space && cx.selection.items.len() > 1; + self.guides.clear(); + // Command still scales content. It does not skip the snap. + if !distribute && cx.snap.any() { + let (mut x_edges, mut y_edges) = resize_edge_flags(handle); + let driver_x = drives_x(handle, from, raw); + if ev.mods.shift { + if driver_x { + y_edges = [false, false, false]; + } else { + x_edges = [false, false, false]; + } + } + let exclude = cx.selection.items.clone(); + let (pad_x, pad_y) = single_stroke_pad(cx); + let proposed = to; + let mut hit = crate::snap::snap( + cx, + SnapRequest { + spread, + gesture: Gesture::Resize, + rect: proposed, + x_edges, + y_edges, + exclude: &exclude, + exclude_guides: &[], + copying: false, + lengths: [axis_length(x_edges, proposed.width(), pad_x), axis_length(y_edges, proposed.height(), pad_y)], + angle: None, + radius: 0.0, + pointer, + }, + ); + to = finish_snapped_resize(from, handle, proposed, ev.mods, driver_x, (x_edges, y_edges), &hit); + crate::snap::lay_dimension_guides(&mut hit.guides, cx.layout.xf(spread), proposed, to, x_edges, y_edges, hit.length_delta); + self.guides = hit.guides; + } + vec![Action::Preview( + "transform.resize".into(), + json!({"from": rect_json(from), "to": rect_json(to), "content": ev.mods.cmd, "distribute": distribute}), + )] + } + Drag::ContentResize { id, handle, from, spread } => { + let to = resize_rect(from, handle, cx.layout.to_spread(spread, p), ev.mods); + vec![Action::Preview("transform.set".into(), json!({"ids": [id.0], "content": true, "bounds": rect_json(to)}))] + } + Drag::Marquee { start, .. } => { + self.drag = Drag::Marquee { start, cur: p }; + vec![] + } + Drag::Place { .. } => vec![], + Drag::Corner { frame, corner, start } => { + let Some(d) = frame.drag_delta(corner, start, p) else { return vec![] }; + let opts = frame.dragged(corner, frame.size(corner) + d, ev.mods.shift); + vec![Action::Preview("object.cornerOptions".into(), corners::params(frame.id, &opts))] + } + Drag::Rotate { center, start_angle, start_rotation } => { + let mut a = ((p - center).atan2() - start_angle).to_degrees(); + self.guides.clear(); + if ev.mods.shift { + // 45 degree steps. The rotation pass does not run while Shift is held. + a = (a / 45.0).round() * 45.0; + } else if let Some(start) = start_rotation { + let raw = -a; + if let Some(resulting) = crate::snap::folded_rotation(start + raw) { + let spread = cx.selection.items.first().and_then(|id| cx.doc.find(*id)).map(|loc| loc.spread).unwrap_or(SpreadRef::Doc(0)); + let exclude = cx.selection.items.clone(); + // The rect is unused by the angle pass. It only has to be finite. + let hit = crate::snap::snap( + cx, + SnapRequest { + spread, + gesture: Gesture::Rotate, + rect: Rect::new(0.0, 0.0, 1.0, 1.0), + x_edges: [false, false, false], + y_edges: [false, false, false], + exclude: &exclude, + exclude_guides: &[], + copying: false, + lengths: [None, None], + angle: Some(resulting), + radius: (p - center).hypot(), + pointer: cx.layout.to_spread(spread, p), + }, + ); + if let Some(snapped) = hit.angle + && let Some(delta) = crate::snap::rotation_command_delta(start, snapped, raw) + { + a = -delta; + } + self.guides = hit.guides; + } + } + // Screen y points down: a positive screen angle is clockwise. + vec![Action::Preview("transform.rotate".into(), json!({"angle": -a}))] + } + Drag::Anchor { id, si, ai, handle, start, at } => { + let d = p - start; + let mut dx = d.x; + let mut dy = d.y; + self.guides.clear(); + // Direction handles do not snap. An anchor excludes the path it is on. + if handle.is_none() + && let Some((spread, origin)) = at + { + let proposed = origin + cx.layout.delta_to_spread(spread, d); + let exclude = [ItemId(id)]; + let hit = crate::snap::snap( + cx, + SnapRequest { + spread, + gesture: Gesture::Point, + rect: Rect::from_points(proposed, proposed), + x_edges: [true, true, true], + y_edges: [true, true, true], + exclude: &exclude, + exclude_guides: &[], + copying: false, + lengths: [None, None], + angle: None, + radius: 0.0, + pointer: proposed, + }, + ); + // dx and dy stay in canvas space. Snap.delta is spread space. + let corr = spread_delta_to_canvas(cx.layout.xf(spread), hit.delta); + if corr.x.is_finite() { + dx += corr.x; + } + if corr.y.is_finite() { + dy += corr.y; + } + self.guides = hit.guides; + } + let mut params = json!({"id": id, "anchors": [[si, ai]], "dx": dx, "dy": dy}); + if let Some(h) = handle { + params["handle"] = json!(h); + } + vec![Action::Preview("path.moveAnchors".into(), params)] + } + Drag::None => vec![], + } + } +} + impl Tool for SelectionTool { fn id(&self) -> &'static str { if self.direct { "directSelection" } else { "selection" } @@ -736,400 +1142,8 @@ impl Tool for SelectionTool { self.hover_handle = handle_at(cx, p); vec![] } - PointerKind::Down => { - self.shift_release = None; - self.key_release = None; - // Type on a path: a bracket moves where the text starts or ends. - if !ev.mods.shift - && let Some((id, end)) = self.bracket_at(cx, p) - { - self.drag = Drag::Bracket { id, end }; - return vec![Action::Begin("Move Bracket".into())]; - } - if self.direct - && let Some((id, si, ai, handle)) = anchor_at(cx, p) - { - let at = anchor_spread_point(cx, id, si, ai); - self.drag = Drag::Anchor { id, si, ai, handle, start: p, at }; - return vec![Action::Begin(if handle.is_some() { "Move Direction Handle".into() } else { "Move Anchor".into() })]; - } - // A selected placed graphic's own handles resize it inside its frame (the - // frame's anchors, under Direct Selection, come first). - if let Some((id, h)) = content_handle_at(cx, p) - && let (Some(loc), Some((_, b))) = (cx.doc.find(id), selected_graphic(cx)) - { - let turns = cx.layout.slot(loc.spread).map_or(0, |s| s.rotation as usize); - let handle = (h + 8 - 2 * turns) % 8; - let from = cx.layout.xf(loc.spread).inverse().transform_rect_bbox(b); - self.drag = Drag::ContentResize { id, handle, from, spread: loc.spread }; - return vec![Action::Begin("Resize Content".into())]; - } - if let Some((frame, corner)) = self.diamond_at(cx, p) { - if ev.mods.alt { - self.drag = Drag::None; - let opts = frame.cycled(corner, ev.mods.shift); - return vec![Action::Exec("object.cornerOptions".into(), corners::params(frame.id, &opts))]; - } - self.drag = Drag::Corner { frame, corner, start: p }; - return vec![Action::Begin("Corner Options".into())]; - } - if let Some(frame) = self.widget_at(cx, p) { - self.live = Some(frame.id); - self.drag = Drag::None; - return vec![Action::Handled]; - } - if !self.direct - && handle_at(cx, p).is_none() - && let Some(center) = rotate_zone(cx, p) - { - self.drag = Drag::Rotate { center, start_angle: (p - center).atan2(), start_rotation: crate::snap::reference_rotation(cx) }; - return vec![Action::Begin("Rotate".into())]; - } - if let Some(h) = handle_at(cx, p).filter(|_| !self.direct) - && let (Some(b), Some(first)) = (cx.selection_bounds(), cx.selection.items.first()) - && let Some(loc) = cx.doc.find(*first) - { - // On a turned spread the canvas handle is another one of the spread's. - let turns = cx.layout.slot(loc.spread).map_or(0, |s| s.rotation as usize); - let handle = (h + 8 - 2 * turns) % 8; - self.drag = - Drag::Resize { handle, start: p, from: cx.layout.xf(loc.spread).inverse().transform_rect_bbox(b), spread: loc.spread }; - return vec![Action::Begin("Resize".into())]; - } - // The content grabber: select the frame's content; a drag moves it. - if !self.direct - && !ev.mods.shift - && let Some(id) = grabber_at(cx, p) - { - self.drag = Drag::Pending { start: p, hit: true, content: true }; - if cx.selection.items == [id] && cx.selection.content { - return vec![]; - } - return vec![Action::Exec("selection.set".into(), json!({"ids": [id.0], "content": true}))]; - } - // Cmd+Shift-click a parent item on a page: override it there and select the copy. - if ev.mods.cmd - && ev.mods.shift - && cx.hit(p).is_none() - && let Some((page, id)) = cx.hit_parent_on_page(p) - { - self.drag = Drag::Pending { start: p, hit: true, content: false }; - return vec![Action::Exec("layout.overrideParentItems".into(), json!({"page": page, "ids": [id.0]}))]; - } - match cx.hit(p) { - Some((sr, id)) => { - let id = if self.direct { id } else { cx.doc.top_level_of(id).unwrap_or(id) }; - let mut out = vec![]; - if ev.mods.shift { - 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) || (self.direct && !cx.selection.content) { - out.push(Action::Exec("selection.set".into(), json!({"ids": [id.0], "content": self.direct}))); - } else if !self.direct && cx.selection.items.len() > 1 { - // InDesign: a click on one of several selected objects makes it the - // key object, on release so that a drag still moves them all. - self.key_release = Some(id.0); - } - let content = self.direct || on_selected_content(cx, p); - self.drag = Drag::Pending { start: p, hit: true, content }; - let _ = sr; - out - } - None => { - self.live = None; - self.drag = Drag::Pending { start: p, hit: false, content: false }; - if ev.mods.shift { vec![] } else { vec![Action::Exec("selection.set".into(), json!({"ids": []}))] } - } - } - } - PointerKind::Drag => match self.drag.clone() { - Drag::Bracket { id, end } => { - let Some((warp, xf, _, _)) = path_type_of(cx, id) else { return vec![] }; - if xf.determinant().abs() < 1e-12 { - return vec![]; - } - let s = warp.nearest(xf.inverse() * p); - vec![Action::Preview("type.pathOptions".into(), json!({"id": id.0, if end { "end" } else { "start" }: s}))] - } - Drag::Pending { start, hit, content } => { - if (p - start).hypot() < cx.tol(3.0) { - return vec![]; - } - self.shift_release = None; - self.key_release = None; - if hit && !cx.selection.items.is_empty() { - // Direct Selection on items without content moves (or duplicates) them whole. - let content = content && cx.selection.items.iter().any(|i| cx.doc.item(*i).is_some_and(holds_content)); - let sr = cx.selection.items.first().and_then(|i| cx.doc.find(*i)).map(|l| l.spread).unwrap_or(SpreadRef::Doc(0)); - let exclude = cx.selection.items.clone(); - let bounds0 = if content { content_bounds(cx) } else { cx.selection_visible_bounds() }; - self.drag = Drag::Move { start, origin_spread: sr, bounds0, exclude: exclude.clone(), content }; - let mut v = vec![]; - // Dragging a content selection by its frame moves the frame: select it. - if !content && cx.selection.content { - let ids: Vec = exclude.iter().map(|i| i.0).collect(); - v.push(Action::Exec("selection.set".into(), json!({"ids": ids}))); - } - let label = if content { - "Move Content" - } else if ev.mods.alt { - "Duplicate" - } else { - "Move" - }; - v.push(Action::Begin(label.into())); - v.extend(self.pointer(cx, ev)); - v - } else { - self.drag = Drag::Marquee { start, cur: p }; - vec![] - } - } - Drag::Move { start, origin_spread, bounds0, exclude, content } => { - let mut d: Vec2 = p - start; - let mut screen_x_locked = false; - let mut screen_y_locked = false; - if ev.mods.shift { - if d.x.abs() > d.y.abs() { - d.y = 0.0; - screen_y_locked = true; - } else { - d.x = 0.0; - screen_x_locked = true; - } - } - // Snap on the spread under the pointer. Shift uses that spread's view rotation. - // Content stays in its frame, on the frame's spread. - let target = if content { origin_spread } else { cx.layout.spread_at(p).map(|(s, _)| s).unwrap_or(origin_spread) }; - // 180 degrees keeps the screen axis. 90 and 270 swap it into the other spread axis. - let odd_turn = cx.layout.slot(target).is_some_and(|slot| slot.rotation % 2 == 1); - let spread_x_locked = if odd_turn { screen_y_locked } else { screen_x_locked }; - let spread_y_locked = if odd_turn { screen_x_locked } else { screen_y_locked }; - let x_edges = if spread_x_locked { [false; 3] } else { [true; 3] }; - let y_edges = if spread_y_locked { [false; 3] } else { [true; 3] }; - self.guides.clear(); - // The movement in the origin spread's 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)); - let q0 = b0s.map_or(Point::ORIGIN, |b| Point::new(b.x0, b.y0)); - // Same corner conversion transform.move already uses when the spread changes. - let mut landed = cx.layout.to_spread(target, cx.layout.to_canvas(origin_spread, q0 + ds)); - // Command suspends snapping. A Shift-locked spread axis is not offered. - if !ev.mods.cmd - && cx.snap.any() - && let Some(b0) = b0s - { - let proposed = if target == origin_spread { - b0 + ds - } else { - let shift = landed - q0; - Rect::new(b0.x0 + shift.x, b0.y0 + shift.y, b0.x1 + shift.x, b0.y1 + shift.y) - }; - let hit = crate::snap::snap( - cx, - SnapRequest { - spread: target, - gesture: Gesture::Move, - rect: proposed, - x_edges, - y_edges, - exclude: &exclude, - exclude_guides: &[], - copying: ev.mods.alt && !content, - lengths: [None, None], - angle: None, - radius: 0.0, - pointer: cx.layout.to_spread(target, p), - }, - ); - if target == origin_spread { - if x_edges != [false, false, false] && hit.delta.x.is_finite() { - ds.x += hit.delta.x; - } - if y_edges != [false, false, false] && hit.delta.y.is_finite() { - ds.y += hit.delta.y; - } - } else { - if x_edges != [false, false, false] && hit.delta.x.is_finite() { - landed.x += hit.delta.x; - } - if y_edges != [false, false, false] && hit.delta.y.is_finite() { - landed.y += hit.delta.y; - } - } - self.guides = hit.guides; - } - // Same spread: dx, dy are the origin spread translation. Crossing: the corner - // conversion, so the reparented item lands on the snapped target point. - let (dx, dy) = if target == origin_spread { - (ds.x, ds.y) - } else { - let dt = landed - q0; - (dt.x, dt.y) - }; - if content { - let ids: Vec = exclude.iter().map(|i| i.0).collect(); - return vec![Action::Preview("transform.move".into(), json!({"dx": dx, "dy": dy, "ids": ids, "content": true}))]; - } - let mut params = json!({"dx": dx, "dy": dy, "copy": ev.mods.alt}); - if target != origin_spread { - params = json!({"dx": dx, "dy": dy, "copy": ev.mods.alt, "toSpread": spread_json(target)}); - } - vec![Action::Preview("transform.move".into(), params)] - } - Drag::Resize { handle, start, from, spread } => { - let _ = start; - let pointer = cx.layout.to_spread(spread, p); - let raw = resize_edges_rect(from, handle, pointer); - let mut to = resize_rect(from, handle, pointer, ev.mods); - // Space while dragging: Live Distribute (several objects keep their size). - let distribute = ev.mods.space && cx.selection.items.len() > 1; - self.guides.clear(); - // Command still scales content. It does not skip the snap. - if !distribute && cx.snap.any() { - let (mut x_edges, mut y_edges) = resize_edge_flags(handle); - let driver_x = drives_x(handle, from, raw); - if ev.mods.shift { - if driver_x { - y_edges = [false, false, false]; - } else { - x_edges = [false, false, false]; - } - } - let exclude = cx.selection.items.clone(); - let (pad_x, pad_y) = single_stroke_pad(cx); - let proposed = to; - let mut hit = crate::snap::snap( - cx, - SnapRequest { - spread, - gesture: Gesture::Resize, - rect: proposed, - x_edges, - y_edges, - exclude: &exclude, - exclude_guides: &[], - copying: false, - lengths: [axis_length(x_edges, proposed.width(), pad_x), axis_length(y_edges, proposed.height(), pad_y)], - angle: None, - radius: 0.0, - pointer, - }, - ); - to = finish_snapped_resize(from, handle, proposed, ev.mods, driver_x, (x_edges, y_edges), &hit); - crate::snap::lay_dimension_guides(&mut hit.guides, cx.layout.xf(spread), proposed, to, x_edges, y_edges, hit.length_delta); - self.guides = hit.guides; - } - vec![Action::Preview( - "transform.resize".into(), - json!({"from": rect_json(from), "to": rect_json(to), "content": ev.mods.cmd, "distribute": distribute}), - )] - } - Drag::ContentResize { id, handle, from, spread } => { - let to = resize_rect(from, handle, cx.layout.to_spread(spread, p), ev.mods); - vec![Action::Preview("transform.set".into(), json!({"ids": [id.0], "content": true, "bounds": rect_json(to)}))] - } - Drag::Marquee { start, .. } => { - self.drag = Drag::Marquee { start, cur: p }; - vec![] - } - Drag::Place { .. } => vec![], - Drag::Corner { frame, corner, start } => { - let Some(d) = frame.drag_delta(corner, start, p) else { return vec![] }; - let opts = frame.dragged(corner, frame.size(corner) + d, ev.mods.shift); - vec![Action::Preview("object.cornerOptions".into(), corners::params(frame.id, &opts))] - } - Drag::Rotate { center, start_angle, start_rotation } => { - let mut a = ((p - center).atan2() - start_angle).to_degrees(); - self.guides.clear(); - if ev.mods.shift { - // 45 degree steps. The rotation pass does not run while Shift is held. - a = (a / 45.0).round() * 45.0; - } else if let Some(start) = start_rotation { - let raw = -a; - if let Some(resulting) = crate::snap::folded_rotation(start + raw) { - let spread = - cx.selection.items.first().and_then(|id| cx.doc.find(*id)).map(|loc| loc.spread).unwrap_or(SpreadRef::Doc(0)); - let exclude = cx.selection.items.clone(); - // The rect is unused by the angle pass. It only has to be finite. - let hit = crate::snap::snap( - cx, - SnapRequest { - spread, - gesture: Gesture::Rotate, - rect: Rect::new(0.0, 0.0, 1.0, 1.0), - x_edges: [false, false, false], - y_edges: [false, false, false], - exclude: &exclude, - exclude_guides: &[], - copying: false, - lengths: [None, None], - angle: Some(resulting), - radius: (p - center).hypot(), - pointer: cx.layout.to_spread(spread, p), - }, - ); - if let Some(snapped) = hit.angle - && let Some(delta) = crate::snap::rotation_command_delta(start, snapped, raw) - { - a = -delta; - } - self.guides = hit.guides; - } - } - // Screen y points down: a positive screen angle is clockwise. - vec![Action::Preview("transform.rotate".into(), json!({"angle": -a}))] - } - Drag::Anchor { id, si, ai, handle, start, at } => { - let d = p - start; - let mut dx = d.x; - let mut dy = d.y; - self.guides.clear(); - // Direction handles do not snap. An anchor excludes the path it is on. - if handle.is_none() - && let Some((spread, origin)) = at - { - let proposed = origin + cx.layout.delta_to_spread(spread, d); - let exclude = [ItemId(id)]; - let hit = crate::snap::snap( - cx, - SnapRequest { - spread, - gesture: Gesture::Point, - rect: Rect::from_points(proposed, proposed), - x_edges: [true, true, true], - y_edges: [true, true, true], - exclude: &exclude, - exclude_guides: &[], - copying: false, - lengths: [None, None], - angle: None, - radius: 0.0, - pointer: proposed, - }, - ); - // dx and dy stay in canvas space. Snap.delta is spread space. - let corr = spread_delta_to_canvas(cx.layout.xf(spread), hit.delta); - if corr.x.is_finite() { - dx += corr.x; - } - if corr.y.is_finite() { - dy += corr.y; - } - self.guides = hit.guides; - } - let mut params = json!({"id": id, "anchors": [[si, ai]], "dx": dx, "dy": dy}); - if let Some(h) = handle { - params["handle"] = json!(h); - } - vec![Action::Preview("path.moveAnchors".into(), params)] - } - Drag::None => vec![], - }, + PointerKind::Down => self.pointer_down(cx, ev), + PointerKind::Drag => self.pointer_drag(cx, ev), PointerKind::Up => { self.guides.clear(); let release = self.shift_release.take();