Add persisted state diagram authoring and context-bound gestures

This commit is contained in:
2026-09-06 03:02:38 +03:00
parent af31c8a11d
commit e0b22108a8
14 changed files with 4316 additions and 753 deletions
+34 -54
View File
@@ -15,7 +15,10 @@ use iced::{
use oa_core::*;
use std::collections::BTreeMap;
pub(crate) mod scenario;
mod state_scene;
pub(crate) mod state_canvas;
pub(crate) mod state_edit;
pub(crate) mod state_menu;
pub(crate) mod state_view;
pub(crate) mod states;
pub(crate) mod topology;
#[derive(Debug, Clone)]
@@ -27,7 +30,8 @@ pub enum Event {
}
impl Event {
pub fn starts_form(&self) -> bool {
matches!(self,
matches!(
self,
Self::Topology(topology::Event::Start(..))
| Self::Scenario(scenario::Event::Start(..))
| Self::States(states::Event::Start(..) | states::Event::Edit(..))
@@ -45,6 +49,8 @@ impl Event {
}
#[derive(Default)]
pub struct State {
pub active_owner: Option<Id>,
pub diagram: state_view::State,
topology: topology::State,
scenario: scenario::State,
states: states::State,
@@ -130,7 +136,7 @@ pub fn view(app: &App) -> Element<'_, Message> {
label: format!("{} · {}", e.kind(), e.name),
})
.collect::<Vec<_>>();
let owner = app.behavior.pending_owner().or_else(|| selected_behavior(p, app.selected));
let owner = selected_owner(app);
let selected = owner
.and_then(|id| choices.iter().find(|c| c.id == id))
.or(choices.first())
@@ -140,8 +146,11 @@ pub fn view(app: &App) -> Element<'_, Message> {
return out.push(label("Start with a Process, Scenario or State machine using Create element. Add occurrences or regions inside it to describe behavior.")).padding(24).into();
};
let e = &p.elements[&selected.id];
if e.kind() == Kind::StateMachine {
out = column![text("States & modes").size(20)].spacing(8);
}
out = out.push(
pick_list(choices, Some(selected), |c| Message::Select(c.id))
pick_list(choices, Some(selected), Message::BehaviorOwner)
.width(Fill)
.text_size(13)
.padding(10),
@@ -152,7 +161,20 @@ pub fn view(app: &App) -> Element<'_, Message> {
if e.kind() == Kind::Scenario {
return scenario::view(app, e.id, out);
}
states::view(app, e.id, out)
state_view::view(app, e.id, out)
}
pub fn selected_owner(app: &App) -> Option<Id> {
let valid = |id: &Id| {
app.project().elements.get(id).is_some_and(|e| {
e.layer() == app.layer && (e.kind().is_sequence() || e.kind() == Kind::StateMachine)
})
};
app.behavior
.pending_owner()
.or(app.behavior.active_owner.filter(valid))
.or_else(|| selected_behavior(app.project(), app.selected).filter(valid))
.or_else(|| app.project().elements.keys().find(|id| valid(id)).copied())
}
/// Mirror the core's legacy-versus-explicit process interpretation. Existing
/// exchange-only lists still describe ordered paths until explicitly migrated.
@@ -179,7 +201,7 @@ fn uses_explicit_topology(p: &Project, owner: Id) -> bool {
)
})
}
fn selected_behavior(p: &Project, selected: Id) -> Option<Id> {
pub fn selected_behavior(p: &Project, selected: Id) -> Option<Id> {
let mut current = Some(
p.relations
.get(&selected)
@@ -570,12 +592,9 @@ impl canvas::Program<Message> for Scene {
) && let Some(p) = cursor.position_in(bounds)
{
let p = Point::new(p.x / self.scale, p.y / self.scale);
if let Some(b) = self
.boxes
.iter()
.rev()
.find(|b| b.rect.contains(p) && (!self.is_container(b) || p.y < b.rect.y + 38.))
{
if let Some(b) = self.boxes.iter().rev().find(|b| {
b.rect.contains(p) && (b.marker != Some(Kind::Occurrence) || p.y < b.rect.y + 38.)
}) {
return Some(Action::publish(Message::Select(b.id)).and_capture());
}
if let Some(l) = self.lines.iter().find(|l| {
@@ -620,10 +639,7 @@ impl canvas::Program<Message> for Scene {
&Path::rectangle(b.rect.position(), b.rect.size()),
Stroke::default().with_color(color),
);
} else if !b
.marker
.is_some_and(|k| k.is_state() && !k.is_stable_state())
{
} else {
let path = Path::rounded_rectangle(b.rect.position(), b.rect.size(), 8.into());
f.fill(&path, Color::from_rgb8(243, 248, 248));
f.stroke(
@@ -686,45 +702,9 @@ impl canvas::Program<Message> for Scene {
}
}
for b in &self.boxes {
let pseudo = b
.marker
.is_some_and(|k| k.is_state() && !k.is_stable_state());
if pseudo {
state_scene::draw_symbol(
&mut f,
b,
if b.id == self.selected {
style::TEAL
} else {
style::INK
},
);
}
let prefix = match b.marker {
Some(Kind::Initial) => "INITIAL · ",
Some(Kind::Final) => "FINAL · ",
Some(Kind::Mode) => "MODE · ",
Some(Kind::State) => "STATE · ",
Some(Kind::Choice) => "CHOICE · ",
Some(Kind::Junction) => "JUNCTION · ",
Some(Kind::Fork) => "FORK · ",
Some(Kind::Join) => "JOIN · ",
Some(Kind::ShallowHistory) => "H · ",
Some(Kind::DeepHistory) => "H* · ",
Some(Kind::EntryPoint) => "ENTRY · ",
Some(Kind::ExitPoint) => "EXIT · ",
Some(Kind::Terminate) => "TERMINATE · ",
_ => "",
};
f.fill_text(Text {
content: format!(
"{prefix}{}",
short(&b.title, ((b.rect.width - 25.) / 7.) as usize)
),
position: Point::new(
b.rect.x + if pseudo { 38. } else { 12. },
b.rect.y + if pseudo { 18. } else { 12. },
),
content: short(&b.title, ((b.rect.width - 25.) / 7.) as usize),
position: Point::new(b.rect.x + 12., b.rect.y + 12.),
size: 13.into(),
color: if b.id == self.selected {
style::TEAL