From 956a5841322a6fdca91411f32a3ede1ba8dcf853 Mon Sep 17 00:00:00 2001 From: "sorokin.oleg.edu" Date: Sat, 12 Sep 2026 06:51:18 -0700 Subject: [PATCH] Render nested call activations with correlation-aware anchors --- crates/oa-desktop/src/behavior.rs | 203 ++--- .../src/behavior/scenario_activation.rs | 389 +++++++++ .../src/behavior/scenario_activation/tests.rs | 749 ++++++++++++++++++ 3 files changed, 1203 insertions(+), 138 deletions(-) create mode 100644 crates/oa-desktop/src/behavior/scenario_activation.rs create mode 100644 crates/oa-desktop/src/behavior/scenario_activation/tests.rs diff --git a/crates/oa-desktop/src/behavior.rs b/crates/oa-desktop/src/behavior.rs index 882bdb60..de440d3e 100644 --- a/crates/oa-desktop/src/behavior.rs +++ b/crates/oa-desktop/src/behavior.rs @@ -20,6 +20,7 @@ mod contextual_source_tests; pub(crate) mod creation; mod form_session; pub(crate) mod scenario; +mod scenario_activation; // Projection acceptance precedes the coordinated scenario renderer integration. #[cfg(test)] mod scenario_lifecycle; @@ -1145,6 +1146,7 @@ struct Scene { lanes: BTreeMap, slots: Vec, rows: BTreeMap, + activations: Vec, open_calls: Vec, sequence_bounds: BTreeMap, } @@ -1208,6 +1210,7 @@ impl Scene { lanes: BTreeMap::new(), slots: vec![], rows: BTreeMap::new(), + activations: vec![], open_calls: vec![], sequence_bounds: BTreeMap::new(), }; @@ -1297,29 +1300,7 @@ impl Scene { let mut y = if scenario { 148. } else { 120. }; scene.occurrences(p, owner, scenario, &lane_x, &mut y, 0); if scenario { - let calls: Vec<_> = scene - .rows - .keys() - .filter_map(|&id| { - if let Some(SharedBody::Occurrence { - body: - OccurrenceBody::Exchange { - message_kind: MessageKind::Call, - receiver: Some(receiver), - .. - }, - .. - }) = p.elements[&id].shared() - { - Some((id, *lane_x.get(receiver)?)) - } else { - None - } - }) - .collect(); - for (call, x) in calls { - scene.activation_sequence(p, p.elements[&call].parent.unwrap(), call, x, None); - } + scene.layout_activations(p, &lane_x); } scene.size.height = (y + 45.).max(210.); for (id, name, detail) in lanes { @@ -1598,106 +1579,6 @@ impl Scene { }); } } - fn activation_segment(&mut self, call: Id, x: f32, top: f32, bottom: f32) { - if bottom <= top { - return; - } - self.boxes.push(BoxItem { - id: call, - rect: Rectangle::new(Point::new(x - 4., top), Size::new(8., bottom - top)), - title: String::new(), - detail: String::new(), - marker: None, - }); - } - /// Follow a single correlation through the ordered tree. Branches are drawn - /// as independent segments, never as one chronological stretch through ALT - /// operands. The result says whether a path can leave this sequence open. - fn activation_sequence( - &mut self, - p: &Project, - owner: Id, - call: Id, - x: f32, - mut start: Option, - ) -> bool { - let entered_open = start.is_some(); - let mut split = false; - for id in p.elements[&owner].steps().into_iter().flatten() { - let Some(&(_, y)) = self.rows.get(id) else { - continue; - }; - if *id == call { - start = Some(y); - } - let Some(top) = start else { - continue; - }; - match p.elements[id].shared() { - Some(SharedBody::Occurrence { - body: - OccurrenceBody::Exchange { - message_kind: MessageKind::Reply, - reply_to: Some(correlation), - .. - }, - .. - }) if *correlation == call => { - self.activation_segment(call, x, top, y); - self.boxes.push(BoxItem { - id: call, - rect: Rectangle::new(Point::new(x - 7., y - 1.), Size::new(14., 2.)), - title: String::new(), - detail: String::new(), - marker: None, - }); - start = None; - } - Some(SharedBody::Occurrence { - body: OccurrenceBody::Fragment { operator, branches }, - .. - }) => { - split = true; - self.activation_segment(call, x, top, y); - let bottom = self - .boxes - .iter() - .find(|b| b.id == *id && !b.title.is_empty()) - .map(|b| b.rect.y + b.rect.height) - .unwrap_or(y); - let outcomes: Vec<_> = branches - .iter() - .filter_map(|branch| { - let &(top, _) = self.sequence_bounds.get(branch)?; - Some(self.activation_sequence(p, *branch, call, x, Some(top))) - }) - .collect(); - let open = match operator { - FragmentOperator::Alternative => { - outcomes.is_empty() || outcomes.iter().any(|open| *open) - } - FragmentOperator::Parallel => outcomes.iter().all(|open| *open), - // Optional/repeated bodies do not guarantee execution. - FragmentOperator::Optional | FragmentOperator::Loop => true, - }; - start = open.then_some(bottom); - } - _ => {} - } - } - if let Some(top) = start { - let bottom = if entered_open || split { - self.sequence_bounds - .get(&owner) - .map_or(top + 26., |(_, bottom)| (*bottom).max(top + 26.)) - } else { - top + 26. - }; - self.activation_segment(call, x, top, bottom); - self.open_calls.push(Point::new(x + 8., bottom - 7.)); - } - start.is_some() - } fn slot_at(&self, point: Point, parent: Option) -> Option<&SequenceSlot> { self.slots .iter() @@ -1749,18 +1630,31 @@ impl Scene { ) .contains(point) || line.points.windows(2).any(|p| { - crate::diagram::distance_to_segment(point, p[0], p[1]) - < 8. / self.scale + crate::diagram::distance_to_segment(point, p[0], p[1]) < 1.5 })) }) .map(|line| line.id) }) .or_else(|| { - self.boxes + self.activations .iter() .rev() - .find(|b| b.rect.contains(point) && b.title.is_empty()) - .map(|b| b.id) + .find(|b| b.rect.contains(point)) + .map(|b| b.call) + }) + // The generous message tolerance must not steal the exposed + // shoulder of another call's execution when the view is zoomed out. + .or_else(|| { + self.lines + .iter() + .find(|line| { + !line.label.is_empty() + && line.points.windows(2).any(|p| { + crate::diagram::distance_to_segment(point, p[0], p[1]) + < 8. / self.scale + }) + }) + .map(|line| line.id) }) .or_else(|| self.lane_at(point)) .or_else(|| { @@ -2060,6 +1954,43 @@ impl canvas::Program for Scene { f.fill_rectangle(Point::ORIGIN, bounds.size(), Color::WHITE); f.translate(self.camera_pan); f.scale(self.scale); + // Guides belong behind execution fills and cards. In particular, a + // lifeline must not cut through the middle of its activation rectangle. + for guide in self.lines.iter().filter(|line| line.label.is_empty()) { + f.stroke( + &Path::new(|path| { + path.move_to(guide.points[0]); + for point in &guide.points[1..] { + path.line_to(*point); + } + }), + Stroke::default() + .with_color(if guide.id == self.selected { + style::TEAL + } else { + style::MUTED + }) + .with_width(if guide.id == self.selected { 2.5 } else { 1.2 }), + ); + } + for activation in &self.activations { + let path = Path::rectangle(activation.rect.position(), activation.rect.size()); + f.fill(&path, Color::from_rgb8(243, 248, 248)); + f.stroke( + &path, + Stroke::default() + .with_color(if activation.call == self.selected { + style::TEAL + } else { + style::LINE + }) + .with_width(if activation.call == self.selected { + 2.5 + } else { + 1. + }), + ); + } for b in &self.boxes { let outline = matches!( b.marker, @@ -2086,7 +2017,7 @@ impl canvas::Program for Scene { ); } } - for l in &self.lines { + for l in self.lines.iter().filter(|line| !line.label.is_empty()) { let color = if l.id == self.selected { style::TEAL } else { @@ -2917,9 +2848,9 @@ mod direct_canvas_tests { p = execute_batch(&p, &commands).unwrap(); let scene = Scene::sequence(&p, f.owner, f.call); let bars: Vec<_> = scene - .boxes + .activations .iter() - .filter(|b| b.id == f.call && b.rect.width == 8.) + .filter(|b| b.call == f.call) .map(|b| b.rect) .collect(); for reply in &replies { @@ -2957,11 +2888,7 @@ mod direct_canvas_tests { #[test] fn direct_execution_span_ends_at_its_correlated_reply_and_marks_an_open_call() { let (f, scene) = scene(); - let bar = scene - .boxes - .iter() - .find(|b| b.id == f.call && b.rect.width == 8.) - .unwrap(); + let bar = scene.activations.iter().find(|b| b.call == f.call).unwrap(); assert_eq!(bar.rect.y + bar.rect.height, scene.rows[&f.reply].1); assert!(scene.open_calls.is_empty()); let p = execute_batch(&f.project, &[Command::Delete(f.reply)]).unwrap(); @@ -2969,9 +2896,9 @@ mod direct_canvas_tests { assert_eq!(scene.open_calls.len(), 1); assert_eq!( scene - .boxes + .activations .iter() - .find(|b| b.id == f.call && b.rect.width == 8.) + .find(|b| b.call == f.call) .unwrap() .rect .height, diff --git a/crates/oa-desktop/src/behavior/scenario_activation.rs b/crates/oa-desktop/src/behavior/scenario_activation.rs new file mode 100644 index 00000000..fdafed58 --- /dev/null +++ b/crates/oa-desktop/src/behavior/scenario_activation.rs @@ -0,0 +1,389 @@ +//! Derived call geometry. Correlation and branch ownership come from the model; +//! columns separate overlapping drawings and do not assert an execution thread. +use super::*; + +const WIDTH: f32 = 12.; +const OFFSET: f32 = 6.; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum Finish { + Reply(Id), + Boundary, + /// Still open at this scope boundary; the visible span is not an isolated stub. + Continuation, + Open, +} + +#[derive(Clone, Debug)] +pub(super) struct Segment { + pub call: Id, + pub receiver: Id, + pub scope: Id, + pub rect: Rectangle, + pub column: usize, + pub finish: Finish, +} +impl Segment { + fn bottom(&self) -> f32 { + self.rect.y + self.rect.height + } + fn overlaps(&self, other: &Self) -> bool { + self.receiver == other.receiver + && self.scope == other.scope + && self.rect.y < other.bottom() + && other.rect.y < self.bottom() + } + fn edge(&self, right: bool) -> f32 { + self.rect.x + if right { self.rect.width } else { 0. } + } +} + +impl Scene { + pub(super) fn layout_activations(&mut self, p: &Project, lanes: &BTreeMap) { + let mut calls: Vec<_> = self + .rows + .iter() + .filter_map(|(&call, &(scope, y))| { + let Some(SharedBody::Occurrence { + body: + OccurrenceBody::Exchange { + message_kind: MessageKind::Call, + receiver: Some(receiver), + .. + }, + .. + }) = p.elements.get(&call).and_then(|e| e.shared()) + else { + return None; + }; + Some((call, *receiver, scope, y, *lanes.get(receiver)?)) + }) + .collect(); + // Rows are laid out in semantic order. UUID order must not choose which + // rectangle obscures another (especially after source reidentification). + calls.sort_by(|a, b| a.3.total_cmp(&b.3).then_with(|| a.0.cmp(&b.0))); + for &(call, receiver, scope, _, x) in &calls { + let first = self.activations.len(); + self.activation_sequence(p, scope, call, receiver, x, None); + let column = self.activations[first..] + .iter() + .flat_map(|segment| { + self.activations[..first] + .iter() + .filter(move |other| segment.overlaps(other)) + }) + .map(|other| other.column + 1) + .max() + .unwrap_or(0); + for segment in &mut self.activations[first..] { + segment.column = column; + segment.rect.x += column as f32 * OFFSET; + } + } + // Shallow executions paint first; columns cannot collide within a scope. + self.activations.sort_by(|a, b| { + a.column + .cmp(&b.column) + .then_with(|| a.rect.y.total_cmp(&b.rect.y)) + .then_with(|| a.call.cmp(&b.call)) + }); + self.open_calls = self + .activations + .iter() + .filter(|a| matches!(a.finish, Finish::Open | Finish::Continuation)) + .map(|a| Point::new(a.rect.x + a.rect.width + 4., a.bottom() - 7.)) + .collect(); + self.anchor_activation_messages(p, lanes); + } + + fn activation_segment( + &mut self, + call: Id, + receiver: Id, + scope: Id, + x: f32, + span: std::ops::Range, + finish: Finish, + ) { + if span.end > span.start { + self.activations.push(Segment { + call, + receiver, + scope, + rect: Rectangle::new( + Point::new(x - WIDTH / 2., span.start), + Size::new(WIDTH, span.end - span.start), + ), + column: 0, + finish, + }); + } + } + + /// Follow one exact correlation. A segment belongs to one ordered scope; + /// vertically stacked operands must not become a single chronological span. + fn activation_sequence( + &mut self, + p: &Project, + owner: Id, + call: Id, + receiver: Id, + x: f32, + mut start: Option, + ) -> bool { + let entered_open = start.is_some(); + let mut split = false; + for id in p.elements[&owner].steps().into_iter().flatten() { + let Some(&(_, y)) = self.rows.get(id) else { + continue; + }; + if *id == call { + // A reflective call is received at the lower end of its loop. + start = Some( + self.lines + .iter() + .find(|line| line.id == call) + .and_then(|line| line.points.last()) + .map_or(y, |point| point.y), + ); + } + let Some(top) = start else { + continue; + }; + match p.elements[id].shared() { + Some(SharedBody::Occurrence { + body: + OccurrenceBody::Exchange { + message_kind: MessageKind::Reply, + reply_to: Some(correlation), + .. + }, + .. + }) if *correlation == call => { + // Execution ends when its reply is sent, including a self reply. + self.activation_segment(call, receiver, owner, x, top..y, Finish::Reply(*id)); + start = None; + } + Some(SharedBody::Occurrence { + body: OccurrenceBody::Fragment { operator, branches }, + .. + }) => { + split = true; + self.activation_segment(call, receiver, owner, x, top..y, Finish::Boundary); + let bottom = self + .boxes + .iter() + .find(|b| b.id == *id && !b.title.is_empty()) + .map(|b| b.rect.y + b.rect.height) + .unwrap_or(y); + let outcomes: Vec<_> = branches + .iter() + .filter_map(|branch| { + let &(top, _) = self.sequence_bounds.get(branch)?; + Some(self.activation_sequence(p, *branch, call, receiver, x, Some(top))) + }) + .collect(); + let open = match operator { + FragmentOperator::Alternative => { + outcomes.is_empty() || outcomes.iter().any(|open| *open) + } + FragmentOperator::Parallel => outcomes.iter().all(|open| *open), + FragmentOperator::Optional | FragmentOperator::Loop => true, + }; + start = open.then_some(bottom); + } + _ => {} + } + } + if let Some(top) = start { + let bottom = if entered_open || split { + self.sequence_bounds + .get(&owner) + .map_or(top + 26., |(_, bottom)| (*bottom).max(top + 26.)) + } else { + top + 26. + }; + let finish = if entered_open || split { + Finish::Continuation + } else { + Finish::Open + }; + self.activation_segment(call, receiver, owner, x, top..bottom, finish); + } + start.is_some() + } + + fn anchor_activation_messages(&mut self, p: &Project, lanes: &BTreeMap) { + let segments = &self.activations; + let mut correlations: BTreeMap = BTreeMap::new(); + for segment in segments { + let correlation = correlations.entry(segment.call).or_insert(Correlation { + start: segment.rect.y, + replies: vec![], + }); + correlation.start = correlation.start.min(segment.rect.y); + if matches!(segment.finish, Finish::Reply(_)) { + correlation.replies.push((segment.scope, segment.bottom())); + } + } + for line in &mut self.lines { + let Some(SharedBody::Occurrence { + body: + OccurrenceBody::Exchange { + sender: Some(sender), + receiver: Some(receiver), + message_kind, + reply_to, + .. + }, + .. + }) = p.elements.get(&line.id).and_then(|e| e.shared()) + else { + continue; + }; + if !matches!( + message_kind, + MessageKind::Call | MessageKind::Reply | MessageKind::Asynchronous + ) { + continue; + } + let (Some(&from), Some(&to), Some(&(scope, _))) = ( + lanes.get(sender), + lanes.get(receiver), + self.rows.get(&line.id), + ) else { + continue; + }; + let send_y = line.points[0].y; + let receive_y = line.points.last().unwrap().y; + let reflective = sender == receiver; + let source = if *message_kind == MessageKind::Reply { + reply_to.and_then(|call| { + segments + .iter() + .find(|a| a.call == call && a.finish == Finish::Reply(line.id)) + }) + } else { + active_segment( + p, + segments, + &correlations, + *sender, + scope, + send_y, + Some(line.id), + ) + }; + let target = if *message_kind == MessageKind::Call { + segments + .iter() + .find(|a| a.call == line.id && a.scope == scope && a.rect.y == receive_y) + } else { + active_segment( + p, + segments, + &correlations, + *receiver, + scope, + receive_y, + *reply_to, + ) + }; + let start = source.map_or(from, |a| a.edge(reflective || to > from)); + let end = target.map_or(to, |a| a.edge(reflective || from > to)); + if reflective { + let outside = start.max(end) + 60.; + line.points = vec![ + Point::new(start, send_y), + Point::new(outside, send_y), + Point::new(outside, receive_y), + Point::new(end, receive_y), + ]; + } else { + line.points = vec![Point::new(start, send_y), Point::new(end, receive_y)]; + } + line.label_at.x = start.min(end) + 12.; + } + } +} + +/// A caller has no implicit execution just because it sent a message. Only an +/// actually drawn, known-duration execution can supply its anchor. Crossed +/// intervals provide no unambiguous innermost execution, so fall back to the lane. +fn active_segment<'a>( + p: &Project, + segments: &'a [Segment], + correlations: &BTreeMap, + receiver: Id, + scope: Id, + y: f32, + exclude: Option, +) -> Option<&'a Segment> { + let candidates: Vec<_> = segments + .iter() + .filter(|a| { + a.receiver == receiver + && a.scope == scope + && Some(a.call) != exclude + && a.rect.y <= y + && y < a.bottom() + && a.finish != Finish::Open + }) + .collect(); + let inner = candidates.iter().max_by_key(|a| a.column).copied()?; + candidates + .iter() + .all(|a| { + a.call == inner.call + || correlations[&a.call].contains(p, scope, &correlations[&inner.call]) + }) + .then_some(inner) +} + +struct Correlation { + start: f32, + replies: Vec<(Id, f32)>, +} +impl Correlation { + fn contains(&self, p: &Project, scope: Id, inner: &Self) -> bool { + // Segment clipping at a fragment can make crossed calls appear nested. + // Compare actual correlated replies on compatible scope paths instead. + let relevant = |&(reply_scope, _): &(Id, f32)| { + ancestor(p, scope, reply_scope) || ancestor(p, reply_scope, scope) + }; + let mut compared = false; + for &(inner_scope, inner_end) in inner.replies.iter().filter(|reply| relevant(reply)) { + let mut matched = false; + for &(outer_scope, outer_end) in self.replies.iter().filter(|reply| relevant(reply)) { + if ancestor(p, inner_scope, outer_scope) || ancestor(p, outer_scope, inner_scope) { + // Alternative operands are not chronological. Compare only + // returns that can belong to the same enclosing branch path. + if outer_end < inner_end { + return false; + } + matched = true; + } + } + if !matched { + return false; + } + compared = true; + } + self.start <= inner.start && compared + } +} +fn ancestor(p: &Project, expected: Id, mut scope: Id) -> bool { + for _ in 0..=p.elements.len() { + if scope == expected { + return true; + } + let Some(parent) = p.elements.get(&scope).and_then(|e| e.parent) else { + return false; + }; + scope = parent; + } + false +} + +#[cfg(test)] +mod tests; diff --git a/crates/oa-desktop/src/behavior/scenario_activation/tests.rs b/crates/oa-desktop/src/behavior/scenario_activation/tests.rs new file mode 100644 index 00000000..abf0cfbd --- /dev/null +++ b/crates/oa-desktop/src/behavior/scenario_activation/tests.rs @@ -0,0 +1,749 @@ +use super::*; + +fn id(n: u128) -> Id { + Id::from_u128(0x21ca1100000040008000000000000000 + n) +} +struct Fixture { + project: Project, + owner: Id, + caller: Id, + callee: Id, + call: Id, + inner: Id, + reply: Id, + inner_reply: Id, +} +fn seed(p: &mut Project, layer: Layer, n: u128, kind: Kind, parent: Option) -> Id { + let mut e = p + .make_element(kind, layer, &format!("Element {n}"), parent) + .unwrap(); + e.id = id(n); + e.alias = format!("element_{n}"); + p.elements.insert(e.id, e); + id(n) +} +fn message( + p: &mut Project, + layer: Layer, + n: u128, + parent: Id, + endpoints: (Id, Id), + reply: Option, +) -> Id { + let e = seed(p, layer, n, Kind::Occurrence, Some(parent)); + *p.elements.get_mut(&e).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { + body: OccurrenceBody::Exchange { + operation: None, + exchange: None, + sender: Some(endpoints.0), + receiver: Some(endpoints.1), + message_kind: if reply.is_some() { + MessageKind::Reply + } else { + MessageKind::Call + }, + reply_to: reply, + values: vec![], + }, + precondition: String::new(), + postcondition: String::new(), + }; + e +} +fn fixture(layer: Layer, reverse_uuid_order: bool) -> Fixture { + let mut p = Project::empty(); + let work = if layer == Layer::Oa { + Kind::Activity + } else { + Kind::Function + }; + let a = seed(&mut p, layer, 1, work, None); + let b = seed(&mut p, layer, 2, work, None); + let owner = seed(&mut p, layer, 3, Kind::Scenario, None); + if let SharedBody::Scenario { viewpoint, .. } = + p.elements.get_mut(&owner).unwrap().shared_mut().unwrap() + { + *viewpoint = if layer == Layer::Oa { + ScenarioViewpoint::OperationalActivity + } else { + ScenarioViewpoint::Functional + }; + } + let caller = seed(&mut p, layer, 4, Kind::Lifeline, Some(owner)); + let callee = seed(&mut p, layer, 5, Kind::Lifeline, Some(owner)); + for (line, represented) in [(caller, a), (callee, b)] { + *p.elements.get_mut(&line).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { + represented: Some(represented), + }; + } + let (first, second) = if reverse_uuid_order { + (40, 30) + } else { + (30, 40) + }; + let call = message(&mut p, layer, first, owner, (caller, callee), None); + let inner = message(&mut p, layer, second, owner, (caller, callee), None); + let reply = message(&mut p, layer, 50, owner, (callee, caller), Some(call)); + let inner_reply = message(&mut p, layer, 60, owner, (callee, caller), Some(inner)); + *p.elements.get_mut(&owner).unwrap().steps_mut().unwrap() = + vec![call, inner, inner_reply, reply]; + Fixture { + project: p, + owner, + caller, + callee, + call, + inner, + reply, + inner_reply, + } +} +fn scene(f: &Fixture) -> Scene { + let errors: Vec<_> = validation::validate(&f.project) + .into_iter() + .filter(|i| i.severity == validation::Severity::Error) + .collect(); + assert!(errors.is_empty(), "Actual core fixture: {errors:#?}"); + let source = language::format(&f.project); + let reparsed = language::parse(&source).unwrap(); + assert_eq!(language::format(&reparsed), source); + let result = Scene::sequence(&reparsed, f.owner, f.owner); + assert_eq!( + language::format(&reparsed), + source, + "Projection must not write geometry into the model" + ); + result +} +fn bar(scene: &Scene, call: Id) -> &Segment { + scene.activations.iter().find(|a| a.call == call).unwrap() +} +fn line(scene: &Scene, occurrence: Id) -> &super::super::Line { + scene.lines.iter().find(|a| a.id == occurrence).unwrap() +} +fn endpoints(p: &mut Project, occurrence: Id, from: Id, to: Id) { + if let SharedBody::Occurrence { + body: OccurrenceBody::Exchange { + sender, receiver, .. + }, + .. + } = p + .elements + .get_mut(&occurrence) + .unwrap() + .shared_mut() + .unwrap() + { + *sender = Some(from); + *receiver = Some(to); + } +} + +#[test] +fn nested_calls_expose_both_bars_and_end_at_their_exact_replies_in_both_directions() { + for layer in [Layer::Oa, Layer::Sa] { + for reverse in [false, true] { + let mut f = fixture(layer, true); + if reverse { + for occurrence in [f.call, f.inner] { + endpoints(&mut f.project, occurrence, f.callee, f.caller); + } + for occurrence in [f.reply, f.inner_reply] { + endpoints(&mut f.project, occurrence, f.caller, f.callee); + } + } + let scene = scene(&f); + let outer = bar(&scene, f.call); + let inner = bar(&scene, f.inner); + assert!(outer.rect.y < inner.rect.y && outer.bottom() > inner.bottom()); + assert_eq!(outer.column, 0); + assert_eq!(inner.column, 1); + assert!(outer.rect.x < inner.rect.x && outer.edge(true) < inner.edge(true)); + for (call, reply) in [(f.call, f.reply), (f.inner, f.inner_reply)] { + let execution = bar(&scene, call); + let received = *line(&scene, call).points.last().unwrap(); + let returned = line(&scene, reply).points[0]; + assert_eq!(execution.rect.y, received.y); + assert_eq!(execution.bottom(), returned.y); + assert_eq!(received.x, execution.edge(reverse)); + assert_eq!(returned.x, execution.edge(reverse)); + assert_eq!(execution.finish, Finish::Reply(reply)); + } + assert!(scene.open_calls.is_empty()); + } + } +} + +#[test] +fn call_uuid_order_does_not_choose_activation_columns_or_message_anchors() { + let f = fixture(Layer::Oa, false); + let g = fixture(Layer::Oa, true); + let a = scene(&f); + let b = scene(&g); + for (x, y) in [ + (f.call, g.call), + (f.inner, g.inner), + (f.reply, g.reply), + (f.inner_reply, g.inner_reply), + ] { + assert_eq!(line(&a, x).points, line(&b, y).points); + } + for (x, y) in [(f.call, g.call), (f.inner, g.inner)] { + assert_eq!(bar(&a, x).rect, bar(&b, y).rect); + } +} + +#[test] +fn sequential_calls_reuse_column_and_crossed_replies_keep_their_correlations() { + for layer in [Layer::Oa, Layer::Sa] { + let mut f = fixture(layer, true); + *f.project + .elements + .get_mut(&f.owner) + .unwrap() + .steps_mut() + .unwrap() = vec![f.call, f.reply, f.inner, f.inner_reply]; + let sequential = scene(&f); + assert_eq!( + bar(&sequential, f.call).rect.x, + bar(&sequential, f.inner).rect.x + ); + *f.project + .elements + .get_mut(&f.owner) + .unwrap() + .steps_mut() + .unwrap() = vec![f.call, f.inner, f.reply, f.inner_reply]; + let crossed = scene(&f); + assert!(bar(&crossed, f.call).bottom() < bar(&crossed, f.inner).bottom()); + assert!(bar(&crossed, f.call).rect.x < bar(&crossed, f.inner).rect.x); + assert_eq!( + line(&crossed, f.reply).points[0].x, + bar(&crossed, f.call).edge(false) + ); + assert_eq!( + line(&crossed, f.inner_reply).points[0].x, + bar(&crossed, f.inner).edge(false) + ); + } +} + +#[test] +fn self_call_receives_at_lower_loop_end_and_self_reply_returns_to_the_parent() { + for layer in [Layer::Oa, Layer::Sa] { + let mut f = fixture(layer, true); + endpoints(&mut f.project, f.inner, f.callee, f.callee); + endpoints(&mut f.project, f.inner_reply, f.callee, f.callee); + let scene = scene(&f); + let parent = bar(&scene, f.call); + let child = bar(&scene, f.inner); + let call = line(&scene, f.inner); + let reply = line(&scene, f.inner_reply); + assert_eq!(call.points[0].x, parent.edge(true)); + assert_eq!(call.points[3], Point::new(child.edge(true), child.rect.y)); + assert_eq!(child.rect.y, scene.rows[&f.inner].1 + 24.); + assert_eq!( + reply.points[0], + Point::new(child.edge(true), child.bottom()) + ); + assert_eq!(reply.points[3].x, parent.edge(true)); + assert!(call.points[1].x > child.edge(true)); + assert!(reply.points[1].x > child.edge(true)); + } +} + +#[test] +fn an_inactive_self_call_has_no_fabricated_parent_and_three_nested_calls_expose_three_columns() { + let mut f = fixture(Layer::Oa, true); + endpoints(&mut f.project, f.inner, f.callee, f.callee); + endpoints(&mut f.project, f.inner_reply, f.callee, f.callee); + f.project.elements.remove(&f.call); + f.project.elements.remove(&f.reply); + *f.project + .elements + .get_mut(&f.owner) + .unwrap() + .steps_mut() + .unwrap() = vec![f.inner, f.inner_reply]; + let isolated = scene(&f); + assert_eq!(isolated.activations.len(), 1); + assert_eq!(bar(&isolated, f.inner).column, 0); + assert_eq!( + line(&isolated, f.inner).points[0].x, + isolated.lanes[&f.callee] + ); + assert_eq!( + line(&isolated, f.inner_reply).points[3].x, + isolated.lanes[&f.callee] + ); + assert_eq!( + bar(&isolated, f.inner).rect.y, + line(&isolated, f.inner).points[3].y + ); + + let mut f = fixture(Layer::Oa, true); + let third = message( + &mut f.project, + Layer::Oa, + 20, + f.owner, + (f.caller, f.callee), + None, + ); + let third_reply = message( + &mut f.project, + Layer::Oa, + 21, + f.owner, + (f.callee, f.caller), + Some(third), + ); + *f.project + .elements + .get_mut(&f.owner) + .unwrap() + .steps_mut() + .unwrap() = vec![f.call, f.inner, third, third_reply, f.inner_reply, f.reply]; + let scene = scene(&f); + let calls: Vec<_> = scene.activations.iter().map(|a| a.call).collect(); + assert_eq!( + calls, + vec![f.call, f.inner, third], + "Paint order is shallow to deep" + ); + for (column, call) in [f.call, f.inner, third].into_iter().enumerate() { + let execution = bar(&scene, call); + assert_eq!(execution.column, column); + let y = (bar(&scene, third).rect.y + bar(&scene, third).bottom()) / 2.; + assert_eq!( + scene.hit(Point::new(execution.rect.x + OFFSET / 2., y)), + Some(call) + ); + } +} + +#[test] +fn reentrant_call_uses_the_other_receiver_without_inventing_a_caller_execution() { + let mut f = fixture(Layer::Oa, true); + endpoints(&mut f.project, f.inner, f.callee, f.caller); + endpoints(&mut f.project, f.inner_reply, f.caller, f.callee); + let scene = scene(&f); + assert_eq!(bar(&scene, f.call).column, 0); + assert_eq!(bar(&scene, f.inner).column, 0); + assert_eq!(bar(&scene, f.inner).receiver, f.caller); + assert_eq!(line(&scene, f.call).points[0].x, scene.lanes[&f.caller]); + assert_eq!( + line(&scene, f.inner).points[0].x, + bar(&scene, f.call).edge(false) + ); +} + +#[test] +fn actual_canvas_pointer_selects_each_exposed_shoulder_under_zoom_and_pan() { + let f = fixture(Layer::Oa, true); + let mut scene = scene(&f); + let context = scenario::direct_tests::context(f.owner); + scene.interaction = Some((context, scenario::CanvasTool::Select, true)); + let y = (bar(&scene, f.inner).rect.y + bar(&scene, f.inner).bottom()) / 2.; + let outer = Point::new(bar(&scene, f.call).rect.x + OFFSET / 2., y); + let inner = Point::new(bar(&scene, f.inner).edge(true) - OFFSET / 2., y); + for scale in [1., 0.25, 0.05] { + scene.scale = scale; + scene.camera_pan = iced::Vector::new(50., 25.); + let bounds = Rectangle::new(Point::new(30., 40.), Size::new(1000., 700.)); + for (point, call) in [(outer, f.call), (inner, f.inner)] { + assert_eq!(scene.hit(point), Some(call)); + let cursor = bounds.position() + + scene.camera_pan + + iced::Vector::new(point.x * scale, point.y * scale); + let mut state = SequenceInput::default(); + let _ = canvas::Program::update( + &scene, + &mut state, + &canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), + bounds, + mouse::Cursor::Available(cursor), + ); + let action = canvas::Program::update( + &scene, + &mut state, + &canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)), + bounds, + mouse::Cursor::Available(cursor), + ) + .unwrap(); + assert!(matches!(action.into_inner().0, + Some(Message::ScenarioCanvas(scenario::CanvasEvent::Select(_, selected))) if selected == call)); + } + } +} + +#[test] +fn missing_reply_is_a_short_explicit_open_span_and_an_uncorrelated_reply_closes_nothing() { + let mut f = fixture(Layer::Oa, true); + if let SharedBody::Occurrence { + body: OccurrenceBody::Exchange { reply_to, .. }, + .. + } = f + .project + .elements + .get_mut(&f.reply) + .unwrap() + .shared_mut() + .unwrap() + { + *reply_to = None; + } + let scene = scene(&f); + assert_eq!(bar(&scene, f.call).finish, Finish::Open); + assert_eq!(bar(&scene, f.call).rect.height, 26.); + assert_eq!(scene.open_calls.len(), 1); + assert_eq!( + bar(&scene, f.inner).column, + 0, + "Unknown duration does not prove later nesting" + ); + assert_eq!(bar(&scene, f.inner).finish, Finish::Reply(f.inner_reply)); +} + +#[test] +fn alternative_and_parallel_operand_local_calls_do_not_nest_from_screen_order() { + for layer in [Layer::Oa, Layer::Sa] { + for operator in [FragmentOperator::Alternative, FragmentOperator::Parallel] { + let mut f = fixture(layer, true); + let fragment = seed(&mut f.project, layer, 70, Kind::Occurrence, Some(f.owner)); + let branches = [id(71), id(72)]; + *f.project + .elements + .get_mut(&fragment) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::Occurrence { + body: OccurrenceBody::Fragment { + operator, + branches: branches.to_vec(), + }, + precondition: String::new(), + postcondition: String::new(), + }; + for (index, occurrences) in [vec![f.call, f.reply], vec![f.inner, f.inner_reply]] + .into_iter() + .enumerate() + { + let branch = seed( + &mut f.project, + layer, + 71 + index as u128, + Kind::Occurrence, + Some(fragment), + ); + for occurrence in &occurrences { + f.project.elements.get_mut(occurrence).unwrap().parent = Some(branch); + } + *f.project + .elements + .get_mut(&branch) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::Occurrence { + body: OccurrenceBody::Branch { + guard: format!("condition_{index}"), + occurrences, + }, + precondition: String::new(), + postcondition: String::new(), + }; + } + *f.project + .elements + .get_mut(&f.owner) + .unwrap() + .steps_mut() + .unwrap() = vec![fragment]; + let scene = scene(&f); + assert_eq!(bar(&scene, f.call).column, 0); + assert_eq!(bar(&scene, f.inner).column, 0); + assert_ne!(bar(&scene, f.call).scope, bar(&scene, f.inner).scope); + assert!(scene.open_calls.is_empty()); + } + } +} + +#[test] +fn inherited_execution_anchors_a_self_call_inside_an_operand_before_returning_after_the_fragment() { + let mut f = fixture(Layer::Oa, true); + endpoints(&mut f.project, f.inner, f.callee, f.callee); + endpoints(&mut f.project, f.inner_reply, f.callee, f.callee); + let fragment = seed( + &mut f.project, + Layer::Oa, + 70, + Kind::Occurrence, + Some(f.owner), + ); + *f.project + .elements + .get_mut(&fragment) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::Occurrence { + body: OccurrenceBody::Fragment { + operator: FragmentOperator::Alternative, + branches: vec![id(71), id(72)], + }, + precondition: String::new(), + postcondition: String::new(), + }; + for (index, occurrences) in [vec![f.inner, f.inner_reply], vec![]] + .into_iter() + .enumerate() + { + let branch = seed( + &mut f.project, + Layer::Oa, + 71 + index as u128, + Kind::Occurrence, + Some(fragment), + ); + for occurrence in &occurrences { + f.project.elements.get_mut(occurrence).unwrap().parent = Some(branch); + } + *f.project + .elements + .get_mut(&branch) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::Occurrence { + body: OccurrenceBody::Branch { + guard: format!("condition_{index}"), + occurrences, + }, + precondition: String::new(), + postcondition: String::new(), + }; + } + *f.project + .elements + .get_mut(&f.owner) + .unwrap() + .steps_mut() + .unwrap() = vec![f.call, fragment, f.reply]; + let scene = scene(&f); + let parent = scene + .activations + .iter() + .find(|a| a.call == f.call && a.scope == id(71)) + .unwrap(); + assert_eq!(parent.finish, Finish::Continuation); + assert_eq!(bar(&scene, f.inner).column, parent.column + 1); + assert_eq!(line(&scene, f.inner).points[0].x, parent.edge(true)); + assert_eq!(line(&scene, f.inner_reply).points[3].x, parent.edge(true)); + assert!( + scene + .activations + .iter() + .any(|a| a.call == f.call && a.finish == Finish::Reply(f.reply)) + ); +} + +#[test] +fn a_fragment_cannot_make_crossed_calls_an_unambiguous_caller_execution() { + for placement in ["plain", "before_fragment", "in_operand"] { + let mut f = fixture(Layer::Oa, true); + let outgoing = message( + &mut f.project, + Layer::Oa, + 80, + f.owner, + (f.callee, f.caller), + None, + ); + if let SharedBody::Occurrence { + body: OccurrenceBody::Exchange { message_kind, .. }, + .. + } = f + .project + .elements + .get_mut(&outgoing) + .unwrap() + .shared_mut() + .unwrap() + { + *message_kind = MessageKind::Asynchronous; + } + let mut ordered = vec![f.call, f.inner]; + if placement != "in_operand" { + ordered.push(outgoing); + } + if placement != "plain" { + let fragment = seed( + &mut f.project, + Layer::Oa, + 70, + Kind::Occurrence, + Some(f.owner), + ); + *f.project + .elements + .get_mut(&fragment) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::Occurrence { + body: OccurrenceBody::Fragment { + operator: FragmentOperator::Alternative, + branches: vec![id(71), id(72)], + }, + precondition: String::new(), + postcondition: String::new(), + }; + for index in 0..2u128 { + let branch = seed( + &mut f.project, + Layer::Oa, + 71 + index, + Kind::Occurrence, + Some(fragment), + ); + let occurrences = if index == 0 && placement == "in_operand" { + f.project.elements.get_mut(&outgoing).unwrap().parent = Some(branch); + vec![outgoing] + } else { + vec![] + }; + *f.project + .elements + .get_mut(&branch) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::Occurrence { + body: OccurrenceBody::Branch { + guard: format!("condition_{index}"), + occurrences, + }, + precondition: String::new(), + postcondition: String::new(), + }; + } + ordered.push(fragment); + } + ordered.extend([f.reply, f.inner_reply]); + *f.project + .elements + .get_mut(&f.owner) + .unwrap() + .steps_mut() + .unwrap() = ordered; + let scene = scene(&f); + assert_eq!( + line(&scene, outgoing).points[0].x, + scene.lanes[&f.callee], + "{placement}" + ); + } +} + +#[test] +fn nested_correlations_returning_in_alternative_operands_remain_nested_before_the_fragment() { + let mut f = fixture(Layer::Oa, true); + let outgoing = message( + &mut f.project, + Layer::Oa, + 80, + f.owner, + (f.callee, f.caller), + None, + ); + if let SharedBody::Occurrence { + body: OccurrenceBody::Exchange { message_kind, .. }, + .. + } = f + .project + .elements + .get_mut(&outgoing) + .unwrap() + .shared_mut() + .unwrap() + { + *message_kind = MessageKind::Asynchronous; + } + let fragment = seed( + &mut f.project, + Layer::Oa, + 70, + Kind::Occurrence, + Some(f.owner), + ); + *f.project + .elements + .get_mut(&fragment) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::Occurrence { + body: OccurrenceBody::Fragment { + operator: FragmentOperator::Alternative, + branches: vec![id(71), id(72)], + }, + precondition: String::new(), + postcondition: String::new(), + }; + for index in 0..2u128 { + let branch = seed( + &mut f.project, + Layer::Oa, + 71 + index, + Kind::Occurrence, + Some(fragment), + ); + let occurrences = if index == 0 { + for reply in [f.inner_reply, f.reply] { + f.project.elements.get_mut(&reply).unwrap().parent = Some(branch); + } + vec![f.inner_reply, f.reply] + } else { + vec![ + message( + &mut f.project, + Layer::Oa, + 90, + branch, + (f.callee, f.caller), + Some(f.inner), + ), + message( + &mut f.project, + Layer::Oa, + 91, + branch, + (f.callee, f.caller), + Some(f.call), + ), + ] + }; + *f.project + .elements + .get_mut(&branch) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::Occurrence { + body: OccurrenceBody::Branch { + guard: format!("condition_{index}"), + occurrences, + }, + precondition: String::new(), + postcondition: String::new(), + }; + } + *f.project + .elements + .get_mut(&f.owner) + .unwrap() + .steps_mut() + .unwrap() = vec![f.call, f.inner, outgoing, fragment]; + let scene = scene(&f); + let inner = scene + .activations + .iter() + .find(|a| a.call == f.inner && a.scope == f.owner) + .unwrap(); + assert_eq!(line(&scene, outgoing).points[0].x, inner.edge(false)); +}