292 lines
12 KiB
Rust
292 lines
12 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.behavior.scenario.forget_view();
|
|
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::Selection(event) => self.multiselect_event(event),
|
|
Event::Remove(intent) => self.remove_from_diagram(intent),
|
|
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,
|
|
selection_target,
|
|
anchor,
|
|
bend,
|
|
route_segment,
|
|
} => {
|
|
let mut request = crate::model_menu::Request::object(object, anchor);
|
|
request.canvas = Some(context);
|
|
request.placement = placement;
|
|
request.selection_target = selection_target.map(Box::new);
|
|
request.bend = bend;
|
|
request.route_segment = route_segment;
|
|
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 let Tool::AdjustAttachment { relation, end } = tool {
|
|
if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty() {
|
|
self.status =
|
|
"Apply or cancel pending edits before adjusting an attachment.".into();
|
|
return Task::none();
|
|
}
|
|
let Some(context) = self.state_context() else {
|
|
return Task::none();
|
|
};
|
|
if state_edit::attachment_node(self.project(), context.diagram, relation, end)
|
|
.is_none()
|
|
{
|
|
return Task::none();
|
|
}
|
|
self.selected = relation;
|
|
self.selected_node = None;
|
|
self.refresh_draft();
|
|
}
|
|
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();
|
|
};
|
|
if let Event::SetBends { relation, .. } = &mutation
|
|
&& *relation != self.selected
|
|
{
|
|
return Task::none();
|
|
}
|
|
if let Event::SetAttachment {
|
|
relation,
|
|
end,
|
|
attachment,
|
|
..
|
|
} = &mutation
|
|
{
|
|
let existing = self.project().diagrams[&context.diagram]
|
|
.routes
|
|
.iter()
|
|
.find(|route| route.edge == *relation)
|
|
.and_then(|route| route.attachment(*end));
|
|
if existing == *attachment {
|
|
return Task::none();
|
|
}
|
|
}
|
|
match state_edit::plan(self.project(), context, &mutation) {
|
|
Ok(plan) => {
|
|
if let Command::SetEdgeRoute { diagram, route } = &plan.command
|
|
&& crate::route_edit::stored_route(
|
|
&self.project().diagrams[diagram],
|
|
route.edge,
|
|
) == *route
|
|
{
|
|
return Task::none();
|
|
}
|
|
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()
|
|
}
|
|
}
|