453 lines
14 KiB
Rust
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
|
|
);
|
|
}
|