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

489 lines
18 KiB
Rust

//! A prerequisite belongs to its pending consumer, never the accepted catalog.
use iced::{
Element as UiElement, Fill,
widget::{button, column, container, row, scrollable, text, text_input},
};
use oa_core::*;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const NAME_INPUT: &str = "behavior-source-name";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Scope {
Lifeline {
owner: Id,
viewpoint: ScenarioViewpoint,
},
Work {
owner: Id,
},
Machine {
layer: Layer,
},
}
impl Scope {
fn owner(self) -> Option<Id> {
match self {
Self::Lifeline { owner, .. } | Self::Work { owner } => Some(owner),
Self::Machine { .. } => None,
}
}
fn layer(self, p: &Project) -> Result<Layer, String> {
match self {
Self::Machine { layer } => Ok(layer),
_ => p
.layer_of(self.owner().unwrap())
.ok_or("The consumer owner no longer exists.".into()),
}
}
fn kinds(self, layer: Layer) -> Vec<Kind> {
match self {
Self::Work { .. } => vec![if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
}],
Self::Lifeline {
viewpoint: ScenarioViewpoint::OperationalActivity,
..
} => vec![Kind::Activity],
Self::Lifeline {
viewpoint: ScenarioViewpoint::Functional,
..
} => vec![Kind::Function],
Self::Lifeline {
viewpoint: ScenarioViewpoint::ParticipantInteraction,
..
} => vec![Kind::Entity, Kind::Actor, Kind::Role],
Self::Lifeline {
viewpoint: ScenarioViewpoint::BoundaryInterface,
..
} => vec![Kind::System, Kind::SystemActor],
Self::Machine { .. } if layer == Layer::Oa => vec![Kind::Entity, Kind::Actor],
Self::Machine { .. } => vec![Kind::System, Kind::SystemActor],
_ => vec![],
}
}
}
#[derive(Debug, Clone)]
pub enum Event {
Open(Id),
Edit(Id, Edit),
}
#[derive(Debug, Clone)]
pub enum Edit {
Name(String),
Kind(Kind),
Parent(Option<Id>),
Search(String),
Page(usize),
Back,
Use,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Draft {
token: Id,
document: Id,
scope: Scope,
layer: Layer,
owner: Option<Element>,
id: Id,
kind: Option<Kind>,
name: String,
parent: Option<Element>,
search: String,
page: usize,
identities: Vec<Id>,
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct State {
editing: Option<Draft>,
staged: Option<Draft>,
#[serde(default)]
engaged: bool,
}
impl State {
pub fn check_scope(&self, expected: Scope, selected: Option<Id>) -> Result<(), String> {
if self
.editing
.as_ref()
.or_else(|| self.staged.as_ref().filter(|d| Some(d.id) == selected))
.is_some_and(|d| d.scope != expected)
{
return Err("The recovered source request belongs to a different consumer context. Go Back or cancel this draft.".into());
}
Ok(())
}
pub fn dismiss(&mut self, event: &Event) -> Result<bool, String> {
if let Event::Edit(token, Edit::Back) = event {
if self.editing.as_ref().is_none_or(|d| d.token != *token) {
return Err("This Back action belongs to a previous source form.".into());
}
self.editing = None;
self.engaged = true;
return Ok(true);
}
Ok(false)
}
pub fn engaged(&self) -> bool {
self.engaged || self.editing.is_some() || self.staged.is_some()
}
pub fn is_open(&self) -> bool {
self.editing.is_some()
}
pub fn back(&mut self) -> bool {
self.editing.take().is_some()
}
pub fn label(&self, selected: Option<Id>) -> Option<String> {
self.staged
.as_ref()
.filter(|d| Some(d.id) == selected)
.map(|d| {
format!(
"New · {} · {}",
d.kind.map_or("Choose kind".into(), |k| k.to_string()),
d.name
)
})
}
pub fn update(
&mut self,
p: &Project,
scope: Scope,
invocation: Id,
event: Event,
) -> Result<Option<Id>, String> {
if self.dismiss(&event)? {
return Ok(None);
}
match event {
Event::Open(token) => {
if token != invocation || self.is_open() {
return Err("This source request is stale. Use the current form.".into());
}
let layer = scope.layer(p)?;
let kinds = scope.kinds(layer);
if kinds.is_empty() {
return Err("Create new sources requires a strict scenario viewpoint; use an existing source here.".into());
}
self.engaged = true;
self.editing = Some(Draft {
token: Id::new_v4(),
document: p.id,
scope,
layer,
owner: scope.owner().and_then(|id| p.elements.get(&id).cloned()),
id: Id::new_v4(),
kind: (kinds.len() == 1).then_some(kinds[0]),
name: String::new(),
parent: None,
search: String::new(),
page: 0,
identities: (0..match scope {
Scope::Work { owner } => p.children(owner).count() + 4,
Scope::Lifeline { .. } => 1,
Scope::Machine { .. } => 0,
})
.map(|_| Id::new_v4())
.collect(),
});
Ok(None)
}
Event::Edit(token, edit) => {
let d = self
.editing
.as_mut()
.filter(|d| d.token == token && d.scope == scope && d.document == p.id)
.ok_or("This source response is stale; return to the current form.")?;
match edit {
Edit::Back => {
self.editing = None;
Ok(None)
}
Edit::Use => {
d.command(p)?;
let id = d.id;
self.staged = self.editing.take();
Ok(Some(id))
}
Edit::Name(name) => {
d.name = name;
Ok(None)
}
Edit::Kind(kind) => {
if !scope.kinds(d.layer).contains(&kind) {
return Err("Choose an explicitly supported source kind.".into());
}
d.kind = Some(kind);
d.parent = None;
d.page = 0;
Ok(None)
}
Edit::Parent(parent) => {
let kind = d.kind.ok_or("Choose the source kind first.")?;
if !validation::can_parent(p, kind, d.layer, parent) {
return Err("Choose a valid semantic parent.".into());
}
d.parent = parent.map(|id| p.elements[&id].clone());
Ok(None)
}
Edit::Search(query) => {
d.search = query;
d.page = 0;
Ok(None)
}
Edit::Page(page) => {
d.page = page;
Ok(None)
}
}
}
}
}
pub fn preview(&self, p: &Project, selected: Option<Id>) -> Result<Option<Project>, String> {
if self.is_open() {
return Err("Use this new source or go Back before creating the consumer.".into());
}
self.staged
.as_ref()
.filter(|d| Some(d.id) == selected)
.map(|d| execute_batch(p, &[d.command(p)?]).map_err(|e| e.to_string()))
.transpose()
}
pub fn combine(
&self,
p: &Project,
selected: Option<Id>,
command: Command,
) -> Result<Command, String> {
let Some(d) = self.staged.as_ref().filter(|d| Some(d.id) == selected) else {
return Ok(command);
};
let preview = execute_batch(p, &[d.command(p)?]).map_err(|e| e.to_string())?;
let mut commands = match command {
Command::Batch(commands) => commands,
other => vec![other],
};
let fresh: Vec<_> = commands
.iter()
.filter(|_| !matches!(d.scope, Scope::Machine { .. }))
.filter_map(|c| match c {
Command::CreateElement(e) => Some(e.id),
Command::CreateRelation(r) => Some(r.id),
_ => None,
})
.collect();
if fresh.len() > d.identities.len() {
return Err(
"The path grew beyond this draft. Return and create the source again.".into(),
);
}
let replacements: BTreeMap<_, _> = fresh
.into_iter()
.zip(d.identities.iter().copied())
.collect();
if replacements.values().any(|id| preview.contains(*id)) {
return Err(
"A reserved consumer identity is already used. Start a fresh source draft.".into(),
);
}
let remap = |id: &mut Id| {
if let Some(new) = replacements.get(id) {
*id = *new;
}
};
for c in &mut commands {
match c {
Command::CreateElement(e) => {
remap(&mut e.id);
references::visit_element_refs(e, remap);
e.alias = preview.fresh_alias(&e.name, "contribution", e.id);
}
Command::CreateRelation(r) => {
remap(&mut r.id);
references::visit_relation_refs(r, remap);
r.alias = preview.fresh_alias(&r.name, "path", r.id);
}
Command::EditRelation(_, r) => references::visit_relation_refs(r, remap),
Command::SetSteps(_, steps) => steps.iter_mut().for_each(remap),
// Machine commands already carry their reserved identities.
Command::CreateDiagram(_) | Command::Layout(_) => {}
_ => return Err("Unsupported prerequisite consumer operation.".into()),
}
}
commands.insert(0, d.command(p)?);
let command = super::topology::unique_batch(p, commands);
execute_batch(p, std::slice::from_ref(&command)).map_err(|e| e.to_string())?;
Ok(command)
}
pub fn view<'a, R: iced::advanced::text::Renderer + 'a>(
&'a self,
p: &'a Project,
) -> Option<UiElement<'a, Event, iced::Theme, R>> {
let d = self.editing.as_ref()?;
let event = move |edit| Event::Edit(d.token, edit);
let mut fields = column![
text(format!("New source · {}", d.layer)).size(18),
text(match d.scope {
Scope::Lifeline { owner, viewpoint } =>
format!("For a lifeline in {} · {:?}", p.named(owner), viewpoint),
Scope::Work { owner } => format!("For a contribution in {}", p.named(owner)),
Scope::Machine { .. } => "Owner for the pending state machine".into(),
})
.size(12),
text("The parent form is retained. This source is created only with its consumer.")
.size(12)
]
.spacing(8);
for kind in d.scope.kinds(d.layer) {
fields = fields.push(
button(text(kind.to_string()))
.on_press(event(Edit::Kind(kind)))
.style(if d.kind == Some(kind) {
button::primary
} else {
button::secondary
}),
);
}
fields = fields.push(
text_input("Source name (independent of the consumer name)", &d.name)
.id(NAME_INPUT)
.on_input(move |v| event(Edit::Name(v)))
.padding(8),
);
fields = fields.push(
text(format!(
"Semantic parent: {}",
d.parent
.as_ref()
.map_or(format!("{} layer root", d.layer), |e| format!(
"{} · {} · @{}",
e.name,
e.kind(),
e.alias
))
))
.size(12),
);
if let Some(kind) = d.kind {
fields = fields.push(button("Use layer root").on_press_maybe(
validation::can_parent(p, kind, d.layer, None).then(|| event(Edit::Parent(None))),
));
fields = fields.push(
text_input("Find a semantic parent (optional)", &d.search)
.on_input(move |v| event(Edit::Search(v)))
.padding(6),
);
let query = d.search.to_lowercase();
let mut parents: Vec<_> = p
.elements
.values()
.filter(|e| validation::can_parent(p, kind, d.layer, Some(e.id)))
.map(|e| (e.id, super::creation::reference_label(p, e.id)))
.filter(|(_, label)| label.to_lowercase().contains(&query))
.collect();
parents.sort_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0)));
let page = d.page.min(parents.len().saturating_sub(1) / 6);
for (id, label) in parents.iter().skip(page * 6).take(6) {
fields = fields.push(
button(text(label.clone()).size(12))
.on_press(event(Edit::Parent(Some(*id))))
.width(Fill),
);
}
if parents.len() > 6 {
fields = fields.push(
row![
button("Previous").on_press_maybe(
(page > 0).then(|| event(Edit::Page(page.saturating_sub(1))))
),
text(format!("{} / {}", page + 1, parents.len().div_ceil(6))),
button("Next").on_press_maybe(
((page + 1) * 6 < parents.len()).then(|| event(Edit::Page(page + 1)))
)
]
.spacing(6),
);
}
}
let ready = d.command(p);
if let Err(error) = &ready {
fields = fields.push(text(error.clone()).size(12));
}
Some(
container(
column![
scrollable(fields).id("behavior-source-fields").height(Fill),
container(
row![
button("Back · Esc").on_press(event(Edit::Back)),
button("Use this new source")
.on_press_maybe(ready.is_ok().then(|| event(Edit::Use)))
]
.spacing(8)
)
.id("behavior-source-actions")
]
.spacing(10),
)
.padding(12)
.height(Fill)
.width(Fill)
.into(),
)
}
}
impl Draft {
fn command(&self, p: &Project) -> Result<Command, String> {
if self.document != p.id
|| self.scope.layer(p)? != self.layer
|| self
.owner
.as_ref()
.is_some_and(|e| p.elements.get(&e.id) != Some(e))
{
return Err(
"The source request's document or consumer changed. Go Back and reopen Create new."
.into(),
);
}
let kind = self
.kind
.filter(|kind| self.scope.kinds(self.layer).contains(kind))
.ok_or("Choose the source kind explicitly.")?;
if self.name.trim().is_empty() {
return Err("Enter the source name.".into());
}
if p.contains(self.id) {
return Err(
"The reserved source identity is already used. Go Back and start again.".into(),
);
}
let parent = self.parent.as_ref().map(|e| e.id);
if self
.parent
.as_ref()
.is_some_and(|e| p.elements.get(&e.id) != Some(e))
|| !validation::can_parent(p, kind, self.layer, parent)
{
return Err("The semantic parent changed. Choose its intended parent again.".into());
}
let e = Element {
id: self.id,
alias: p.fresh_alias(&self.name, "source", self.id),
name: self.name.clone(),
description: String::new(),
parent,
body: ElementBody::new(kind, self.layer)
.ok_or("This source kind is unavailable in this layer.")?,
};
let command = Command::CreateElement(e);
execute_batch(p, std::slice::from_ref(&command)).map_err(|e| e.to_string())?;
Ok(command)
}
}