Files
ARCADIA-Studio/crates/oa-desktop/src/behavior/state_view.rs
T

289 lines
9.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<Id>,
pub placement: Option<Id>,
pub tool: Tool,
pub viewport: Option<Size>,
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<Id> {
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 {
match e {
Event::Selection(selection) => Message::DiagramSelection(selection),
other => Message::StateDiagram(other),
}
}
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::<Vec<_>>();
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::<Vec<_>>();
// Keep the optional picker in its own stable slot. Inserting it as a
// sibling before the canvas would replace the positional Iced focus tree.
// Reserve the picker height even when all elements are placed. Group Hide
// and Undo must not resize the canvas and silently refit its camera.
let mut missing_controls = Column::new().height(34);
if !missing.is_empty() {
let position = Position {
x: (48. - state.pan.x) / state.zoom,
y: (90. - state.pan.y) / state.zoom,
};
missing_controls = missing_controls.push(
pick_list(missing, None::<Choice>, 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,
selection: app.diagram_selection.as_ref(),
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(
column![
container(missing_controls).id("state-missing-picker"),
selection_controls(app),
crate::diagram::focus::wrap(
canvas.map(event),
context,
crate::app::diagram_removal::focus_enabled(app),
app.busy,
Message::DiagramTraverse,
)
]
.spacing(8)
.height(Fill),
);
let base: Element<'_, Message> = container(out.spacing(8))
.padding(14)
.width(Fill)
.height(Fill)
.into();
with_form(app, machine, base)
}
fn selection_controls(app: &App) -> Element<'_, Message> {
use crate::app::multiselect::{Event as SelectionEvent, placements};
let snapshot = app.multiselect_snapshot();
let count = snapshot.as_ref().map_or(0, |s| s.targets().len());
let mut controls = row![label(format!("{count} selected"))]
.spacing(7)
.align_y(iced::Center)
.height(32);
if let Some(snapshot) = snapshot.filter(|s| s.targets().len() > 1) {
if placements(&snapshot).is_some() {
controls = controls.push(action(
"Hide selected",
Message::DiagramSelection(SelectionEvent::Hide(snapshot.clone())),
));
}
controls = controls
.push(action(
"Clear",
Message::DiagramSelection(SelectionEvent::Clear(snapshot.context())),
))
.push(
label(format!(
"Properties: {} only",
crate::views::short(app.project().named(app.selected), 10)
))
.wrapping(iced::widget::text::Wrapping::None),
);
}
let hint = if app.behavior.diagram.tool == Tool::Select {
"Shift+click adds or removes from selection"
} else {
"Use Select / move to select multiple elements"
};
controls
.push(crate::views::diagram_help(
crate::guide::Topic::States,
hint,
))
.into()
}
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
}
}