//! Controller identity and actual presentation input, independent of App routing. use super::*; use oa_core::{Diagram, DiagramLayout, ProjectionKind, ScenarioLanePosition, ScenarioLayout}; fn saved(p: &mut Project, owner: Id, lanes: &[(Id, f32)]) -> Representation { let id = Id::new_v4(); p.diagrams.insert( id, Diagram { id, alias: format!("saved_{}", p.diagrams.len()), name: "Saved scenario".into(), description: String::new(), layer: p.elements[&owner].layer(), projection: ProjectionKind::Scenario { scenario: owner }, view_options: Default::default(), nodes: Default::default(), routes: vec![], layout: Some(DiagramLayout::Scenario(ScenarioLayout { lanes: lanes .iter() .map(|&(lifeline, x)| ScenarioLanePosition { lifeline, x }) .collect(), messages: vec![], })), }, ); let errors: Vec<_> = oa_core::validation::validate(p) .into_iter() .filter(|issue| issue.severity == oa_core::validation::Severity::Error) .collect(); assert!(errors.is_empty(), "{errors:?}"); Representation::Saved { diagram: id } } #[test] fn saved_visits_distinguish_same_owner_roundtrips_and_suspended_return_without_editing() { let mut f = direct_tests::fixture(); let a = saved(&mut f.project, f.owner, &[(f.sender, -500.)]); let b = saved(&mut f.project, f.owner, &[(f.sender, 900.)]); let original = f.project.clone(); let document = Id::new_v4(); let mut state = State::default(); assert!(state.enter_representation(document, f.owner, a)); let first = state.context(document, f.owner, 7, 9).unwrap(); assert!(!state.enter_representation(document, f.owner, a)); assert_eq!(state.context(document, f.owner, 7, 9), Some(first)); state.presentation.pan = iced::Vector::new(23., -17.); state.leave(); assert_eq!(state.representation(), Some(a)); assert_eq!(state.owner(), Some(f.owner)); assert!(state.context(document, f.owner, 7, 9).is_none()); assert!(state.enter_representation(document, f.owner, a)); let returned = state.context(document, f.owner, 7, 9).unwrap(); assert_ne!(returned.visit, first.visit); assert_eq!(state.presentation.pan, iced::Vector::new(23., -17.)); assert!(state.enter_representation(document, f.owner, b)); let middle = state.context(document, f.owner, 7, 9).unwrap(); assert!(state.enter_representation(document, f.owner, a)); let last = state.context(document, f.owner, 7, 9).unwrap(); assert_ne!(last.visit, returned.visit); assert_ne!(last.visit, middle.visit); assert!(!state.coordinate_input(&f.project, first)); assert!(state.coordinate_input(&f.project, last)); let session = Id::new_v4(); let mount = Id::new_v4(); assert_ne!( focus::Stamp::new(first, session, mount), focus::Stamp::new(last, session, mount) ); assert_eq!(f.project, original); state.forget_view(); assert_eq!(state.representation(), None); assert_eq!(state.owner(), None); assert!(state.context(document, f.owner, 7, 9).is_none()); } #[test] fn saved_caller_restores_exact_representation_with_fresh_visit_and_rejects_deleted_view() { let (mut project, [a, b, _], [reference, _, _]) = direct_tests::navigation_fixture(); let view = saved(&mut project, a, &[]); let original = project.clone(); let document = Id::new_v4(); let mut state = State::default(); state.enter_representation(document, a, view); let initial = state.context(document, a, 0, 0).unwrap(); state.presentation.pan = iced::Vector::new(11., 29.); state.details_offset = iced::Vector::new(0., 71.); assert_eq!(state.open_use(&project, initial, reference).unwrap(), b); state.enter(document, b); let callee = state.context(document, b, 0, 0).unwrap(); let frame = state.back(&project, callee).unwrap(); assert_eq!(frame.representation, view); state.enter_representation(document, a, frame.representation); let returned = state.context(document, a, 0, 0).unwrap(); state.restore_caller(returned, frame); assert_ne!(returned.visit, initial.visit); assert_eq!(state.representation(), Some(view)); assert_eq!(state.presentation.pan, iced::Vector::new(11., 29.)); assert!(state.scroll_restore(initial, ScrollArea::Page).is_none()); assert_eq!( state .scroll_restore(returned, ScrollArea::Page) .unwrap() .offset, iced::Vector::new(0., 71.) ); assert_eq!(project, original); state.open_use(&project, returned, reference).unwrap(); state.enter(document, b); let Representation::Saved { diagram } = view else { unreachable!() }; project.diagrams.remove(&diagram); let callee = state.context(document, b, 0, 0).unwrap(); assert!(state.back(&project, callee).is_none()); } #[test] fn complete_fallback_scene_selects_but_cannot_capture_or_finish_coordinate_gestures() { let mut f = direct_tests::fixture(); // Equal authored lane centers are structurally valid, but cannot render a distinct-endpoint call. let rep = saved( &mut f.project, f.owner, &[(f.sender, 900.), (f.receiver, 900.)], ); let original = f.project.clone(); let mut state = State::default(); state.enter_representation(Id::new_v4(), f.owner, rep); let document = state.presentation.identity.unwrap().0; let context = state.context(document, f.owner, 0, 0).unwrap(); let shown = state.display(&f.project, f.owner, f.call).unwrap(); assert!(matches!( shown.status(), presentation::PresentationStatus::Fallback { .. } )); assert!(!state.coordinate_input(&f.project, context)); let automatic = presentation::present( &f.project, Representation::Derived { owner: f.owner }, f.call, ); let automatic = automatic.scene().unwrap(); assert_eq!(shown.scene().unwrap().render_lanes, automatic.render_lanes); assert_eq!(shown.scene().unwrap().activations, automatic.activations); assert!(!automatic.activations.is_empty()); let mut scene = shown.into_scene().unwrap(); scene.coordinate_input = false; let line = scene.lines.iter().find(|line| line.id == f.call).unwrap(); let point = Point::new( (line.points[0].x + line.points.last().unwrap().x) / 2., line.points[0].y, ); assert_eq!(scene.hit(point), Some(f.call)); let bounds = Rectangle::with_size(Size::new(1600., 1000.)); for tool in [ CanvasTool::Select, CanvasTool::Message(MessageStyle::CallReply), CanvasTool::Add(Tool::Reuse), ] { scene.interaction = Some((context, tool, true)); let mut input = SequenceInput::default(); let mut messages = vec![]; for event in [ mouse::Event::CursorMoved { position: point }, mouse::Event::ButtonPressed(mouse::Button::Left), mouse::Event::CursorMoved { position: Point::new(point.x, point.y + 100.), }, mouse::Event::ButtonReleased(mouse::Button::Left), ] { if let Some(message) = canvas::Program::update( &scene, &mut input, &canvas::Event::Mouse(event), bounds, mouse::Cursor::Available(point), ) .and_then(|action| action.into_inner().0) { messages.push(message); } } assert_eq!(messages.len(), 1); assert!( matches!(messages[0], Message::ScenarioCanvas(CanvasEvent::Select(c, id)) if c == context && id == f.call) ); assert!(input.press.is_none() && input.source.is_none()); } // Capture from an actual Ready presentation, then revoke coordinate input // with the complete fallback frame of the same Saved representation. let Representation::Saved { diagram } = rep else { unreachable!() }; let mut ready_project = f.project.clone(); ready_project.diagrams.get_mut(&diagram).unwrap().layout = Some(DiagramLayout::Scenario(ScenarioLayout::default())); let ready_original = ready_project.clone(); let ready = state.display(&ready_project, f.owner, f.call).unwrap(); assert!(matches!( ready.status(), presentation::PresentationStatus::Ready )); let mut ready_scene = ready.into_scene().unwrap(); let line = ready_scene .lines .iter() .find(|line| line.id == f.call) .unwrap(); let ready_point = Point::new( (line.points[0].x + line.points.last().unwrap().x) / 2., line.points[0].y, ); let hit = ready_scene.hit_item(ready_point).unwrap(); assert_eq!(hit.id, f.call); assert_eq!(hit.role, SceneHitRole::MessageBody); let stamp = spacing::Stamp { context, camera_epoch: 17, }; ready_scene.coordinate_input = true; ready_scene.interaction = Some((context, CanvasTool::Select, true)); ready_scene.spacing_stamp = Some(stamp); let mut input = SequenceInput::default(); let begin = canvas::Program::update( &ready_scene, &mut input, &canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), bounds, mouse::Cursor::Available(ready_point), ) .and_then(|action| action.into_inner().0) .unwrap(); let Message::ScenarioCanvas(CanvasEvent::Spacing(spacing::Event::Begin { stamp: captured_stamp, token, target: spacing::Target::Message(target), .. })) = begin else { panic!("Ready Saved message must begin a stamped spacing gesture"); }; assert_eq!(captured_stamp, stamp); assert_eq!(target, f.call); assert!(matches!(input.press, Some(SequencePress::Spacing { stamp: held_stamp, token: held_token }) if held_stamp == stamp && held_token == token)); scene.interaction = Some((context, CanvasTool::Select, true)); assert!(!scene.coordinate_input && scene.spacing_stamp.is_none()); let release = canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)); let result = canvas::Program::update( &scene, &mut input, &release, bounds, mouse::Cursor::Available(ready_point), ) .and_then(|action| action.into_inner().0); assert!( matches!(result, Some(Message::ScenarioCanvas(CanvasEvent::Spacing( spacing::Event::Cancel { stamp: cancelled_stamp, token: cancelled_token } ))) if cancelled_stamp == stamp && cancelled_token == token) ); assert!(input.press.is_none() && input.source.is_none()); assert!( canvas::Program::update( &scene, &mut input, &release, bounds, mouse::Cursor::Available(ready_point), ) .and_then(|action| action.into_inner().0) .is_none() ); assert_eq!(ready_project, ready_original); assert_eq!(f.project, original); } #[test] fn saved_camera_fits_actual_negative_origin_and_tiny_scale_without_authored_mutation() { let mut f = direct_tests::fixture(); let rep = saved( &mut f.project, f.owner, &[(f.sender, -1_000_000.), (f.receiver, 1_000_000.)], ); let original = f.project.clone(); let mut state = State::default(); state.enter_representation(Id::new_v4(), f.owner, rep); state.viewport(Size::new(700., 500.)); assert!( state.presentation.fit, "new saved views await their real viewport in Fit mode" ); assert!( !state.camera_zoom(&f.project, f.owner, None), "already-active Fit is idempotent" ); let shown = state.display(&f.project, f.owner, f.owner).unwrap(); assert!(matches!( shown.status(), presentation::PresentationStatus::Ready )); let bounds = shown.bounds().unwrap(); assert!(bounds.x < -1_000_000. && bounds.x + bounds.width > 1_000_000.); let fitted = state.presentation.camera(bounds); assert!(fitted.scale > 0. && fitted.scale < 0.001); let left = bounds.x * fitted.scale + fitted.pan.x; let right = (bounds.x + bounds.width) * fitted.scale + fitted.pan.x; assert!((left - 8.).abs() < 0.001 && (right - 692.).abs() < 0.001); assert!(state.camera( &f.project, f.owner, super::super::canvas_workspace::Event::Pan(iced::Vector::new(13., -7.)) )); let moved = state.presentation.camera(bounds); assert_eq!(moved.scale, fitted.scale); assert_eq!(moved.pan, fitted.pan + iced::Vector::new(13., -7.)); assert!(!state.presentation.fit); assert_eq!(f.project, original); } #[test] fn deleted_saved_identity_remains_unavailable_and_never_falls_into_derived_geometry() { let mut f = direct_tests::fixture(); let rep = saved(&mut f.project, f.owner, &[]); let mut state = State::default(); let document = Id::new_v4(); state.enter_representation(document, f.owner, rep); let Representation::Saved { diagram } = rep else { unreachable!() }; f.project.diagrams.remove(&diagram); let original = f.project.clone(); let shown = state.display(&f.project, f.owner, f.owner).unwrap(); assert_eq!(shown.requested(), rep); assert!(matches!( shown.status(), presentation::PresentationStatus::Unavailable { .. } )); assert!(shown.scene().is_none() && shown.bounds().is_none()); assert_eq!(state.owner(), Some(f.owner)); assert!(state.context(document, f.owner, 0, 0).is_some()); assert!(!state.camera_zoom(&f.project, f.owner, None)); assert_eq!(f.project, original); }