363 lines
12 KiB
Rust
363 lines
12 KiB
Rust
use crate::{
|
|
app::{App, Message},
|
|
connection_edit,
|
|
connection_tool::Tool,
|
|
diagram::{DiagramCanvas, State},
|
|
model_menu, port_edit,
|
|
};
|
|
use iced::{
|
|
Point, Rectangle, Size, Vector, mouse,
|
|
widget::canvas::{self, Program},
|
|
};
|
|
use oa_core::*;
|
|
|
|
fn fixture() -> (App, Id, Id) {
|
|
let mut p = Project::empty();
|
|
let a = p.add(Kind::Function, "Transmit", 100., 200.);
|
|
let b = p.add(Kind::Function, "Receive", 500., 200.);
|
|
let output = Id::new_v4();
|
|
let input = Id::new_v4();
|
|
p.elements.insert(
|
|
output,
|
|
make_port(
|
|
&p,
|
|
output,
|
|
a,
|
|
"Send",
|
|
PortContract::FunctionOutput {
|
|
outgoing_items: vec![],
|
|
},
|
|
)
|
|
.unwrap(),
|
|
);
|
|
p.elements.insert(
|
|
input,
|
|
make_port(
|
|
&p,
|
|
input,
|
|
b,
|
|
"Receive",
|
|
PortContract::FunctionInput {
|
|
incoming_items: vec![],
|
|
},
|
|
)
|
|
.unwrap(),
|
|
);
|
|
(
|
|
{
|
|
let mut app = App::with_project(p);
|
|
app.pan = Vector::new(20., 30.);
|
|
app
|
|
},
|
|
output,
|
|
input,
|
|
)
|
|
}
|
|
fn bounds() -> Rectangle {
|
|
Rectangle::new(Point::new(260., 100.), Size::new(2200., 1600.))
|
|
}
|
|
fn screen(p: Position, zoom: f32) -> Point {
|
|
Point::new(bounds().x + p.x * zoom + 20., bounds().y + p.y * zoom + 30.)
|
|
}
|
|
fn canvas(app: &App, zoom: f32) -> DiagramCanvas<'_> {
|
|
DiagramCanvas {
|
|
project: app.project(),
|
|
view_index: None,
|
|
diagram: app.active_diagram.unwrap(),
|
|
selected: app.selected,
|
|
navigation_focus: None,
|
|
zoom,
|
|
pan: app.pan,
|
|
context: app.route_context().unwrap(),
|
|
selected_bend: None,
|
|
editable: true,
|
|
placing: false,
|
|
connection: app.connection.as_ref(),
|
|
}
|
|
}
|
|
fn event(
|
|
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 batch(app: &App, state: &mut State, start: Point, end: Point, zoom: f32) -> Vec<Message> {
|
|
let c = canvas(app, zoom);
|
|
[
|
|
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| event(&c, state, e, end))
|
|
.collect()
|
|
}
|
|
fn assert_history(app: &mut App, before: &oa_core::source_project::SourceBundle, revision: u64) {
|
|
assert_eq!(app.document.revision(), revision + 1);
|
|
let after = app.document.bundle().clone();
|
|
assert_eq!(
|
|
oa_core::source_project::compile_bundle(&after, Default::default())
|
|
.unwrap()
|
|
.decoded
|
|
.project,
|
|
*app.project()
|
|
);
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.document.bundle(), before);
|
|
let _ = app.update(Message::Redo);
|
|
assert_eq!(app.document.bundle(), &after);
|
|
}
|
|
|
|
#[test]
|
|
fn coalesced_fe_ports_commit_once_through_app_and_source_history() {
|
|
for zoom in [0.4, 1., 2.] {
|
|
let (mut app, output, input) = fixture();
|
|
let before = app.document.bundle().clone();
|
|
let revision = app.document.revision();
|
|
let _ = app.update(Message::Connection(connection_edit::Event::Choose(Some(
|
|
Tool::FunctionalExchange,
|
|
))));
|
|
let view = projection::project_view(app.project(), app.active_diagram.unwrap());
|
|
let at = |id| {
|
|
screen(
|
|
view.ports
|
|
.iter()
|
|
.find(|p| p.element == id)
|
|
.unwrap()
|
|
.position,
|
|
zoom,
|
|
)
|
|
};
|
|
let messages = batch(&app, &mut State::default(), at(output), at(input), zoom);
|
|
assert_eq!(messages.len(), 2);
|
|
assert!(
|
|
matches!(&messages[0],Message::Connection(connection_edit::Event::Endpoint(_,id))if *id==output)
|
|
);
|
|
assert!(
|
|
matches!(&messages[1],Message::Connection(connection_edit::Event::Endpoint(_,id))if *id==input)
|
|
);
|
|
for message in messages {
|
|
let _ = app.update(message);
|
|
}
|
|
assert_eq!(app.project().relations.len(), 1);
|
|
assert_eq!(app.project().elements.len(), 4);
|
|
assert!(
|
|
matches!(app.project().relations[&app.selected].body,RelationBody::FunctionalExchange{source_port,target_port,..}if source_port==output&&target_port==input)
|
|
);
|
|
assert!(app.connection.is_none());
|
|
assert_history(&mut app, &before, revision);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn coalesced_port_drag_commits_once_through_app_and_source_history() {
|
|
for zoom in [0.4, 1., 2.] {
|
|
let (mut app, output, _) = fixture();
|
|
let before = app.document.bundle().clone();
|
|
let revision = app.document.revision();
|
|
let view = projection::project_view(app.project(), app.active_diagram.unwrap());
|
|
let port = view.ports.iter().find(|p| p.element == output).unwrap();
|
|
let owner = view.nodes.iter().find(|n| n.id == port.owner).unwrap();
|
|
let dest = projection::port_anchor_position(owner, PortAnchorSide::Top, 0.35);
|
|
let messages = batch(
|
|
&app,
|
|
&mut State::default(),
|
|
screen(port.position, zoom),
|
|
screen(dest, zoom),
|
|
zoom,
|
|
);
|
|
assert_eq!(messages.len(), 2);
|
|
assert!(matches!(&messages[0],Message::Select(id)if *id==output));
|
|
assert!(
|
|
matches!(&messages[1],Message::PortAnchor(port_edit::Event::Commit(_,_,anchor))if anchor.port==output&&anchor.side==PortAnchorSide::Top)
|
|
);
|
|
let node = owner.id;
|
|
for message in messages {
|
|
let _ = app.update(message);
|
|
}
|
|
assert_eq!(app.project().elements.len(), 4);
|
|
assert!(app.project().relations.is_empty());
|
|
assert_eq!(
|
|
app.project().diagrams[&app.active_diagram.unwrap()].nodes[&node].port_anchors[0].port,
|
|
output
|
|
);
|
|
assert_history(&mut app, &before, revision);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn menu_captured_motion_must_not_turn_following_click_into_old_port_drag() {
|
|
let (mut app, output, input) = fixture();
|
|
let before = app.document.bundle().clone();
|
|
let view = projection::project_view(app.project(), app.active_diagram.unwrap());
|
|
let a = screen(
|
|
view.ports
|
|
.iter()
|
|
.find(|p| p.element == output)
|
|
.unwrap()
|
|
.position,
|
|
1.,
|
|
);
|
|
let b = screen(
|
|
view.ports
|
|
.iter()
|
|
.find(|p| p.element == input)
|
|
.unwrap()
|
|
.position,
|
|
1.,
|
|
);
|
|
let mut state = State::default();
|
|
let _ = event(
|
|
&canvas(&app, 1.),
|
|
&mut state,
|
|
mouse::Event::CursorMoved { position: a },
|
|
a,
|
|
);
|
|
let right = event(
|
|
&canvas(&app, 1.),
|
|
&mut state,
|
|
mouse::Event::ButtonPressed(mouse::Button::Right),
|
|
a,
|
|
)
|
|
.unwrap();
|
|
let _ = app.update(right);
|
|
assert!(app.model_menu.is_some());
|
|
// The overlay consumes the right release, movement to B, outside press and
|
|
// its release. None of those events reach the underlying canvas.
|
|
let _ = app.update(Message::Menu(model_menu::Event::Close));
|
|
let press = event(
|
|
&canvas(&app, 1.),
|
|
&mut state,
|
|
mouse::Event::ButtonPressed(mouse::Button::Left),
|
|
b,
|
|
)
|
|
.unwrap();
|
|
let release = event(
|
|
&canvas(&app, 1.),
|
|
&mut state,
|
|
mouse::Event::ButtonReleased(mouse::Button::Left),
|
|
b,
|
|
);
|
|
let picked = match &press {
|
|
Message::Select(id) => Some(*id),
|
|
_ => None,
|
|
};
|
|
let release_debug = format!("{release:?}");
|
|
let _ = app.update(press);
|
|
if let Some(message) = release {
|
|
let _ = app.update(message);
|
|
}
|
|
assert_eq!(
|
|
picked,
|
|
Some(input),
|
|
"Click at B picked another port; following release: {release_debug}"
|
|
);
|
|
assert_eq!(
|
|
app.document.bundle(),
|
|
&before,
|
|
"A stationary click after closing a menu must not edit port geometry"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn drag_from_already_selected_stationary_port_keeps_source_between_batches() {
|
|
let (mut app, output, _) = fixture();
|
|
let view = projection::project_view(app.project(), app.active_diagram.unwrap());
|
|
let port = view.ports.iter().find(|p| p.element == output).unwrap();
|
|
let owner = view.nodes.iter().find(|n| n.id == port.owner).unwrap();
|
|
let a = screen(port.position, 1.);
|
|
let b = screen(
|
|
projection::port_anchor_position(owner, PortAnchorSide::Top, 0.35),
|
|
1.,
|
|
);
|
|
let mut state = State::default();
|
|
let selected = batch(&app, &mut state, a, a, 1.);
|
|
for message in selected {
|
|
let _ = app.update(message);
|
|
}
|
|
assert_eq!(app.selected, output);
|
|
// No new motion to A occurs: the previous selection ended there. Winit
|
|
// batches press(A), motion(B), release(B) with cursor B on every event.
|
|
let c = canvas(&app, 1.);
|
|
let messages: Vec<_> = [
|
|
mouse::Event::ButtonPressed(mouse::Button::Left),
|
|
mouse::Event::CursorMoved { position: b },
|
|
mouse::Event::ButtonReleased(mouse::Button::Left),
|
|
]
|
|
.into_iter()
|
|
.filter_map(|e| event(&c, &mut state, e, b))
|
|
.collect();
|
|
assert!(
|
|
matches!(messages.first(),Some(Message::Select(id)) if *id==output),
|
|
"Stationary source was lost between batches: {messages:?}"
|
|
);
|
|
assert!(
|
|
matches!(messages.last(),Some(Message::PortAnchor(port_edit::Event::Commit(_,_,anchor))) if anchor.port==output)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn modal_and_stale_epoch_block_queued_connection_and_port_commits() {
|
|
for is_connection in [false, true] {
|
|
for barrier in ["menu", "modal", "epoch"] {
|
|
let (mut app, output, input) = fixture();
|
|
let before = app.document.bundle().clone();
|
|
if is_connection {
|
|
let _ = app.update(Message::Connection(connection_edit::Event::Choose(Some(
|
|
Tool::FunctionalExchange,
|
|
))));
|
|
}
|
|
let view = projection::project_view(app.project(), app.active_diagram.unwrap());
|
|
let out = view.ports.iter().find(|p| p.element == output).unwrap();
|
|
let target = if is_connection {
|
|
view.ports
|
|
.iter()
|
|
.find(|p| p.element == input)
|
|
.unwrap()
|
|
.position
|
|
} else {
|
|
projection::port_anchor_position(
|
|
view.nodes.iter().find(|n| n.id == out.owner).unwrap(),
|
|
PortAnchorSide::Top,
|
|
0.35,
|
|
)
|
|
};
|
|
let messages = batch(
|
|
&app,
|
|
&mut State::default(),
|
|
screen(out.position, 1.),
|
|
screen(target, 1.),
|
|
1.,
|
|
);
|
|
match barrier {
|
|
"menu" => {
|
|
let _ = app.update(model_menu::open(output, Point::ORIGIN));
|
|
}
|
|
"modal" => {
|
|
let _ = app.update(Message::AddRelation);
|
|
}
|
|
_ => {
|
|
let _ = app.update(Message::Escape);
|
|
}
|
|
}
|
|
for message in messages {
|
|
let _ = app.update(message);
|
|
}
|
|
assert_eq!(
|
|
app.document.bundle(),
|
|
&before,
|
|
"{barrier}; connection={is_connection}"
|
|
);
|
|
}
|
|
}
|
|
}
|