Integrate contextual source creation with lifecycle rendering

This commit is contained in:
2026-09-11 10:38:05 -07:00
parent 4da92ab481
commit 321897fd55
21 changed files with 4250 additions and 471 deletions
+132 -3
View File
@@ -94,6 +94,7 @@ impl std::fmt::Display for CanvasTool {
}
#[derive(Debug, Clone)]
pub enum CanvasEvent {
First(Context, Tool),
Scrolled(Context, ScrollArea, iced::Vector),
Restored(Context, Id, ScrollArea, iced::Vector),
Viewport(Context, Size),
@@ -132,7 +133,8 @@ pub enum ScrollArea {
impl CanvasEvent {
pub fn context(&self) -> Context {
match self {
Self::Scrolled(context, _, _)
Self::First(context, _)
| Self::Scrolled(context, _, _)
| Self::Restored(context, ..)
| Self::Viewport(context, _)
| Self::Zoom(context, _)
@@ -162,6 +164,7 @@ impl std::fmt::Display for Operator {
}
#[derive(Debug, Clone)]
pub enum Event {
Source(super::source_draft::Event),
Start(Id, Tool),
Cancel,
Submit,
@@ -240,6 +243,13 @@ struct CallChoiceContext {
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Draft {
#[serde(default)]
source: super::source_draft::State,
#[serde(default = "Id::new_v4")]
invocation: Id,
#[serde(default)]
source_origin: Option<(Id, oa_core::Element)>,
owner: Id,
parent: Id,
before: Option<Id>,
@@ -272,6 +282,28 @@ struct Draft {
choice_page: usize,
}
impl State {
pub(super) fn pending_name(&self) -> Option<&str> {
self.draft.as_ref().map(|d| d.name.as_str())
}
pub(super) fn source_view<'a>(&'a self, p: &'a Project) -> Option<iced::Element<'a, Event>> {
self.draft
.as_ref()?
.source
.view(p)
.map(|v| v.map(Event::Source))
}
pub fn source_active(&self) -> bool {
self.draft.as_ref().is_some_and(|d| d.source.engaged())
}
pub fn source_open(&self) -> bool {
self.draft.as_ref().is_some_and(|d| d.source.is_open())
}
pub fn source_back(&mut self) -> bool {
self.draft.as_mut().is_some_and(|d| d.source.back())
}
pub(crate) fn enter(&mut self, document: Id, owner: Id) {
if self.presentation.identity != Some((document, owner)) {
self.presentation = workspace::Presentation {
@@ -452,8 +484,16 @@ impl State {
}
cached.as_ref().unwrap().1.clone()
}
pub(crate) fn created_selection(&self, command: &Command) -> Option<Id> {
pub(crate) fn created_selection(&self, p: &Project, command: &Command) -> Option<Id> {
let draft = self.draft.as_ref()?;
if self
.draft
.as_ref()
.is_some_and(|d| d.source.label(d.target).is_some())
&& self.prepare_pending(p).ok().as_ref() != Some(command)
{
return None;
}
fn created(command: &Command, parent: Id) -> Option<Id> {
match command {
Command::CreateElement(element) if element.parent == Some(parent) => {
@@ -568,6 +608,61 @@ impl State {
}
pub fn update(&mut self, p: &Project, event: Event) -> Result<Option<Command>, String> {
if let Event::Source(event) = event {
if self
.draft
.as_mut()
.ok_or("Start the consumer form first.")?
.source
.dismiss(&event)?
{
return Ok(None);
}
let d = self
.draft
.as_ref()
.ok_or("Start the consumer form first.")?;
if d.tool != Tool::Lifeline {
return Err("This reference does not support staged source creation.".into());
}
if d.source_origin.as_ref().is_some_and(|(document, owner)| {
*document != p.id || p.elements.get(&d.owner) != Some(owner)
}) {
return Err("The original consumer changed. Cancel and reopen its form; the current draft is retained.".into());
}
if d.source_origin.is_none() && d.source.engaged() {
return Err("The recovered source draft has no original document binding. Go Back or cancel it.".into());
}
let mut next = d.clone();
if next.source_origin.is_none() && matches!(&event, super::source_draft::Event::Open(_))
{
next.source_origin = Some((
p.id,
p.elements
.get(&d.owner)
.ok_or("The consumer owner no longer exists.")?
.clone(),
));
}
if let Some(id) = next.source.update(
p,
super::source_draft::Scope::Lifeline {
owner: d.owner,
viewpoint: viewpoint(p, d.owner),
},
d.invocation,
event,
)? {
next.target = Some(id);
next.command(p)?;
}
self.draft = Some(next);
return Ok(None);
}
if self.source_open() && !matches!(&event, Event::Cancel) {
return Err("Use the new source or go Back before editing the parent form.".into());
}
if self.draft.is_some() && matches!(&event, Event::Start(..)) {
return Err(PENDING_DRAFT.into());
}
@@ -577,6 +672,9 @@ impl State {
return Err("Choose a scenario.".into());
}
self.draft = Some(Draft {
source: Default::default(),
invocation: Id::new_v4(),
source_origin: Some((p.id, p.elements[&owner].clone())),
owner,
parent: owner,
before: None,
@@ -940,7 +1038,14 @@ fn valid_body_choice(p: &Project, owner: Id, body: OccurrenceBody) -> bool {
execute_batch(p, &[Command::CreateElement(e)]).is_ok()
}
fn sources(p: &Project, d: &Draft) -> Vec<Choice> {
let layer = p.elements[&d.owner].layer();
let Some(owner) = p
.elements
.get(&d.owner)
.filter(|e| e.kind() == Kind::Scenario)
else {
return vec![];
};
let layer = owner.layer();
let mut values: Vec<_> = p
.elements
.values()
@@ -1256,6 +1361,28 @@ impl Draft {
e
}
fn command(&self, p: &Project) -> Result<Command, String> {
if self.source.engaged()
&& self.source_origin.as_ref().is_none_or(|(document, owner)| {
*document != p.id || p.elements.get(&self.owner) != Some(owner)
})
{
return Err("The original consumer document or owner changed. Cancel and reopen its form; your draft is retained.".into());
}
self.source.check_scope(
super::source_draft::Scope::Lifeline {
owner: self.owner,
viewpoint: viewpoint(p, self.owner),
},
self.target,
)?;
let preview = self.source.preview(p, self.target)?;
let command = self.consumer_command(preview.as_ref().unwrap_or(p))?;
self.source.combine(p, self.target, command)
}
fn consumer_command(&self, p: &Project) -> Result<Command, String> {
if p.elements.get(&self.owner).map(oa_core::Element::kind) != Some(Kind::Scenario) {
return Err("The scenario no longer exists. Cancel or restore its owner.".into());
}
if !parents(p, self.owner).iter().any(|c| c.id == self.parent) {
return Err("Choose this scenario or one of its operands as destination.".into());
}
@@ -1266,6 +1393,8 @@ impl Draft {
.unwrap_or(self.tool.label());
let name = if self.name.trim().is_empty() {
default
} else if self.source.label(self.target).is_some() {
self.name.as_str()
} else {
self.name.trim()
};