Compare commits

...
Author SHA1 Message Date
sorokin.oleg.edu 316dc559d7 Reset state camera and tools only when navigation changes view
Rust checks / check (windows-latest) (push) Canceled after 0s
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
2026-09-06 03:19:18 +03:00
3 changed files with 262 additions and 8 deletions
+11 -6
View File
@@ -642,7 +642,7 @@ impl App {
if !self.project().contains(id) {
return;
}
let previous_owner = crate::behavior::selected_owner(self);
let previous_state_view = self.state_view_identity();
self.selected = id;
self.selected_node = None;
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)
{
self.behavior.active_owner = Some(owner);
if previous_owner != Some(owner) {
self.behavior.diagram = Default::default();
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
}
}
if self.reset_state_view_after_navigation(previous_state_view)
&& self.page == Page::Behavior
{
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
}
self.refresh_draft();
}
@@ -1200,13 +1201,14 @@ impl App {
Message::Page(page) => {
if page != Page::Text || !self.draft_dirty() || self.apply() {
if page == Page::Behavior && self.page != Page::Behavior {
let previous_state_view = self.state_view_identity();
if let Some(owner) =
crate::behavior::selected_behavior(self.project(), self.selected)
{
self.behavior.active_owner = Some(owner);
}
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.selected_node = if page == Page::Behavior {
@@ -1360,6 +1362,7 @@ impl App {
}
Message::Layer(layer) => {
if self.apply() {
let previous_state_view = self.state_view_identity();
self.layer = layer;
self.create_kind = if layer == Layer::Oa {
Kind::Activity
@@ -1375,6 +1378,8 @@ impl App {
.map(|d| d.id);
self.selected = self.project().id;
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.fit();
}
+25 -2
View File
@@ -6,12 +6,35 @@ use crate::behavior::{
};
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> {
if self.page != Page::Behavior {
return None;
}
let owner = behavior::selected_owner(self)?;
let diagram = behavior::state_view::diagram(self, owner)?;
let diagram = self.state_view_identity().1?;
Some(Context {
document: self.canvas_document,
diagram,
@@ -89,6 +89,232 @@ fn assert_source_model_agree(app: &App) {
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]
fn create_view_places_only_its_machine_and_restores_exact_source_in_one_undo() {
let mut f = fixture();