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

453 lines
14 KiB
Rust

use super::*;
use crate::app::{App, Page};
use iced::widget::canvas::Program;
fn fixture() -> App {
let mut p = Project::empty();
let a = p.add(Kind::Activity, "Send", 100., 150.);
let b = p.add(Kind::Activity, "Receive", 650., 150.);
let edge = p.relate(RelationKind::Interaction, "Report", a, b);
let diagram = p.ensure_diagram(Layer::Oa);
p.diagrams
.get_mut(&diagram)
.unwrap()
.routes
.push(EdgeRoute {
edge,
waypoints: vec![Position { x: 450., y: 350. }],
label: Some(EdgeLabel {
fraction: 0.4,
offset: Position { x: 5., y: 10. },
}),
source_attachment: None,
target_attachment: Some(EdgeAttachment {
side: PortAnchorSide::Left,
offset: 0.6,
}),
});
let mut app = App::with_project(p);
app.page = Page::Diagram;
app.active_diagram = Some(diagram);
let _ = app.update(Message::Select(edge));
app
}
fn canvas(app: &App, zoom: f32) -> DiagramCanvas<'_> {
DiagramCanvas {
selection: None,
allocation: None,
project: app.project(),
view_index: None,
diagram: app.active_diagram.unwrap(),
selected: app.selected,
navigation_focus: None,
zoom,
pan: Vector::new(40., 50.),
context: app.route_context().unwrap(),
selected_bend: None,
selected_attachment: app
.selected_attachment
.filter(|(c, e, _)| Some(*c) == app.route_context() && *e == app.selected)
.map(|(_, _, end)| end),
editable: !app.draft_dirty() && !app.source_dirty(),
placing: false,
connection: None,
}
}
fn bounds() -> Rectangle {
Rectangle::new(Point::new(120., 80.), Size::new(2400., 1800.))
}
fn screen(c: &DiagramCanvas<'_>, p: Position) -> Point {
Point::new(
bounds().x + p.x * c.zoom + c.pan.x,
bounds().y + p.y * c.zoom + c.pan.y,
)
}
fn send(
c: &DiagramCanvas<'_>,
state: &mut State,
event: mouse::Event,
final_cursor: Point,
) -> Option<Message> {
c.update(
state,
&canvas::Event::Mouse(event),
bounds(),
mouse::Cursor::Available(final_cursor),
)
.and_then(|a| a.into_inner().0)
}
fn positions(c: &DiagramCanvas<'_>) -> (Point, Point) {
let scene = c.scene(&State::default());
let start = *scene.routed.edges[&c.selected].points.first().unwrap();
let node = route_edit::attachment_node(
c.project,
&c.project.diagrams[&c.diagram],
&scene.view,
c.selected,
EdgeEnd::Source,
)
.unwrap();
let end = oa_core::routing::attachment_point(
c.project,
&c.project.diagrams[&c.diagram],
node,
&EdgeAttachment {
side: PortAnchorSide::Top,
offset: 0.25,
},
);
(screen(c, start), screen(c, end))
}
fn drag(app: &App, zoom: f32) -> Message {
let c = canvas(app, zoom);
let (start, end) = positions(&c);
let mut state = State::default();
let messages = [
mouse::Event::CursorMoved { position: start },
mouse::Event::ButtonPressed(mouse::Button::Left),
mouse::Event::CursorMoved { position: end },
mouse::Event::ButtonReleased(mouse::Button::Left),
]
.into_iter()
.filter_map(|e| send(&c, &mut state, e, end))
.collect::<Vec<_>>();
assert_eq!(messages.len(), 1);
messages.into_iter().next().unwrap()
}
#[test]
fn attachment_event_batches_commit_one_source_history_change_at_three_zooms() {
for zoom in [0.4, 1., 2.] {
let mut app = fixture();
let before = app.document.bundle().clone();
let project = app.project().clone();
let revision = app.document.revision();
let message = drag(&app, zoom);
let Message::Route(RouteEvent::CommitAttachment(_, ref route, EdgeEnd::Source, _)) =
message
else {
panic!("attachment commit required")
};
assert_eq!(
route.source_attachment,
Some(EdgeAttachment {
side: PortAnchorSide::Top,
offset: 0.25
})
);
let intended = route.clone();
let _ = app.update(message);
assert_eq!(app.document.revision(), revision + 1);
let mut expected = project;
route_edit::with_route(
expected
.diagrams
.get_mut(&app.active_diagram.unwrap())
.unwrap(),
intended,
);
assert_eq!(
*app.project(),
expected,
"only the intended view attachment changes"
);
let after = app.document.bundle().clone();
assert_eq!(
oa_core::source_project::compile_bundle(&after, Default::default())
.unwrap()
.decoded
.project,
expected
);
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle(), &before);
let _ = app.update(Message::Redo);
assert_eq!(app.document.bundle(), &after);
}
}
#[test]
fn attachment_stationary_return_and_escape_leave_source_unchanged() {
let mut app = fixture();
let before = app.document.bundle().clone();
let c = canvas(&app, 1.);
let (start, end) = positions(&c);
let mut state = State::default();
for event in [
mouse::Event::CursorMoved { position: start },
mouse::Event::ButtonPressed(mouse::Button::Left),
mouse::Event::CursorMoved { position: end },
mouse::Event::CursorMoved { position: start },
mouse::Event::ButtonReleased(mouse::Button::Left),
] {
assert!(send(&c, &mut state, event, start).is_none());
}
for event in [
mouse::Event::CursorMoved { position: start },
mouse::Event::ButtonPressed(mouse::Button::Left),
mouse::Event::CursorMoved { position: end },
] {
assert!(send(&c, &mut state, event, end).is_none());
}
let _ = app.update(Message::Escape);
assert!(
send(
&canvas(&app, 1.),
&mut state,
mouse::Event::ButtonReleased(mouse::Button::Left),
end
)
.is_none()
);
assert_eq!(app.document.bundle(), &before);
}
#[test]
fn attachment_camera_layout_focus_and_outside_release_cancel_after_motion() {
for cancel in 0..8 {
let app = fixture();
let before = app.document.bundle().clone();
let mut c = canvas(&app, 1.);
let (start, end) = positions(&c);
let mut state = State::default();
for event in [
mouse::Event::CursorMoved { position: start },
mouse::Event::ButtonPressed(mouse::Button::Left),
mouse::Event::CursorMoved { position: end },
] {
assert!(send(&c, &mut state, event, end).is_none());
}
assert!(state.route_drag.as_ref().is_some_and(|g| g.changed));
let mut current_bounds = bounds();
let mut release = end;
match cancel {
0 => c.zoom = 2.,
1 => c.pan.x += 20.,
2 => current_bounds.x += 20.,
3 => current_bounds.height -= 100.,
4..=6 => {
let event = match cancel {
4 => canvas::Event::Window(iced::window::Event::Unfocused),
5 => canvas::Event::Window(iced::window::Event::Resized(Size::new(800., 600.))),
_ => canvas::Event::Mouse(mouse::Event::CursorLeft),
};
let message = c
.update(
&mut state,
&event,
current_bounds,
mouse::Cursor::Available(end),
)
.and_then(|a| a.into_inner().0);
assert!(message.is_none());
}
_ => {
release = Point::new(-100., -100.);
assert!(
send(
&c,
&mut state,
mouse::Event::CursorMoved { position: release },
release
)
.is_none()
);
}
}
let message = c
.update(
&mut state,
&canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
current_bounds,
mouse::Cursor::Available(release),
)
.and_then(|a| a.into_inner().0);
assert!(message.is_none(), "cancellation case {cancel}");
assert!(state.route_drag.is_none(), "cancellation case {cancel}");
assert_eq!(app.document.bundle(), &before);
}
}
#[test]
fn attachment_wheel_and_middle_pan_cancel_before_same_batch_left_release() {
for wheel in [true, false] {
let mut app = fixture();
let before = app.document.bundle().clone();
let revision = app.document.revision();
let c = canvas(&app, 1.);
let (start, end) = positions(&c);
let mut state = State::default();
for event in [
mouse::Event::CursorMoved { position: start },
mouse::Event::ButtonPressed(mouse::Button::Left),
mouse::Event::CursorMoved { position: end },
] {
assert!(send(&c, &mut state, event, end).is_none());
}
let camera_message = send(
&c,
&mut state,
if wheel {
mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines { x: 0., y: 1. },
}
} else {
mouse::Event::ButtonPressed(mouse::Button::Middle)
},
end,
);
assert_eq!(camera_message.is_some(), wheel);
assert!(state.route_drag.is_none());
// Deliberately retain the same canvas and defer App camera dispatch.
assert!(
send(
&c,
&mut state,
mouse::Event::ButtonReleased(mouse::Button::Left),
end
)
.is_none()
);
if !wheel {
assert!(
send(
&c,
&mut state,
mouse::Event::ButtonReleased(mouse::Button::Middle),
end
)
.is_none()
);
}
if let Some(message) = camera_message {
assert!(matches!(message, Message::Zoom(_)));
let _ = app.update(message);
}
assert_eq!(app.document.revision(), revision);
assert_eq!(app.document.bundle(), &before);
}
}
#[test]
fn attachment_fast_release_uses_release_position_and_stays_in_one_view() {
let mut app = fixture();
let original = app.active_diagram.unwrap();
let _ = app.update(Message::DuplicateDiagram(original));
let other = app.active_diagram.unwrap();
assert_ne!(original, other);
app.active_diagram = Some(original);
let edge = app.project().diagrams[&original].routes[0].edge;
let _ = app.update(Message::Select(edge));
let untouched = app.project().diagrams[&other].clone();
let c = canvas(&app, 0.7);
let (start, end) = positions(&c);
let mut state = State::default();
assert!(
send(
&c,
&mut state,
mouse::Event::ButtonPressed(mouse::Button::Left),
start
)
.is_none()
);
let message = send(
&c,
&mut state,
mouse::Event::ButtonReleased(mouse::Button::Left),
end,
)
.expect("release completes drag without final move");
let _ = app.update(message);
assert_eq!(
app.project().diagrams[&original].routes[0].source_attachment,
Some(EdgeAttachment {
side: PortAnchorSide::Top,
offset: 0.25
})
);
assert_eq!(app.project().diagrams[&other], untouched);
}
#[test]
fn attachment_stale_context_wrong_placement_and_unowned_route_fields_are_rejected() {
let mut app = fixture();
let before = app.document.bundle().clone();
let message = drag(&app, 1.);
let Message::Route(RouteEvent::CommitAttachment(context, route, end, node)) = message else {
panic!()
};
let mut changed = route.clone();
changed.waypoints.clear();
let _ = app.update(Message::Route(RouteEvent::CommitAttachment(
context, changed, end, node,
)));
let _ = app.update(Message::Route(RouteEvent::CommitAttachment(
context,
route.clone(),
end,
Id::new_v4(),
)));
let _ = app.update(Message::Escape);
let _ = app.update(Message::Route(RouteEvent::CommitAttachment(
context, route, end, node,
)));
assert_eq!(app.document.bundle(), &before);
}
#[test]
fn attachment_reset_preserves_opposite_end_pins_label_and_semantics() {
let mut app = fixture();
let before = app.project().clone();
let _ = app.update(Message::Route(RouteEvent::ResetAttachment(EdgeEnd::Target)));
let mut expected = before;
expected
.diagrams
.get_mut(&app.active_diagram.unwrap())
.unwrap()
.routes[0]
.target_attachment = None;
assert_eq!(*app.project(), expected);
let revision = app.document.revision();
let _ = app.update(Message::Route(RouteEvent::ResetAttachment(EdgeEnd::Target)));
assert_eq!(app.document.revision(), revision);
}
#[test]
fn coincident_attachment_handles_honor_explicit_end_choice() {
let mut app = fixture();
let _ = app.update(Message::Route(RouteEvent::ChooseAttachment(
EdgeEnd::Target,
)));
let c = canvas(&app, 1.);
let core_routed = Arc::new(route_diagram(
c.project,
&c.project.diagrams[&c.diagram],
&projection::project_view(c.project, c.diagram),
));
let mut scene = Scene {
label_layout: None,
profile: label_layout_experiment::Profile::Baseline,
font_version: None,
work: None,
view: projection::project_view(c.project, c.diagram),
routed: core_routed.clone(),
core_routed,
};
let routed = Arc::make_mut(&mut scene.routed);
let edge = routed.edges.get_mut(&c.selected).unwrap();
let start = *edge.points.first().unwrap();
*edge.points.last_mut().unwrap() = start;
assert_eq!(
c.attachment_at(&scene, point(start)).unwrap().0,
EdgeEnd::Target
);
let mut source = c;
source.selected_attachment = Some(EdgeEnd::Source);
assert_eq!(
source.attachment_at(&scene, point(start)).unwrap().0,
EdgeEnd::Source
);
}