349 lines
14 KiB
Rust
349 lines
14 KiB
Rust
//! 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);
|
|
}
|