//! State workspace controls. Persistent semantics and layout live in the model. use super::state_edit::{Event, Tool}; use super::*; use iced::widget::{Column, column, opaque, stack}; pub struct State { pub diagram: Option, pub placement: Option, pub tool: Tool, pub viewport: Option, pub zoom: f32, pub pan: iced::Vector, pub fit: bool, } impl Default for State { fn default() -> Self { Self { diagram: None, placement: None, tool: Tool::Select, viewport: None, zoom: 0.8, pan: iced::Vector::new(24., 48.), fit: true, } } } pub fn diagram(app: &App, machine: Id) -> Option { let valid = |id: &Id| { app.project() .diagrams .get(id) .is_some_and(|d| d.projection.state_machine() == Some(machine)) }; app.behavior .diagram .diagram .filter(valid) .or_else(|| app.project().diagrams.keys().find(|id| valid(id)).copied()) } fn event(e: Event) -> Message { Message::StateDiagram(e) } pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Element<'a, Message> { let Some(id) = diagram(app, machine) else { let base = out.push(container(column![ text("Create a state diagram").size(20), label("Place the machine’s existing regions and states in an editable view. Creating another view leaves the existing views unchanged."), button("Create diagram").padding(12).on_press(event(Event::CreateView(machine))), row![ action("Add region…", Message::Behavior(super::Event::States(states::Event::Start(machine, states::Tool::Region, app.selected)))), action("Add state…", Message::Behavior(super::Event::States(states::Event::Start(machine, states::Tool::Vertex, app.selected)))), transition_details(machine, app.selected), ].spacing(5), ].spacing(14)).padding(24).style(style::card)).padding(18).into(); return with_form(app, machine, base); }; let context = app .state_context() .expect("a visible state diagram has a context"); let p = app.project(); let state = &app.behavior.diagram; let views = p .diagrams .values() .filter(|d| d.projection.state_machine() == Some(machine)) .map(|d| Choice { id: d.id, label: d.name.clone(), }) .collect::>(); let current = views.iter().find(|v| v.id == id).cloned(); out = out.push( row![ pick_list(views, current, Message::BehaviorDiagram) .padding(8) .text_size(12) .width(Fill), action("+ View", event(Event::CreateView(machine))), action("Fit", event(Event::Fit(context))), action("−", event(Event::Zoom(context, state.zoom / 1.2))), label(format!("{:.0}%", state.zoom * 100.)), action("+", event(Event::Zoom(context, state.zoom * 1.2))), ] .spacing(5) .align_y(iced::Center), ); let tool = |name: &'static str, value: Tool| { button(text(name).size(12)) .padding([8, 10]) .style(if state.tool == value { button::primary } else { style::nav }) .on_press(event(Event::Choose(value))) }; let kinds = [ Kind::State, Kind::Mode, Kind::Initial, Kind::Final, Kind::Choice, Kind::Junction, Kind::Fork, Kind::Join, Kind::ShallowHistory, Kind::DeepHistory, Kind::EntryPoint, Kind::ExitPoint, Kind::Terminate, ]; let selected_kind = if let Tool::Vertex(kind) = state.tool { Some(kind) } else { None }; out = out.push( row![ tool("Select / move", Tool::Select), pick_list(kinds.to_vec(), selected_kind, |k| event(Event::Choose( Tool::Vertex(k) ))) .placeholder("+ State / symbol") .padding(8) .text_size(12), tool("+ Region", Tool::Region), tool("Transition", Tool::Transition), ] .spacing(4), ); let missing = state_edit::unplaced(p, id) .into_iter() .map(|element| Choice { id: element, label: format!("{} · {}", p.named(element), p.elements[&element].kind()), }) .collect::>(); if !missing.is_empty() { let position = Position { x: (48. - state.pan.x) / state.zoom, y: (90. - state.pan.y) / state.zoom, }; out = out.push( pick_list(missing, None::, move |c| { event(Event::Place { context, element: c.id, position, }) }) .placeholder("Show an element not yet in this view…") .padding(8) .text_size(12) .width(Fill), ); } let canvas: Element<'_, Event> = canvas::Canvas::new(super::state_canvas::StateCanvas { project: p, diagram: id, context, selected: app.selected, navigation_focus: app .diagram_focus(crate::diagram_usage::Surface::Saved(id)) .map(|usage| usage.bounds), tool: state.tool, zoom: state.zoom, pan: state.pan, editable: !app.draft_dirty() && !app.source_dirty() && !app.behavior.has_pending(), }) .width(Fill) .height(Fill) .into(); out = out.push(canvas.map(event)); let base: Element<'_, Message> = container(out.spacing(8)) .padding(14) .width(Fill) .height(Fill) .into(); with_form(app, machine, base) } fn transition_details(machine: Id, selected: Id) -> Element<'static, Message> { action( "New transition with details…", Message::Behavior(super::Event::States(states::Event::Start( machine, states::Tool::Transition, selected, ))), ) .into() } fn with_form<'a>(app: &'a App, machine: Id, base: Element<'a, Message>) -> Element<'a, Message> { if let Some(form) = states::form(app, machine) { stack![ base, opaque( container( container( scrollable(form).height((app.window_size.height - 250.).clamp(280., 610.)) ) .padding(18) .width(600) .style(style::card) ) .center(Fill) .style(|_| container::Style { background: Some(Color::from_rgba(0.08, 0.12, 0.18, 0.38).into()), ..Default::default() }) ) ] .into() } else { base } }