//! Adapter from captured state-canvas events to the shared document transaction. use super::*; use crate::behavior::{ self, state_edit::{self, Context, Event, Tool}, }; impl App { /// A saved state view has one renderer and tool set: the behavior workspace. /// Run after navigation/adoption, never as a model or property edit. Keeping /// selection and its Draft intact also preserves unrelated raw properties. pub(super) fn route_state_diagram_workspace(&mut self) { if self.page != Page::Diagram { return; } let Some((diagram, machine, layer)) = self.active_diagram.and_then(|id| { let view = self.project().diagrams.get(&id)?; Some((id, view.projection.state_machine()?, view.layer)) }) else { return; }; // Existing navigation guards own pending behavior forms. Never replace // their owner/camera if an exceptional caller leaves one on another page. if self.behavior.has_pending() { return; } self.layer = layer; self.page = Page::Behavior; self.behavior.active_owner = Some(machine); self.behavior.diagram = behavior::state_view::State { diagram: Some(diagram), placement: self.project().diagrams[&diagram] .nodes .values() .find(|node| node.id == self.selected || node.element == self.selected) .map(|node| node.id), ..Default::default() }; self.selected_node = None; self.canvas_epoch = self.canvas_epoch.wrapping_add(1); self.fit_state_diagram(); } /// 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 diagram = self.state_view_identity().1?; Some(Context { document: self.canvas_document, diagram, revision: self.document.revision(), epoch: self.canvas_epoch, }) } pub(super) fn fit_state_diagram(&mut self) { let Some(context) = self.state_context() else { return; }; let diagram = &self.project().diagrams[&context.diagram]; let view = projection::project_diagram(self.project(), diagram); let bounds = routing::route_diagram(self.project(), diagram, &view).bounds; let viewport = self.behavior.diagram.viewport.unwrap_or(iced::Size::new( (self.window_size.width - 610.).max(280.), (self.window_size.height - 320.).max(240.), )); let width = (viewport.width - 48.).max(1.); let height = (viewport.height - 80.).max(1.); let zoom = (width / bounds.size.width.max(1.)) .min(height / bounds.size.height.max(1.)) .clamp(0.15, 1.25); self.behavior.diagram.zoom = zoom; self.behavior.diagram.pan = iced::Vector::new( 24. + (width - bounds.size.width * zoom) / 2. - bounds.position.x * zoom, 56. + (height - bounds.size.height * zoom) / 2. - bounds.position.y * zoom, ); } pub(super) fn edit_state_diagram(&mut self, event: Event) -> Task { if self.page != Page::Behavior { return Task::none(); } if let Some(captured) = event.context() { let Some(current) = self.state_context() else { return Task::none(); }; if state_edit::validate_context(self.project(), captured, current).is_err() { return Task::none(); } } match event { Event::Viewport(_, size) => { if size.width.is_finite() && size.height.is_finite() && size.width > 0. && size.height > 0. { let resized = self.behavior.diagram.viewport != Some(size); self.behavior.diagram.viewport = Some(size); if resized && self.behavior.diagram.fit { self.fit_state_diagram(); } } } Event::Fit(_) => { self.behavior.diagram.fit = true; self.fit_state_diagram(); } Event::Zoom(_, zoom) if zoom.is_finite() => { let state = &mut self.behavior.diagram; let new = zoom.clamp(0.15, 3.); let center = state.viewport.map_or(iced::Vector::new(300., 220.), |s| { iced::Vector::new(s.width / 2., s.height / 2.) }); state.pan = center - (center - state.pan) * (new / state.zoom); state.zoom = new; state.fit = false; } Event::Pan(_, delta) if delta.x.is_finite() && delta.y.is_finite() => { self.behavior.diagram.pan += delta; self.behavior.diagram.fit = false; } Event::Select(_, id) => { if !self.behavior.has_pending() { self.select(id); self.selected_node = None; } } Event::ContextMenu { context, object, placement, anchor, } => { let mut request = crate::model_menu::Request::object(object, anchor); request.canvas = Some(context); request.placement = placement; return self.handle(Message::Menu(crate::model_menu::Event::Open(request))); } Event::EditTransition(_, id) => { if self.behavior.has_pending() || !self.apply() { return Task::none(); } return self.handle(Message::Behavior(behavior::Event::States( behavior::states::Event::Edit(id), ))); } Event::Choose(tool) => { if !self.apply() { return Task::none(); } self.behavior.diagram.tool = tool; self.canvas_epoch = self.canvas_epoch.wrapping_add(1); } Event::CreateView(machine) => { if behavior::selected_owner(self) != Some(machine) || !self.apply() { return Task::none(); } match state_edit::create_view(self.project(), machine) { Ok(plan) => { if self.execute(plan.command) { self.behavior.active_owner = Some(machine); self.behavior.diagram.diagram = Some(plan.diagram); self.behavior.diagram.placement = plan.placement; self.behavior.diagram.tool = Tool::Select; self.behavior.diagram.fit = true; self.selected = plan.selected; self.selected_node = None; self.refresh_draft(); self.fit_state_diagram(); self.status = "State diagram created · existing model elements are placed in this view".into(); } } Err(error) => self.status = error.to_string(), } } Event::Zoom(..) | Event::Pan(..) => {} mutation => { if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty() { self.status = "Apply or cancel pending edits before changing the state diagram.".into(); return Task::none(); } let Some(context) = self.state_context() else { return Task::none(); }; match state_edit::plan(self.project(), context, &mutation) { Ok(plan) => { if self.execute(plan.command) { self.behavior.diagram.diagram = Some(plan.diagram); self.behavior.diagram.placement = plan.placement; self.selected = plan.selected; self.selected_node = None; self.refresh_draft(); if !matches!(mutation, Event::Move { .. }) { self.behavior.diagram.tool = Tool::Select; self.canvas_epoch = self.canvas_epoch.wrapping_add(1); } self.status = "State diagram updated · Undo restores the complete operation" .into(); } } Err(error) => self.status = error.to_string(), } } } Task::none() } }