//! Border dragging changes diagram presentation, never semantic ownership. use crate::route_edit::{Context, point, position}; use iced::Point; use oa_core::{projection::*, *}; #[derive(Debug, Clone)] pub enum Event { Commit(Context, Id, PortAnchor), Reset(Context, Id, Id), } #[derive(Debug, Clone)] pub struct Gesture { pub context: Context, pub owner: ProjectedNode, pub anchor: PortAnchor, pub start: Point, pub changed: bool, } impl Gesture { pub fn new(context: Context, owner: ProjectedNode, port: &ProjectedPort, start: Point) -> Self { let anchor = PortAnchor { port: port.element, side: port.side, offset: port_anchor_offset(&owner, port.side, port.position), }; Self { context, owner, anchor, start, changed: false, } } pub fn move_to(&mut self, cursor: Point, zoom: f32) { if !self.changed && cursor.distance(self.start) * zoom < 3. { return; } self.changed = true; let distance = |side| { let offset = port_anchor_offset(&self.owner, side, position(cursor)); cursor.distance(point(port_anchor_position(&self.owner, side, offset))) }; let nearest = [ PortAnchorSide::Left, PortAnchorSide::Right, PortAnchorSide::Top, PortAnchorSide::Bottom, ] .into_iter() .min_by(|a, b| distance(*a).total_cmp(&distance(*b))) .unwrap(); // Six screen pixels of hysteresis avoids flickering between corner sides. if distance(self.anchor.side) > distance(nearest) + 6. / zoom { self.anchor.side = nearest; } self.anchor.offset = (port_anchor_offset(&self.owner, self.anchor.side, position(cursor)) * 100.).round() / 100.; } } pub fn with_anchor(diagram: &mut Diagram, node: Id, anchor: PortAnchor) { if let Some(node) = diagram.nodes.get_mut(&node) { if let Some(previous) = node.port_anchors.iter_mut().find(|a| a.port == anchor.port) { *previous = anchor; } else { node.port_anchors.push(anchor); } } } /// Graphical ownership edits intentionally reset anchors left on the old owner. /// Raw source/API edits remain strict and must explicitly repair their anchors. pub fn repair_owner_change(project: &Project, command: Command) -> Command { let change = match &command { Command::Reparent { element, parent } => Some((*element, *parent)), Command::EditElement(original, element) => Some((*original, element.parent)), _ => None, }; let Some((port, parent)) = change.filter(|(id, parent)| { project .elements .get(id) .is_some_and(|e| e.kind() == Kind::Port && e.parent != *parent) }) else { return command; }; let mut commands: Vec<_> = project .diagrams .values() .flat_map(|d| { d.nodes .values() .filter(|n| { Some(n.element) != parent && n.port_anchors.iter().any(|a| a.port == port) }) .map(move |n| Command::RemovePortAnchor { diagram: d.id, node: n.id, port, }) }) .collect(); if commands.is_empty() { return command; } commands.push(command); Command::Batch(commands) } #[cfg(test)] mod tests { use super::*; use crate::app::{App, Message}; fn fixture() -> (App, Id, Id, Id) { let mut p = Project::empty(); let a = p.add(Kind::Function, "Transmit", 100., 100.); let b = p.add(Kind::Function, "Alternate owner", 500., 100.); let port = Id::new_v4(); let element = make_port( &p, port, a, "Output", PortContract::FunctionOutput { outgoing_items: vec![], }, ) .unwrap(); p.elements.insert(port, element); (App::with_project(p), a, b, port) } #[test] fn all_border_drags_commit_exact_preview_and_restore_automatic_anchors_in_history() { for side in [ PortAnchorSide::Left, PortAnchorSide::Right, PortAnchorSide::Top, PortAnchorSide::Bottom, ] { let (mut app, owner, _, port_id) = fixture(); let context = app.route_context().unwrap(); let view = project_view(app.project(), context.diagram); let node = view.nodes.iter().find(|n| n.element == owner).unwrap(); let port = view.ports.iter().find(|p| p.element == port_id).unwrap(); let mut gesture = Gesture::new(context, node.clone(), port, point(port.position)); let end = port_anchor_position(node, side, 0.25); gesture.move_to(point(end), 0.7); assert_eq!(gesture.anchor.side, side); assert_eq!(gesture.anchor.offset, 0.25); let before = app.document.bundle().clone(); let _ = app.update(Message::PortAnchor(Event::Commit( context, node.id, gesture.anchor, ))); let after = app.document.bundle().clone(); let projected = project_view(app.project(), context.diagram); assert_eq!( projected.nodes.iter().find(|n| n.id == node.id).unwrap(), node ); assert_eq!( projected .ports .iter() .find(|p| p.element == port_id) .unwrap() .position, end ); let _ = app.update(Message::Undo); assert_eq!(*app.document.bundle(), before); let _ = app.update(Message::Redo); assert_eq!(*app.document.bundle(), after); let latest = app.route_context().unwrap(); let _ = app.update(Message::PortAnchor(Event::Reset(latest, node.id, port_id))); assert!( app.project().diagrams[&context.diagram].nodes[&node.id] .port_anchors .is_empty() ); let _ = app.update(Message::Undo); assert_eq!(*app.document.bundle(), after); } } #[test] fn graphical_port_reparent_clears_old_anchors_in_the_same_history_operation() { let (mut app, owner, next, port) = fixture(); let context = app.route_context().unwrap(); let node = project_view(app.project(), context.diagram) .nodes .iter() .find(|n| n.element == owner) .unwrap() .id; let _ = app.update(Message::PortAnchor(Event::Commit( context, node, PortAnchor { port, side: PortAnchorSide::Top, offset: 0.5, }, ))); let before = app.document.bundle().clone(); let revision = app.document.revision(); let _ = app.update(Message::Command(Command::Reparent { element: port, parent: Some(next), })); assert_eq!(app.document.revision(), revision + 1, "{}", app.status); assert_eq!(app.project().elements[&port].parent, Some(next)); assert!( app.project().diagrams[&context.diagram].nodes[&node] .port_anchors .is_empty() ); let _ = app.update(Message::Undo); assert_eq!(*app.document.bundle(), before); } }