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

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