419 lines
15 KiB
Rust
419 lines
15 KiB
Rust
//! Independent first-layout navigation oracles, through the real App boundary.
|
|
use super::*;
|
|
use crate::diagram_usage::{self as usage, Target, Usage};
|
|
use iced::{Size, Vector};
|
|
use std::collections::BTreeMap;
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
enum Canvas {
|
|
Architecture,
|
|
Process,
|
|
Scenario,
|
|
State,
|
|
}
|
|
const CANVASES: [Canvas; 4] = [
|
|
Canvas::Architecture,
|
|
Canvas::Process,
|
|
Canvas::Scenario,
|
|
Canvas::State,
|
|
];
|
|
const WIDE: Size = Size::new(1200., 700.);
|
|
const ACTUAL: Size = Size::new(500., 300.);
|
|
|
|
fn send(app: &mut App, message: Message) {
|
|
let _ = app.update(message);
|
|
}
|
|
|
|
fn fixture(canvas: Canvas) -> (App, Usage) {
|
|
let mut p = Project::empty();
|
|
let work = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Respond", None)
|
|
.unwrap();
|
|
let (surface, subject) = match canvas {
|
|
Canvas::Architecture => {
|
|
let diagram = p.ensure_diagram(Layer::Oa);
|
|
p.place(
|
|
diagram,
|
|
work,
|
|
Position {
|
|
x: 12_000.,
|
|
y: 9_000.,
|
|
},
|
|
);
|
|
(Surface::Saved(diagram), work)
|
|
}
|
|
Canvas::Process | Canvas::Scenario => {
|
|
let kind = if matches!(canvas, Canvas::Process) {
|
|
Kind::Process
|
|
} else {
|
|
Kind::Scenario
|
|
};
|
|
let owner = p.add_element(kind, Layer::Oa, "Response", None).unwrap();
|
|
let occurrence = p
|
|
.add_element(Kind::Occurrence, Layer::Oa, "Respond here", Some(owner))
|
|
.unwrap();
|
|
*p.elements
|
|
.get_mut(&occurrence)
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = SharedBody::Occurrence {
|
|
body: OccurrenceBody::Action {
|
|
action: Some(work),
|
|
lifeline: None,
|
|
},
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
let steps = p.elements.get_mut(&owner).unwrap().steps_mut().unwrap();
|
|
if !steps.contains(&occurrence) {
|
|
steps.push(occurrence);
|
|
}
|
|
(
|
|
if matches!(canvas, Canvas::Process) {
|
|
Surface::Process(owner)
|
|
} else {
|
|
Surface::Scenario(owner)
|
|
},
|
|
occurrence,
|
|
)
|
|
}
|
|
Canvas::State => {
|
|
let machine = p
|
|
.add_element(Kind::StateMachine, Layer::Oa, "Modes", None)
|
|
.unwrap();
|
|
let region = p
|
|
.add_element(Kind::Region, Layer::Oa, "Main", Some(machine))
|
|
.unwrap();
|
|
let state = p
|
|
.add_element(Kind::State, Layer::Oa, "Ready", Some(region))
|
|
.unwrap();
|
|
let diagram = Id::new_v4();
|
|
p.diagrams.insert(
|
|
diagram,
|
|
Diagram {
|
|
id: diagram,
|
|
alias: "ready_view".into(),
|
|
name: "Ready view".into(),
|
|
description: String::new(),
|
|
layer: Layer::Oa,
|
|
projection: ProjectionKind::StateMachine { machine },
|
|
nodes: BTreeMap::new(),
|
|
layout: None,
|
|
routes: vec![],
|
|
view_options: Default::default(),
|
|
},
|
|
);
|
|
p.place(
|
|
diagram,
|
|
state,
|
|
Position {
|
|
x: 8_000.,
|
|
y: 11_000.,
|
|
},
|
|
);
|
|
(Surface::Saved(diagram), state)
|
|
}
|
|
};
|
|
let mut app = App::with_project(p);
|
|
let found = usage::collect(
|
|
app.project(),
|
|
subject,
|
|
&app.behavior.topology,
|
|
&app.behavior.scenario,
|
|
)
|
|
.into_iter()
|
|
.find(|u| u.surface == surface && u.target != Target::Whole)
|
|
.unwrap();
|
|
app.canvas_size = Some(WIDE);
|
|
app.behavior.diagram.viewport = Some(WIDE);
|
|
if let Surface::Process(owner) = surface {
|
|
app.behavior.topology.enter(app.canvas_document, owner);
|
|
app.behavior.topology.viewport(WIDE);
|
|
}
|
|
if let Surface::Scenario(owner) = surface {
|
|
app.behavior.scenario.enter(app.canvas_document, owner);
|
|
app.behavior.scenario.viewport(WIDE);
|
|
}
|
|
app.page = Page::Text;
|
|
(app, found)
|
|
}
|
|
|
|
fn measurement(
|
|
canvas: Canvas,
|
|
context: Context,
|
|
epoch: u64,
|
|
surface: Surface,
|
|
size: Size,
|
|
visit: Id,
|
|
) -> Message {
|
|
match canvas {
|
|
Canvas::Architecture => Message::DiagramRevealViewport(context, epoch, size),
|
|
Canvas::Process => Message::TopologyCanvas(behavior::topology::CanvasEvent::Viewport(
|
|
behavior::topology::Context {
|
|
document: context.document,
|
|
owner: surface.id(),
|
|
revision: context.revision,
|
|
epoch,
|
|
},
|
|
size,
|
|
)),
|
|
Canvas::Scenario => Message::ScenarioCanvas(behavior::scenario::CanvasEvent::Viewport(
|
|
behavior::scenario::Context {
|
|
document: context.document,
|
|
scenario: surface.id(),
|
|
representation: Representation::Derived {
|
|
owner: surface.id(),
|
|
},
|
|
visit,
|
|
revision: context.revision,
|
|
epoch,
|
|
},
|
|
size,
|
|
)),
|
|
Canvas::State => Message::StateDiagram(behavior::state_edit::Event::Viewport(
|
|
behavior::state_edit::Context {
|
|
document: context.document,
|
|
diagram: surface.id(),
|
|
revision: context.revision,
|
|
epoch,
|
|
},
|
|
size,
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn stamp(app: &App) -> (Context, u64, Id) {
|
|
let (context, _, epoch) = app
|
|
.pending_diagram_reveal
|
|
.as_ref()
|
|
.expect("reveal awaits measured destination");
|
|
(
|
|
*context,
|
|
*epoch,
|
|
app.pending_scenario_reveal_visit.unwrap_or(Id::nil()),
|
|
)
|
|
}
|
|
|
|
fn assert_centered(app: &App, canvas: Canvas, target: &Usage) {
|
|
// All independently authored fixture targets fit ACTUAL at the readable cap.
|
|
// This oracle does not call the production camera/reveal implementation.
|
|
let zoom = 1.25;
|
|
let pan = Vector::new(
|
|
250. - target.bounds.center_x() * zoom,
|
|
150. - target.bounds.center_y() * zoom,
|
|
);
|
|
match canvas {
|
|
Canvas::Architecture => {
|
|
assert_eq!(app.canvas_size, Some(ACTUAL));
|
|
assert_eq!(app.zoom, zoom);
|
|
assert_eq!(app.pan, pan);
|
|
assert!(!app.fit_to_view);
|
|
}
|
|
Canvas::State => {
|
|
assert_eq!(app.behavior.diagram.viewport, Some(ACTUAL));
|
|
assert_eq!(app.behavior.diagram.zoom, zoom);
|
|
assert_eq!(app.behavior.diagram.pan, pan);
|
|
assert!(!app.behavior.diagram.fit);
|
|
}
|
|
Canvas::Process => {
|
|
let mut expected = behavior::topology::State::default();
|
|
expected.enter(app.canvas_document, target.surface.id());
|
|
expected.viewport(ACTUAL);
|
|
expected.zoom(Some(zoom));
|
|
expected.camera(behavior::canvas_workspace::Event::Pan(pan));
|
|
assert!(app.behavior.topology.camera_boundary() == expected.camera_boundary());
|
|
}
|
|
Canvas::Scenario => {
|
|
let mut expected = behavior::scenario::State::default();
|
|
expected.enter(app.canvas_document, target.surface.id());
|
|
expected.choose_tool(target.surface.id(), behavior::scenario::CanvasTool::Select);
|
|
expected.viewport(ACTUAL);
|
|
expected.zoom(Some(zoom));
|
|
expected.camera(
|
|
app.project(),
|
|
target.surface.id(),
|
|
behavior::canvas_workspace::Event::Pan(pan),
|
|
);
|
|
assert!(app.behavior.scenario.camera_boundary() == expected.camera_boundary());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn all_reveal_surfaces_recenter_once_after_the_real_viewport_and_preserve_authored_state() {
|
|
for canvas in CANVASES {
|
|
let (mut app, target) = fixture(canvas);
|
|
let accepted = app.document.snapshot();
|
|
let drafts = app.source_workspace.bundle();
|
|
let revision = app.document.revision();
|
|
let history = (
|
|
app.document.can_undo(),
|
|
app.document.can_redo(),
|
|
app.document.dirty(),
|
|
);
|
|
assert!(app.reveal_diagram_usage(&target));
|
|
let (context, epoch, visit) = stamp(&app);
|
|
send(
|
|
&mut app,
|
|
measurement(canvas, context, epoch, target.surface, ACTUAL, visit),
|
|
);
|
|
assert!(app.pending_diagram_reveal.is_none());
|
|
assert_centered(&app, canvas, &target);
|
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
assert_eq!(app.source_workspace.bundle(), drafts);
|
|
assert_eq!(app.document.revision(), revision);
|
|
assert_eq!(
|
|
(
|
|
app.document.can_undo(),
|
|
app.document.can_redo(),
|
|
app.document.dirty()
|
|
),
|
|
history
|
|
);
|
|
// The exact delivered callback is a replay after the one-shot is consumed.
|
|
let camera = app.camera_boundary();
|
|
send(
|
|
&mut app,
|
|
measurement(canvas, context, epoch, target.surface, ACTUAL, visit),
|
|
);
|
|
assert!(app.pending_diagram_reveal.is_none());
|
|
assert!(app.camera_boundary() == camera);
|
|
assert_centered(&app, canvas, &target);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_or_wrong_stamp_measurements_cannot_poison_a_pending_reveal() {
|
|
for canvas in CANVASES {
|
|
for invalid in 0..6 {
|
|
let (mut app, target) = fixture(canvas);
|
|
assert!(app.reveal_diagram_usage(&target));
|
|
let (context, epoch, visit) = stamp(&app);
|
|
let camera = app.camera_boundary();
|
|
let mut stale = context;
|
|
let mut wrong_epoch = epoch;
|
|
let mut size = Size::new(111., 122.);
|
|
match invalid {
|
|
0 => stale.document = Id::new_v4(),
|
|
1 => stale.revision = stale.revision.wrapping_add(1),
|
|
2 => wrong_epoch = wrong_epoch.wrapping_add(1),
|
|
3 => size.width = f32::NAN,
|
|
4 => size.height = f32::INFINITY,
|
|
_ => size.width = 0.,
|
|
}
|
|
send(
|
|
&mut app,
|
|
measurement(canvas, stale, wrong_epoch, target.surface, size, visit),
|
|
);
|
|
assert!(
|
|
app.pending_diagram_reveal.is_some(),
|
|
"{canvas:?} invalid case {invalid}"
|
|
);
|
|
assert_eq!(stamp(&app), (context, epoch, visit));
|
|
assert_eq!(app.canvas_epoch, epoch);
|
|
assert!(
|
|
app.camera_boundary() == camera,
|
|
"{canvas:?} invalid case {invalid}"
|
|
);
|
|
send(
|
|
&mut app,
|
|
measurement(canvas, context, epoch, target.surface, ACTUAL, visit),
|
|
);
|
|
assert!(app.pending_diagram_reveal.is_none());
|
|
assert_centered(&app, canvas, &target);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn actual_window_resize_before_first_measurement_retains_focus_for_the_new_context() {
|
|
for canvas in CANVASES {
|
|
let (mut app, target) = fixture(canvas);
|
|
let accepted = app.document.snapshot();
|
|
assert!(app.reveal_diagram_usage(&target));
|
|
let (context, old_epoch, visit) = stamp(&app);
|
|
send(&mut app, Message::WindowResized(Size::new(1100., 700.)));
|
|
let (current, epoch, current_visit) = stamp(&app);
|
|
assert_eq!(current_visit, visit);
|
|
assert_eq!(current, context);
|
|
assert_eq!(epoch, app.canvas_epoch);
|
|
if matches!(canvas, Canvas::Process | Canvas::Scenario) {
|
|
assert_ne!(epoch, old_epoch);
|
|
let camera = app.camera_boundary();
|
|
send(
|
|
&mut app,
|
|
measurement(canvas, context, old_epoch, target.surface, ACTUAL, visit),
|
|
);
|
|
assert_eq!(stamp(&app), (current, epoch, visit));
|
|
assert!(app.camera_boundary() == camera);
|
|
}
|
|
send(
|
|
&mut app,
|
|
measurement(canvas, current, epoch, target.surface, ACTUAL, visit),
|
|
);
|
|
assert!(app.pending_diagram_reveal.is_none());
|
|
assert_centered(&app, canvas, &target);
|
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn user_camera_edit_and_navigation_cancel_deferred_focus_without_late_snapping() {
|
|
for canvas in CANVASES {
|
|
for action in 0..3 {
|
|
let (mut app, target) = fixture(canvas);
|
|
let accepted = app.document.snapshot();
|
|
assert!(app.reveal_diagram_usage(&target));
|
|
let (context, epoch, visit) = stamp(&app);
|
|
let event = match action {
|
|
0 => match canvas {
|
|
Canvas::Architecture => Message::Pan(Vector::new(31., -17.)),
|
|
Canvas::State => Message::StateDiagram(behavior::state_edit::Event::Pan(
|
|
app.state_context().unwrap(),
|
|
Vector::new(31., -17.),
|
|
)),
|
|
Canvas::Process => {
|
|
Message::TopologyCanvas(behavior::topology::CanvasEvent::Camera(
|
|
app.topology_context().unwrap(),
|
|
app.camera_epoch,
|
|
behavior::canvas_workspace::Event::Pan(Vector::new(31., -17.)),
|
|
))
|
|
}
|
|
Canvas::Scenario => {
|
|
Message::ScenarioCanvas(behavior::scenario::CanvasEvent::Camera(
|
|
app.scenario_context().unwrap(),
|
|
app.camera_epoch,
|
|
behavior::canvas_workspace::Event::Pan(Vector::new(31., -17.)),
|
|
))
|
|
}
|
|
},
|
|
1 => Message::Field("/name".into(), "Pending title edit".into()),
|
|
_ => Message::Page(Page::Catalog),
|
|
};
|
|
send(&mut app, event);
|
|
assert!(
|
|
app.pending_diagram_reveal.is_none(),
|
|
"{canvas:?} action {action}"
|
|
);
|
|
let camera = app.camera_boundary();
|
|
let page = app.page;
|
|
// A stale layout callback at the already retained size cannot
|
|
// restore the old navigation camera after deliberate user input.
|
|
send(
|
|
&mut app,
|
|
measurement(canvas, context, epoch, target.surface, WIDE, visit),
|
|
);
|
|
assert!(app.pending_diagram_reveal.is_none());
|
|
assert!(
|
|
app.camera_boundary() == camera,
|
|
"{canvas:?} action {action}"
|
|
);
|
|
assert_eq!(app.page, page);
|
|
assert!(Arc::ptr_eq(&accepted, &app.document.snapshot()));
|
|
if action == 1 {
|
|
assert!(app.draft_dirty());
|
|
}
|
|
}
|
|
}
|
|
}
|