//! 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 { match self { Self::Lifeline { owner, .. } | Self::Work { owner } => Some(owner), Self::Machine { .. } => None, } } fn layer(self, p: &Project) -> Result { 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 { 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), 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, id: Id, kind: Option, name: String, parent: Option, search: String, page: usize, identities: Vec, } #[derive(Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct State { editing: Option, staged: Option, #[serde(default)] engaged: bool, } impl State { pub fn check_scope(&self, expected: Scope, selected: Option) -> 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 { 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) -> Option { 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, 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) -> Result, 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, command: Command, ) -> Result { 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> { 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 { 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) } }