From 9118218ac31a73263626f8d107d66e00c7bc4fed Mon Sep 17 00:00:00 2001 From: "sorokin.oleg.edu" Date: Sun, 6 Sep 2026 03:19:18 +0300 Subject: [PATCH] Reset state camera and tools only when navigation changes view --- crates/oa-desktop/src/app.rs | 17 +- crates/oa-desktop/src/app/state_diagram.rs | 27 ++- .../oa-desktop/src/app/state_diagram_tests.rs | 226 ++++++++++++++++++ 3 files changed, 262 insertions(+), 8 deletions(-) diff --git a/crates/oa-desktop/src/app.rs b/crates/oa-desktop/src/app.rs index 69ad549d..18487522 100644 --- a/crates/oa-desktop/src/app.rs +++ b/crates/oa-desktop/src/app.rs @@ -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(); } diff --git a/crates/oa-desktop/src/app/state_diagram.rs b/crates/oa-desktop/src/app/state_diagram.rs index ca34bf4e..63e9cfe3 100644 --- a/crates/oa-desktop/src/app/state_diagram.rs +++ b/crates/oa-desktop/src/app/state_diagram.rs @@ -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, Option) { + 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, Option), + ) -> 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 { 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, diff --git a/crates/oa-desktop/src/app/state_diagram_tests.rs b/crates/oa-desktop/src/app/state_diagram_tests.rs index 67b864f9..47a69842 100644 --- a/crates/oa-desktop/src/app/state_diagram_tests.rs +++ b/crates/oa-desktop/src/app/state_diagram_tests.rs @@ -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();