227 lines
7.6 KiB
Rust
227 lines
7.6 KiB
Rust
//! 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);
|
|
}
|
|
}
|