use super::hit::{constrain_45, line_xfrm, resize_box}; use super::{ Affine, DRAW_TABLE, Drag, Mods, Point, Rect, Result, Session, ShapeId, ShapeKind, TextSel, ToolKind, Value, Vec2, Xfrm, cmd, json, set_warp_adj, }; struct MoveDrag { ids: Vec, start: Point, origs: Vec<(ShapeId, Xfrm)>, moved: bool, duplicate: bool, enter_text: Option<(ShapeId, Point)>, } impl Session { pub(super) fn drag(&mut self, p: Point, mods: Mods, tol: f64) -> Result { let Some(drag) = self.tool.drag.clone() else { return Ok(Value::Null) }; match drag { Drag::Guide { index } => { let vertical = self.active().and_then(|d| d.doc.guides.get(index)).is_some_and(|g| g.vertical); let pos = if vertical { p.x } else { p.y }; // Guides move in whole points unless snapping is off, as in PowerPoint. let pos = if self.prefs.snap_to_grid && !mods.alt { pos.round() } else { pos }; self.tool.guide_drag = Some((index, pos)); } Drag::Move { ids, start, origs, moved, duplicate, enter_text } => { return self.drag_move(MoveDrag { ids, start, origs, moved, duplicate, enter_text }, p, mods, tol); } Drag::Resize { id, orig, ids, handle, origs, start } => { let keep_aspect = mods.shift || self.is_picture(id); let nx = resize_box(&orig, handle, p - start, keep_aspect, mods.alt); let nx = self.snap_resize(nx, handle, &ids, tol); let sx = if orig.w > 1e-9 { (nx.w / orig.w) * if nx.flip_h != orig.flip_h { -1.0 } else { 1.0 } - 1.0 } else { 0.0 }; let sy = if orig.h > 1e-9 { (nx.h / orig.h) * if nx.flip_v != orig.flip_v { -1.0 } else { 1.0 } - 1.0 } else { 0.0 }; let div = if mods.alt { 2.0 } else { 1.0 }; let hx = match handle { 0 | 6 | 7 => -1.0, 2..=4 => 1.0, _ => 0.0, }; let hy = match handle { 0..=2 => -1.0, 4..=6 => 1.0, _ => 0.0, }; for (tid, o) in &origs { let target_nx = if *tid == id { nx } else { let lp = Affine::rotate(o.rot.to_radians()) * Point::new( hx * sx * o.w / div * if o.flip_h { -1.0 } else { 1.0 }, hy * sy * o.h / div * if o.flip_v { -1.0 } else { 1.0 }, ); resize_box(o, handle, Vec2::new(lp.x, lp.y), keep_aspect, mods.alt) }; self.set_xfrm(*tid, target_nx)?; } } Drag::LineEnd { id, end, orig } => { let a = orig.affine(); let (p0, p1) = (a * Point::new(0.0, 0.0), a * Point::new(orig.w, orig.h)); let mut q = p; let other = if end == 0 { p1 } else { p0 }; if mods.shift { q = constrain_45(other, q); } let glue = self.update_sites(q, tol, Some(id)); if let Some((_, _, at)) = glue { q = at; } let (s0, s1) = if end == 0 { (q, p1) } else { (p0, q) }; self.set_xfrm(id, line_xfrm(s0, s1, orig.rot))?; self.edit(|doc, sel| { if let Some(list) = crate::shapes_mut(doc, sel) { crate::connect::set_glue(list, id, end, glue.map(|g| (g.0, g.1))); } Ok(()) })?; } Drag::Rotate { ids, center, start, origs } => { let ang = (p.y - center.y).atan2(p.x - center.x); let mut delta = (ang - start).to_degrees(); for (id, o) in &origs { let mut r = o.rot + delta; if mods.shift { r = (r / 15.0).round() * 15.0; delta = r - o.rot; } let mut nx = *o; nx.rot = ((r % 360.0) + 360.0) % 360.0; if ids.len() > 1 { // Rotate positions around the common centre too. let c = o.center(); let rc = Affine::translate(center.to_vec2()) * Affine::rotate(delta.to_radians()) * Affine::translate(-center.to_vec2()) * c; nx.x = rc.x - o.w / 2.0; nx.y = rc.y - o.h / 2.0; } self.set_xfrm(*id, nx)?; } } Drag::Adjust { id, index, orig } => return self.drag_adjust(id, index, orig, p), Drag::Vertex { id, path, index } => { self.tool.drag = Some(Drag::Vertex { id, path, index }); self.edit_vertex(id, path, index, p)?; return Ok(Value::Null); } Drag::Handle { id, path, index, out } => { self.tool.drag = Some(Drag::Handle { id, path, index, out }); let side = if out { "out" } else { "in" }; // Shift, Ctrl or Alt held while dragging a handle makes the point smooth, straight or // corner, as in PowerPoint; Cmd and Option count as Ctrl and Alt. let held = if mods.shift { Some("smooth") } else if mods.cmd { Some("straight") } else if mods.alt { Some("corner") } else { None }; if let Err(e) = self.execute_inner_move_handle(id, path, index, side, p, held) { log::debug!("edit handle: {e}"); } return Ok(Value::Null); } Drag::Warp { id, index, name, base, orig } => { let v = { let st = self.doc()?; let Some(sh) = st.shape(id) else { return Ok(Value::Null) }; let tr = deckcraft_render::shape_geometry(sh, orig.w, orig.h).text_rect; let hs = deckcraft_geom::warp::handles(&name, &base, tr); let Some(h) = hs.iter().find(|h| h.index == index) else { return Ok(Value::Null) }; h.value_at(orig.affine().inverse() * p) }; self.edit(|doc, sel| { let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("adjust", "no slide"))?; if let Some(sh) = deckcraft_model::find_shape_mut(list, id) { set_warp_adj(sh, &name, &base, index, v); } Ok(()) })?; } Drag::Marquee { start, .. } => { self.tool.marquee = Some(Rect::from_points(start, p)); } Drag::Create { start } => self.drag_create(start, p, mods, tol), Drag::TextSelect { shape, press, .. } => return self.drag_text_select(shape, press, p), Drag::TableBorder { id, column, index, start, size, frame, moved } => { let local = frame.inverse() * p; let d = if column { local.x - start.x } else { local.y - start.y }; if !moved && d.abs() < tol.max(1.0) { return Ok(Value::Null); } if !moved { self.begin(if column { "Column Width" } else { "Row Height" }); } // An inner column border moves between its two columns (the table keeps its // width); the right edge widens the last column and the table. A row's height is // a minimum: rows still grow to fit their text. if column { let width = (size + d).max(cmd::table::MIN_COLUMN_WIDTH); self.execute("table.columnWidth", &json!({"id": id, "col": index, "width": width, "keepTotal": true}))?; } else { self.execute("table.rowHeight", &json!({"id": id, "row": index, "height": (size + d).max(1.0)}))?; } self.tool.drag = Some(Drag::TableBorder { id, column, index, start, size, frame, moved: true }); } Drag::Freeform { mode, mut points, pressed } => { // Dragging scribbles (except for curves, which only take clicked points). if pressed && mode != "curve" && points.last().is_none_or(|l| (*l - p).hypot() >= 1.5) { points.push(p); } let mut v = points.clone(); if mode == "curve" { v.push(p); } self.tool.path_preview = v; self.tool.drag = Some(Drag::Freeform { mode, points, pressed }); } Drag::Ink { mut points } => { points.push(self.ink_sample(p)); self.tool.ink_preview = points.clone(); self.tool.drag = Some(Drag::Ink { points }); } } Ok(Value::Null) } fn drag_move(&mut self, drag: MoveDrag, p: Point, mods: Mods, tol: f64) -> Result { let MoveDrag { ids, start, origs, moved, duplicate, enter_text } = drag; let mut d = p - start; if !moved && (d.hypot() < tol.max(1.0) || origs.is_empty()) { return Ok(Value::Null); } if !moved { self.begin(if duplicate { "Duplicate" } else { "Move" }); if duplicate { // Alt-drag: drag a copy, leave the original. self.execute_dup_in_place(&ids)?; } } let ids_now = if duplicate { self.doc()?.selection.shapes.clone() } else { ids.clone() }; if mods.shift { if d.x.abs() > d.y.abs() { d.y = 0.0; } else { d.x = 0.0; } } // Smart guides on the moved group's bounds. let bounds = origs.iter().map(|(_, x)| x.bounds()).reduce(|a, b| a.union(b)).unwrap_or_default(); let moved_b = bounds + d; let (snap, guides) = self.snap_rect(moved_b, &ids_now, tol * 1.5, !mods.cmd); d += snap; self.tool.guides = guides; let origs2: Vec<(ShapeId, Xfrm)> = if duplicate { ids_now.iter().copied().zip(origs.iter().map(|o| o.1)).collect() } else { origs.clone() }; self.edit(|doc, sel| { let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("move", "no slide"))?; for (id, o) in &origs2 { if let Some(sh) = deckcraft_model::find_shape_mut(list, *id) { sh.xfrm = Some(Xfrm { x: o.x + d.x, y: o.y + d.y, ..*o }); } } // A connector dragged away from shapes that stay put comes unglued at those ends. let moved: Vec = origs2.iter().map(|o| o.0).collect(); for sh in list.iter_mut().filter(|s| moved.contains(&s.id)) { if let ShapeKind::Connector { start, end } = &mut sh.kind { for c in [start, end] { if c.is_some_and(|(to, _)| !moved.contains(&to)) { *c = None; } } } } Ok(()) })?; self.tool.drag = Some(Drag::Move { ids: ids_now, start, origs: origs2, moved: true, duplicate: false, enter_text: enter_text.filter(|_| false) }); Ok(Value::Null) } fn drag_adjust(&mut self, id: ShapeId, index: usize, orig: Xfrm, p: Point) -> Result { let st = self.doc()?; let Some(sh) = st.shape(id).cloned() else { return Ok(Value::Null) }; let local = orig.affine().inverse() * p; let geo = deckcraft_render::shape_geometry(&sh, orig.w, orig.h); let Some(h) = geo.handles.iter().find(|h| h.index == index) else { return Ok(Value::Null) }; let free2d = matches!(sh.geom.preset_name(), Some(n) if n.contains("Callout") && !n.contains("Arrow")); let vals: Vec<(usize, f64)> = if free2d { let dx = (local.x - orig.w / 2.0) / orig.w.max(1e-6) * 1e5; let dy = (local.y - orig.h / 2.0) / orig.h.max(1e-6) * 1e5; vec![(0, dx.round()), (1, dy.round())] } else { vec![(index, h.value_at(local))] }; self.edit(|doc, sel| { let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("adjust", "no slide"))?; if let Some(sh) = deckcraft_model::find_shape_mut(list, id) && let deckcraft_model::Geom::Preset { name, adj } = &mut sh.geom { let defaults = deckcraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]); for (i, v) in &vals { while adj.len() <= *i { let k = adj.len(); adj.push(defaults.get(k).copied().unwrap_or(0.0)); } if let Some(slot) = adj.get_mut(*i) { *slot = *v; } } } Ok(()) })?; Ok(Value::Null) } fn drag_create(&mut self, start: Point, p: Point, mods: Mods, tol: f64) { let line = matches!(&self.tool.kind, ToolKind::Shape { preset } if deckcraft_geom::preset::is_line_like(preset)); let x = if line { let mut q = if mods.shift { constrain_45(start, p) } else { p }; if let Some((_, _, at)) = self.update_sites(q, tol, None) { q = at; } let s0 = self.glue_at(start, tol).map(|g| g.2).unwrap_or(start); line_xfrm(s0, q, 0.0) } else if matches!(&self.tool.kind, ToolKind::Shape { preset } if preset == DRAW_TABLE) && crate::cmd::table::table_at(self, &Value::Null, (start.x, start.y)).is_some() { // The pencil inside a table draws a straight border, across or down. let q = if (p.x - start.x).abs() >= (p.y - start.y).abs() { Point::new(p.x, start.y) } else { Point::new(start.x, p.y) }; let r = Rect::from_points(start, q); Xfrm::new(r.x0, r.y0, r.width(), r.height()) } else { let mut r = Rect::from_points(start, p); if mods.shift { let s = r.width().max(r.height()); let sx = if p.x < start.x { -s } else { s }; let sy = if p.y < start.y { -s } else { s }; r = Rect::from_points(start, start + Vec2::new(sx, sy)); } if mods.alt { let d = p - start; r = Rect::new(start.x - d.x.abs(), start.y - d.y.abs(), start.x + d.x.abs(), start.y + d.y.abs()); } Xfrm::new(r.x0, r.y0, r.width(), r.height()) }; self.tool.preview = Some(x); } fn drag_text_select(&mut self, shape: ShapeId, press: Option, p: Point) -> Result { // In a table, leaving the cell the drag started in selects whole cells, as in // PowerPoint; coming back to that cell selects text in it again. let st = self.doc()?; if let Some(start) = press.as_ref().and_then(|t| t.cell) && let Some(f) = st.shape(shape).and_then(|sh| cmd::table::table_frame(st, sh)) && let Some(cell) = f.cell_at(f.local(p)) { if cell != start { self.select(|_, sel| { sel.text = None; sel.shapes = vec![shape]; sel.cells = Some((shape, start, cell)); })?; return Ok(Value::Null); } if st.selection.text.is_none() { self.select(|_, sel| { sel.cells = None; sel.text = press; })?; } } let st = self.doc()?; if let Some(t) = st.selection.text.as_ref().filter(|t| t.shape == shape) { let pos = cmd::text::text_pos_at(st, t, p); self.select(|_, sel| { if let Some(t) = sel.text.as_mut() { t.caret = pos; } })?; } Ok(Value::Null) } /// Moves a vertex during a drag (one undo step for the whole drag). fn edit_vertex(&mut self, id: ShapeId, path: usize, index: usize, p: Point) -> Result<()> { let r = cmd::points::move_point_raw(self, id, path, index, p); if let Err(e) = r { log::debug!("edit point: {e}"); } Ok(()) } /// Moves a control handle during a drag. `held` is the point type set by the modifier held /// during the drag; the point keeps that type for later drags, as after `shape.pointType`. fn execute_inner_move_handle(&mut self, id: ShapeId, path: usize, index: usize, side: &str, p: Point, held: Option<&str>) -> Result { let kind = match held { Some(k) => Some(k.to_string()), None => self.tool.vertex_kind.clone().filter(|(s, pp, i, _)| (*s, *pp, *i) == (id, path, index)).map(|k| k.3), }; let r = cmd::points::move_handle_raw(self, &json!({"id": id.0, "path": path, "index": index, "side": side, "x": p.x, "y": p.y, "type": kind}))?; if let Some(k) = held { self.tool.vertex_kind = Some((id, path, index, k.to_string())); } Ok(r) } }