Files
ARCADIA-Studio/crates/oa-desktop/src/port_edit.rs
T

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);
}
}