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