Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
316dc559d7 |
@@ -642,7 +642,7 @@ impl App {
|
|||||||
if !self.project().contains(id) {
|
if !self.project().contains(id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let previous_owner = crate::behavior::selected_owner(self);
|
let previous_state_view = self.state_view_identity();
|
||||||
self.selected = id;
|
self.selected = id;
|
||||||
self.selected_node = None;
|
self.selected_node = None;
|
||||||
if let Some(layer) = self.project().layer_of(id)
|
if let Some(layer) = self.project().layer_of(id)
|
||||||
@@ -656,10 +656,11 @@ impl App {
|
|||||||
&& let Some(owner) = crate::behavior::selected_behavior(self.project(), id)
|
&& let Some(owner) = crate::behavior::selected_behavior(self.project(), id)
|
||||||
{
|
{
|
||||||
self.behavior.active_owner = Some(owner);
|
self.behavior.active_owner = Some(owner);
|
||||||
if previous_owner != Some(owner) {
|
}
|
||||||
self.behavior.diagram = Default::default();
|
if self.reset_state_view_after_navigation(previous_state_view)
|
||||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
&& self.page == Page::Behavior
|
||||||
}
|
{
|
||||||
|
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||||
}
|
}
|
||||||
self.refresh_draft();
|
self.refresh_draft();
|
||||||
}
|
}
|
||||||
@@ -1200,13 +1201,14 @@ impl App {
|
|||||||
Message::Page(page) => {
|
Message::Page(page) => {
|
||||||
if page != Page::Text || !self.draft_dirty() || self.apply() {
|
if page != Page::Text || !self.draft_dirty() || self.apply() {
|
||||||
if page == Page::Behavior && self.page != Page::Behavior {
|
if page == Page::Behavior && self.page != Page::Behavior {
|
||||||
|
let previous_state_view = self.state_view_identity();
|
||||||
if let Some(owner) =
|
if let Some(owner) =
|
||||||
crate::behavior::selected_behavior(self.project(), self.selected)
|
crate::behavior::selected_behavior(self.project(), self.selected)
|
||||||
{
|
{
|
||||||
self.behavior.active_owner = Some(owner);
|
self.behavior.active_owner = Some(owner);
|
||||||
}
|
}
|
||||||
self.behavior.active_owner = crate::behavior::selected_owner(self);
|
self.behavior.active_owner = crate::behavior::selected_owner(self);
|
||||||
self.behavior.diagram.fit = true;
|
self.reset_state_view_after_navigation(previous_state_view);
|
||||||
}
|
}
|
||||||
self.page = page;
|
self.page = page;
|
||||||
self.selected_node = if page == Page::Behavior {
|
self.selected_node = if page == Page::Behavior {
|
||||||
@@ -1360,6 +1362,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
Message::Layer(layer) => {
|
Message::Layer(layer) => {
|
||||||
if self.apply() {
|
if self.apply() {
|
||||||
|
let previous_state_view = self.state_view_identity();
|
||||||
self.layer = layer;
|
self.layer = layer;
|
||||||
self.create_kind = if layer == Layer::Oa {
|
self.create_kind = if layer == Layer::Oa {
|
||||||
Kind::Activity
|
Kind::Activity
|
||||||
@@ -1375,6 +1378,8 @@ impl App {
|
|||||||
.map(|d| d.id);
|
.map(|d| d.id);
|
||||||
self.selected = self.project().id;
|
self.selected = self.project().id;
|
||||||
self.selected_node = None;
|
self.selected_node = None;
|
||||||
|
self.behavior.active_owner = crate::behavior::selected_owner(self);
|
||||||
|
self.reset_state_view_after_navigation(previous_state_view);
|
||||||
self.refresh_draft();
|
self.refresh_draft();
|
||||||
self.fit();
|
self.fit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,35 @@ use crate::behavior::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
/// The behavior view resolved independently of the current page. Capture
|
||||||
|
/// this before selection/layer changes, while it still identifies the view
|
||||||
|
/// whose camera and tool are retained during navigation.
|
||||||
|
pub(super) fn state_view_identity(&self) -> (Option<Id>, Option<Id>) {
|
||||||
|
let owner = behavior::selected_owner(self);
|
||||||
|
let diagram = owner.and_then(|owner| behavior::state_view::diagram(self, owner));
|
||||||
|
(owner, diagram)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn reset_state_view_after_navigation(
|
||||||
|
&mut self,
|
||||||
|
previous: (Option<Id>, Option<Id>),
|
||||||
|
) -> bool {
|
||||||
|
let current = self.state_view_identity();
|
||||||
|
if current == previous {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.behavior.diagram = behavior::state_view::State {
|
||||||
|
diagram: current.1,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
pub fn state_context(&self) -> Option<Context> {
|
pub fn state_context(&self) -> Option<Context> {
|
||||||
if self.page != Page::Behavior {
|
if self.page != Page::Behavior {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let owner = behavior::selected_owner(self)?;
|
let diagram = self.state_view_identity().1?;
|
||||||
let diagram = behavior::state_view::diagram(self, owner)?;
|
|
||||||
Some(Context {
|
Some(Context {
|
||||||
document: self.canvas_document,
|
document: self.canvas_document,
|
||||||
diagram,
|
diagram,
|
||||||
|
|||||||
@@ -89,6 +89,232 @@ fn assert_source_model_agree(app: &App) {
|
|||||||
assert!(!app.source_dirty());
|
assert!(!app.source_dirty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct NavigationFixture {
|
||||||
|
app: App,
|
||||||
|
machine_a: Id,
|
||||||
|
diagram_a: Id,
|
||||||
|
state_a: Id,
|
||||||
|
transition_a: Id,
|
||||||
|
root_event: Id,
|
||||||
|
machine_b: Id,
|
||||||
|
diagram_b: Id,
|
||||||
|
state_b: Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn navigation_fixture(layer_b: Layer) -> NavigationFixture {
|
||||||
|
let mut project = Project::empty();
|
||||||
|
let mut machine = |layer, name| {
|
||||||
|
let machine = project
|
||||||
|
.add_element(Kind::StateMachine, layer, name, None)
|
||||||
|
.unwrap();
|
||||||
|
let region = project
|
||||||
|
.add_element(Kind::Region, layer, "Main", Some(machine))
|
||||||
|
.unwrap();
|
||||||
|
let state = project
|
||||||
|
.add_element(Kind::State, layer, "Ready", Some(region))
|
||||||
|
.unwrap();
|
||||||
|
(machine, state)
|
||||||
|
};
|
||||||
|
let (machine_a, state_a) = machine(Layer::Sa, "Machine A");
|
||||||
|
let (machine_b, state_b) = machine(layer_b, "Machine B");
|
||||||
|
let root_event = project
|
||||||
|
.add_element(Kind::Event, Layer::Sa, "External event", None)
|
||||||
|
.unwrap();
|
||||||
|
let transition_a = project.relate(RelationKind::StateTransition, "Repeat", state_a, state_a);
|
||||||
|
let plan_a = state_edit::create_view(&project, machine_a).unwrap();
|
||||||
|
project = prepare_command(&project, &plan_a.command).unwrap().project;
|
||||||
|
let plan_b = state_edit::create_view(&project, machine_b).unwrap();
|
||||||
|
project = prepare_command(&project, &plan_b.command).unwrap().project;
|
||||||
|
let mut app = App::with_project(project);
|
||||||
|
send(&mut app, Message::Select(state_a));
|
||||||
|
send(&mut app, Message::Page(Page::Behavior));
|
||||||
|
send(
|
||||||
|
&mut app,
|
||||||
|
Message::BehaviorDiagram(Choice {
|
||||||
|
id: plan_a.diagram,
|
||||||
|
label: "A view".into(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let context = app.state_context().unwrap();
|
||||||
|
event(
|
||||||
|
&mut app,
|
||||||
|
Event::Viewport(context, iced::Size::new(820., 540.)),
|
||||||
|
);
|
||||||
|
event(&mut app, Event::Zoom(context, 0.65));
|
||||||
|
event(
|
||||||
|
&mut app,
|
||||||
|
Event::Pan(context, iced::Vector::new(-1_400., -800.)),
|
||||||
|
);
|
||||||
|
event(
|
||||||
|
&mut app,
|
||||||
|
Event::Choose(Tool::Reconnect {
|
||||||
|
relation: transition_a,
|
||||||
|
endpoint: Endpoint::Target,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert_eq!(app.state_context().unwrap().diagram, plan_a.diagram);
|
||||||
|
assert!(!app.behavior.diagram.fit);
|
||||||
|
NavigationFixture {
|
||||||
|
app,
|
||||||
|
machine_a,
|
||||||
|
diagram_a: plan_a.diagram,
|
||||||
|
state_a,
|
||||||
|
transition_a,
|
||||||
|
root_event,
|
||||||
|
machine_b,
|
||||||
|
diagram_b: plan_b.diagram,
|
||||||
|
state_b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_switched_state_view(f: &mut NavigationFixture) {
|
||||||
|
assert_eq!(behavior::selected_owner(&f.app), Some(f.machine_b));
|
||||||
|
assert_eq!(f.app.state_context().unwrap().diagram, f.diagram_b);
|
||||||
|
let defaults = behavior::state_view::State::default();
|
||||||
|
let state = &f.app.behavior.diagram;
|
||||||
|
assert_eq!(state.diagram, Some(f.diagram_b));
|
||||||
|
assert_eq!(
|
||||||
|
state.tool,
|
||||||
|
Tool::Select,
|
||||||
|
"Reconnect from A must not be armed in B"
|
||||||
|
);
|
||||||
|
assert_eq!(state.pan, defaults.pan);
|
||||||
|
assert_eq!(state.zoom, defaults.zoom);
|
||||||
|
assert!(state.fit);
|
||||||
|
assert!(
|
||||||
|
state.viewport.is_none(),
|
||||||
|
"B needs its own first viewport measurement"
|
||||||
|
);
|
||||||
|
assert!(state.placement.is_none());
|
||||||
|
let context = f.app.state_context().unwrap();
|
||||||
|
event(
|
||||||
|
&mut f.app,
|
||||||
|
Event::Viewport(context, iced::Size::new(820., 540.)),
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
f.app.behavior.diagram.pan != defaults.pan || f.app.behavior.diagram.zoom != defaults.zoom,
|
||||||
|
"The unchanged widget size still fits a newly selected view"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn catalog_selects_another_machine(layer_b: Layer) {
|
||||||
|
let mut f = navigation_fixture(layer_b);
|
||||||
|
let before = f.app.document.snapshot();
|
||||||
|
send(&mut f.app, Message::Page(Page::Catalog));
|
||||||
|
send(&mut f.app, Message::Select(f.state_b));
|
||||||
|
assert_eq!(f.app.layer, layer_b);
|
||||||
|
send(&mut f.app, Message::Page(Page::Behavior));
|
||||||
|
assert_switched_state_view(&mut f);
|
||||||
|
assert!(Arc::ptr_eq(&before, &f.app.document.snapshot()));
|
||||||
|
assert_source_model_agree(&f.app);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_navigation_catalog_selection_of_another_machine_resets_camera_and_tool() {
|
||||||
|
catalog_selects_another_machine(Layer::Sa);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_navigation_catalog_selection_across_layers_resets_camera_and_tool() {
|
||||||
|
catalog_selects_another_machine(Layer::Oa);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_navigation_layer_change_resets_the_new_machine_at_the_same_viewport_size() {
|
||||||
|
let mut f = navigation_fixture(Layer::Oa);
|
||||||
|
let before = f.app.document.snapshot();
|
||||||
|
send(&mut f.app, Message::Layer(Layer::Oa));
|
||||||
|
assert_switched_state_view(&mut f);
|
||||||
|
assert!(Arc::ptr_eq(&before, &f.app.document.snapshot()));
|
||||||
|
assert_source_model_agree(&f.app);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_navigation_return_to_the_same_view_preserves_camera_tool_and_fit_mode() {
|
||||||
|
for fit in [false, true] {
|
||||||
|
let mut f = navigation_fixture(Layer::Sa);
|
||||||
|
if fit {
|
||||||
|
let context = f.app.state_context().unwrap();
|
||||||
|
event(&mut f.app, Event::Fit(context));
|
||||||
|
}
|
||||||
|
let state = &f.app.behavior.diagram;
|
||||||
|
let retained = (
|
||||||
|
state.diagram,
|
||||||
|
state.pan,
|
||||||
|
state.zoom,
|
||||||
|
state.tool,
|
||||||
|
state.fit,
|
||||||
|
state.viewport,
|
||||||
|
);
|
||||||
|
let before = f.app.document.snapshot();
|
||||||
|
send(&mut f.app, Message::Page(Page::Catalog));
|
||||||
|
send(&mut f.app, Message::Select(f.state_a));
|
||||||
|
send(&mut f.app, Message::Page(Page::Behavior));
|
||||||
|
let context = f.app.state_context().unwrap();
|
||||||
|
event(
|
||||||
|
&mut f.app,
|
||||||
|
Event::Viewport(context, iced::Size::new(820., 540.)),
|
||||||
|
);
|
||||||
|
assert_eq!(behavior::selected_owner(&f.app), Some(f.machine_a));
|
||||||
|
assert_eq!(context.diagram, f.diagram_a);
|
||||||
|
let state = &f.app.behavior.diagram;
|
||||||
|
assert_eq!(
|
||||||
|
(
|
||||||
|
state.diagram,
|
||||||
|
state.pan,
|
||||||
|
state.zoom,
|
||||||
|
state.tool,
|
||||||
|
state.fit,
|
||||||
|
state.viewport
|
||||||
|
),
|
||||||
|
retained
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
state.tool,
|
||||||
|
Tool::Reconnect {
|
||||||
|
relation: f.transition_a,
|
||||||
|
endpoint: Endpoint::Target
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert!(Arc::ptr_eq(&before, &f.app.document.snapshot()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_navigation_root_event_and_same_layer_keep_the_current_camera_and_tool() {
|
||||||
|
let mut f = navigation_fixture(Layer::Sa);
|
||||||
|
let state = &f.app.behavior.diagram;
|
||||||
|
let retained = (
|
||||||
|
state.diagram,
|
||||||
|
state.pan,
|
||||||
|
state.zoom,
|
||||||
|
state.tool,
|
||||||
|
state.fit,
|
||||||
|
state.viewport,
|
||||||
|
);
|
||||||
|
let before = f.app.document.snapshot();
|
||||||
|
send(&mut f.app, Message::Select(f.root_event));
|
||||||
|
assert_eq!(f.app.selected, f.root_event);
|
||||||
|
send(&mut f.app, Message::Page(Page::Catalog));
|
||||||
|
send(&mut f.app, Message::Page(Page::Behavior));
|
||||||
|
send(&mut f.app, Message::Layer(Layer::Sa));
|
||||||
|
assert_eq!(behavior::selected_owner(&f.app), Some(f.machine_a));
|
||||||
|
assert_eq!(f.app.state_context().unwrap().diagram, f.diagram_a);
|
||||||
|
let state = &f.app.behavior.diagram;
|
||||||
|
assert_eq!(
|
||||||
|
(
|
||||||
|
state.diagram,
|
||||||
|
state.pan,
|
||||||
|
state.zoom,
|
||||||
|
state.tool,
|
||||||
|
state.fit,
|
||||||
|
state.viewport
|
||||||
|
),
|
||||||
|
retained
|
||||||
|
);
|
||||||
|
assert!(Arc::ptr_eq(&before, &f.app.document.snapshot()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create_view_places_only_its_machine_and_restores_exact_source_in_one_undo() {
|
fn create_view_places_only_its_machine_and_restores_exact_source_in_one_undo() {
|
||||||
let mut f = fixture();
|
let mut f = fixture();
|
||||||
|
|||||||
Reference in New Issue
Block a user