Files
ARCADIA-Studio/crates/oa-desktop/src/app/state_diagram.rs
T

236 lines
9.7 KiB
Rust

//! 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<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 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<Message> {
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()
}
}