Integrate lifecycle rendering with typed scenario activations
Reuse published lifecycle bridge and renderer checks from peer checkpoint 7aa007b. Preserve logical correlation independently from clipped paint spans, terminate executions on receiver death, and retain conservative sibling uncertainty without resurrection.
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
//! Scene bridge for local lifetime geometry; no authoring or model mutations.
|
||||
use super::{Geometry, LaneLifetime, Life, Marker, MarkerKind, Span, project_lifetimes};
|
||||
use crate::{behavior::Line, style, views::short};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Size,
|
||||
widget::canvas::{self, Frame, Path, Stroke, Text},
|
||||
};
|
||||
use oa_core::{Id, OccurrenceBody, Project, SharedBody};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// Test-only geometry input, deliberately upstream of rows, scopes, lifecycle
|
||||
// and activations. This is not an authored placement or drag policy.
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
static TEST_ROW_OFFSETS: std::cell::RefCell<BTreeMap<Id, f32>> = Default::default();
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(in crate::behavior) fn apply_test_row_offset(id: Id, y: &mut f32) {
|
||||
TEST_ROW_OFFSETS.with(|offsets| {
|
||||
if let Some(offset) = offsets.borrow().get(&id) {
|
||||
*y += offset;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct EventMarker {
|
||||
event: Marker,
|
||||
lifeline: Id,
|
||||
center: Point,
|
||||
capsule: Rectangle,
|
||||
name: String,
|
||||
sender_x: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub(in crate::behavior) struct Rendering {
|
||||
// Complete rendering positions, including warning-admitted unassigned lanes.
|
||||
// Scene.lanes independently retains the existing authoring eligibility rule.
|
||||
lanes: BTreeMap<Id, f32>,
|
||||
span: Option<Span>,
|
||||
lifetimes: Vec<LaneLifetime>,
|
||||
markers: Vec<EventMarker>,
|
||||
}
|
||||
|
||||
impl Rendering {
|
||||
pub(in crate::behavior) fn new(
|
||||
project: &Project,
|
||||
owner: Id,
|
||||
lanes: &BTreeMap<Id, f32>,
|
||||
rows: &BTreeMap<Id, (Id, f32)>,
|
||||
insertion_scopes: &BTreeMap<Id, (f32, f32)>,
|
||||
height: f32,
|
||||
) -> Self {
|
||||
let span = Span {
|
||||
start: 85.,
|
||||
end: height - 20.,
|
||||
};
|
||||
if !span.valid() {
|
||||
return Self::default();
|
||||
}
|
||||
// Insertion scopes deliberately start later and end earlier. Empty and
|
||||
// Create-only scenarios still have the full visible lifetime extent.
|
||||
let mut scopes = insertion_scopes.clone();
|
||||
for (scope, (start, end)) in &mut scopes {
|
||||
if *scope != owner && *start == *end && start.is_finite() {
|
||||
// An empty operand has 42 px between its insertion axis and
|
||||
// frame end. Match the existing 26 px open-activation allowance.
|
||||
*end = *start + 26.;
|
||||
}
|
||||
}
|
||||
scopes.insert(owner, (span.start, span.end));
|
||||
let lifetimes = project_lifetimes(
|
||||
project,
|
||||
owner,
|
||||
&Geometry {
|
||||
rows,
|
||||
scopes: &scopes,
|
||||
},
|
||||
);
|
||||
let lanes: BTreeMap<_, _> = lanes
|
||||
.iter()
|
||||
.filter(|(_, x)| x.is_finite())
|
||||
.map(|(id, x)| (*id, *x))
|
||||
.collect();
|
||||
let mut markers = Vec::new();
|
||||
for lane in &lifetimes {
|
||||
let Some(&x) = lanes.get(&lane.lifeline) else {
|
||||
continue;
|
||||
};
|
||||
for event in &lane.markers {
|
||||
let sender_x = project
|
||||
.elements
|
||||
.get(&event.occurrence)
|
||||
.and_then(|e| e.shared())
|
||||
.and_then(|body| match body {
|
||||
SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
sender: Some(sender),
|
||||
..
|
||||
},
|
||||
..
|
||||
} => lanes.get(sender).copied(),
|
||||
_ => None,
|
||||
});
|
||||
markers.push(EventMarker {
|
||||
event: *event,
|
||||
lifeline: lane.lifeline,
|
||||
center: Point::new(x, event.y),
|
||||
capsule: Rectangle::new(
|
||||
Point::new(x - 70., event.y - 14.),
|
||||
Size::new(140., 28.),
|
||||
),
|
||||
name: project.named(lane.lifeline).to_owned(),
|
||||
sender_x,
|
||||
});
|
||||
}
|
||||
}
|
||||
Self {
|
||||
lanes,
|
||||
span: Some(span),
|
||||
lifetimes,
|
||||
markers,
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn align_messages(&self, lines: &mut [Line]) {
|
||||
for marker in &self.markers {
|
||||
let Some(sender) = marker.sender_x else {
|
||||
continue;
|
||||
};
|
||||
let Some(line) = lines
|
||||
.iter_mut()
|
||||
.find(|line| line.id == marker.event.occurrence)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let x = marker.center.x;
|
||||
let y = marker.center.y;
|
||||
let self_message = sender == x;
|
||||
let end_x = match marker.event.kind {
|
||||
MarkerKind::Birth if sender < x => marker.capsule.x,
|
||||
MarkerKind::Birth => marker.capsule.x + marker.capsule.width,
|
||||
MarkerKind::Death => x,
|
||||
};
|
||||
line.points = if self_message {
|
||||
// The receiver is on the recorded occurrence row, including self
|
||||
// lifecycle events. The elbow stays outside the birth capsule.
|
||||
let elbow = x + 112.;
|
||||
line.label_at = Point::new(x + 12., y - 52.);
|
||||
vec![
|
||||
Point::new(x, y - 24.),
|
||||
Point::new(elbow, y - 24.),
|
||||
Point::new(elbow, y),
|
||||
Point::new(end_x, y),
|
||||
]
|
||||
} else {
|
||||
if marker.event.kind == MarkerKind::Birth {
|
||||
// Keep the label above the capsule, including right-to-left
|
||||
// messages whose existing label starts beside the receiver.
|
||||
line.label_at.y = y - 40.;
|
||||
}
|
||||
vec![Point::new(sender, y), Point::new(end_x, y)]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn alive_spans(
|
||||
&self,
|
||||
lifeline: Id,
|
||||
scope: Id,
|
||||
top: f32,
|
||||
bottom: f32,
|
||||
) -> Vec<Span> {
|
||||
if !top.is_finite() || !bottom.is_finite() || bottom <= top {
|
||||
return vec![];
|
||||
}
|
||||
self.lifetimes
|
||||
.iter()
|
||||
.find(|lane| lane.lifeline == lifeline)
|
||||
.into_iter()
|
||||
.flat_map(|lane| &lane.intervals)
|
||||
.filter(|interval| interval.scope == scope && interval.life == Life::Alive)
|
||||
.filter_map(|interval| {
|
||||
let start = top.max(interval.span.start);
|
||||
let end = bottom.min(interval.span.end);
|
||||
(start < end).then_some(Span { start, end })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn marker_at(&self, point: Point) -> Option<Id> {
|
||||
self.markers
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|marker| match marker.event.kind {
|
||||
MarkerKind::Birth => marker.capsule.contains(point),
|
||||
MarkerKind::Death => marker_distance(marker, point) <= 1.25,
|
||||
})
|
||||
.map(|marker| marker.event.occurrence)
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn nearest_marker(&self, point: Point) -> Option<(Id, f32)> {
|
||||
if !point.x.is_finite() || !point.y.is_finite() {
|
||||
return None;
|
||||
}
|
||||
// Exact visible shapes are tested first by Scene. A low-zoom tolerance
|
||||
// must not outrank another marker's actual capsule, a header, or a label.
|
||||
self.markers
|
||||
.iter()
|
||||
.map(|marker| (marker.event.occurrence, marker_distance(marker, point)))
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0)))
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn lane_at(&self, point: Point, scale: f32) -> Option<Id> {
|
||||
let span = self.span?;
|
||||
if !scale.is_finite() || scale <= 0. || point.y < span.start || point.y > span.end {
|
||||
return None;
|
||||
}
|
||||
self.lanes
|
||||
.iter()
|
||||
.filter(|(_, x)| (**x - point.x).abs() < 12. / scale)
|
||||
.min_by(|a, b| (a.1 - point.x).abs().total_cmp(&(b.1 - point.x).abs()))
|
||||
.map(|(id, _)| *id)
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn draw_lanes(&self, frame: &mut Frame, selected: Id) {
|
||||
let Some(span) = self.span else {
|
||||
return;
|
||||
};
|
||||
for (&id, &x) in &self.lanes {
|
||||
frame.stroke(
|
||||
&Path::line(Point::new(x, span.start), Point::new(x, span.end)),
|
||||
Stroke {
|
||||
line_dash: canvas::LineDash {
|
||||
segments: &[2., 5.],
|
||||
offset: 0,
|
||||
},
|
||||
..Stroke::default()
|
||||
.with_width(1.)
|
||||
.with_color(if id == selected {
|
||||
Color::from_rgb8(148, 190, 187)
|
||||
} else {
|
||||
Color::from_rgb8(214, 222, 222)
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
for lifetime in &self.lifetimes {
|
||||
let Some(&x) = self.lanes.get(&lifetime.lifeline) else {
|
||||
continue;
|
||||
};
|
||||
for interval in &lifetime.intervals {
|
||||
match interval.life {
|
||||
Life::Alive => frame.stroke(
|
||||
&Path::line(
|
||||
Point::new(x, interval.span.start),
|
||||
Point::new(x, interval.span.end),
|
||||
),
|
||||
Stroke::default().with_width(2.).with_color(
|
||||
if lifetime.lifeline == selected {
|
||||
style::TEAL
|
||||
} else {
|
||||
Color::from_rgb8(91, 113, 113)
|
||||
},
|
||||
),
|
||||
),
|
||||
Life::Unresolved if interval.span.end - interval.span.start >= 16. => {
|
||||
frame.fill_text(Text {
|
||||
content: "?".into(),
|
||||
position: Point::new(x + 9., interval.span.start + 2.),
|
||||
size: 11.into(),
|
||||
color: style::MUTED,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !self.lanes.is_empty() {
|
||||
frame.fill_text(Text {
|
||||
content:
|
||||
"Dotted: editing guide · Solid: local lifetime · ?: lifetime depends on path"
|
||||
.into(),
|
||||
position: Point::new(28., span.end + 4.),
|
||||
size: 10.into(),
|
||||
color: style::MUTED,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::behavior) fn draw_markers(
|
||||
&self,
|
||||
frame: &mut Frame,
|
||||
selected: Id,
|
||||
scale: f32,
|
||||
pan: iced::Vector,
|
||||
) {
|
||||
for marker in &self.markers {
|
||||
let color = if selected == marker.event.occurrence || selected == marker.lifeline {
|
||||
style::TEAL
|
||||
} else {
|
||||
style::INK
|
||||
};
|
||||
match marker.event.kind {
|
||||
MarkerKind::Birth => {
|
||||
let path = Path::rounded_rectangle(
|
||||
marker.capsule.position(),
|
||||
marker.capsule.size(),
|
||||
6.into(),
|
||||
);
|
||||
frame.fill(&path, Color::from_rgb8(243, 248, 248));
|
||||
frame.stroke(
|
||||
&path,
|
||||
Stroke::default().with_color(color).with_width(
|
||||
if selected == marker.event.occurrence {
|
||||
2.5
|
||||
} else {
|
||||
1.5
|
||||
},
|
||||
),
|
||||
);
|
||||
if let Some(clip) = birth_label_clip(pan, marker.capsule, scale, frame.size()) {
|
||||
// WGPU clipped frames start with an identity transform;
|
||||
// neither the parent's pan/scale nor its clip is inherited.
|
||||
frame.with_clip(clip, |label| {
|
||||
label.translate(pan);
|
||||
label.scale(scale);
|
||||
label.fill_text(Text {
|
||||
content: short(&marker.name, 18),
|
||||
position: marker.capsule.position() + iced::Vector::new(8., 6.),
|
||||
size: 12.into(),
|
||||
color,
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
MarkerKind::Death => {
|
||||
for (a, b) in death_strokes(marker.center) {
|
||||
frame.stroke(
|
||||
&Path::line(a, b),
|
||||
Stroke::default().with_color(color).with_width(2.5),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn birth_label_clip(
|
||||
pan: iced::Vector,
|
||||
capsule: Rectangle,
|
||||
scale: f32,
|
||||
viewport: Size,
|
||||
) -> Option<Rectangle> {
|
||||
if !pan.x.is_finite() || !pan.y.is_finite() || !scale.is_finite() || scale <= 0. {
|
||||
return None;
|
||||
}
|
||||
Rectangle::new(
|
||||
Point::new(
|
||||
pan.x + (capsule.x + 8.) * scale,
|
||||
pan.y + (capsule.y + 4.) * scale,
|
||||
),
|
||||
Size::new((capsule.width - 16.) * scale, (capsule.height - 8.) * scale),
|
||||
)
|
||||
.intersection(&Rectangle::new(Point::ORIGIN, viewport))
|
||||
}
|
||||
|
||||
fn marker_distance(marker: &EventMarker, point: Point) -> f32 {
|
||||
match marker.event.kind {
|
||||
MarkerKind::Birth => {
|
||||
let dx = (marker.capsule.x - point.x)
|
||||
.max(point.x - marker.capsule.x - marker.capsule.width)
|
||||
.max(0.);
|
||||
let dy = (marker.capsule.y - point.y)
|
||||
.max(point.y - marker.capsule.y - marker.capsule.height)
|
||||
.max(0.);
|
||||
dx.hypot(dy)
|
||||
}
|
||||
MarkerKind::Death => death_strokes(marker.center)
|
||||
.iter()
|
||||
.map(|(a, b)| crate::diagram::distance_to_segment(point, *a, *b))
|
||||
.fold(f32::INFINITY, f32::min),
|
||||
}
|
||||
}
|
||||
|
||||
fn death_strokes(center: Point) -> [(Point, Point); 2] {
|
||||
[
|
||||
(
|
||||
center + iced::Vector::new(-7., -7.),
|
||||
center + iced::Vector::new(7., 7.),
|
||||
),
|
||||
(
|
||||
center + iced::Vector::new(-7., 7.),
|
||||
center + iced::Vector::new(7., -7.),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "render_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,991 @@
|
||||
use super::*;
|
||||
use crate::{behavior::Scene, document::Document};
|
||||
use iced::Size;
|
||||
use oa_core::*;
|
||||
|
||||
#[path = "union_tests.rs"]
|
||||
mod union_tests;
|
||||
|
||||
struct Fixture {
|
||||
project: Project,
|
||||
owner: Id,
|
||||
sender: Id,
|
||||
receiver: Id,
|
||||
work: Id,
|
||||
exchange: Id,
|
||||
}
|
||||
impl Fixture {
|
||||
fn new(layer: Layer) -> Self {
|
||||
let mut project = Project::empty();
|
||||
let kind = if layer == Layer::Oa {
|
||||
Kind::Activity
|
||||
} else {
|
||||
Kind::Function
|
||||
};
|
||||
let a = project.add_element(kind, layer, "Request", None).unwrap();
|
||||
let work = project.add_element(kind, layer, "Provide", None).unwrap();
|
||||
let exchange = project.relate(
|
||||
if layer == Layer::Oa {
|
||||
RelationKind::Interaction
|
||||
} else {
|
||||
RelationKind::FunctionalExchange
|
||||
},
|
||||
"Service",
|
||||
a,
|
||||
work,
|
||||
);
|
||||
let owner = project
|
||||
.add_element(Kind::Scenario, layer, "Lifecycle", None)
|
||||
.unwrap();
|
||||
let mut lane = |name, represented| {
|
||||
let id = project
|
||||
.add_element(Kind::Lifeline, layer, name, Some(owner))
|
||||
.unwrap();
|
||||
*project.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
||||
represented: Some(represented),
|
||||
};
|
||||
id
|
||||
};
|
||||
let sender = lane("Requester", a);
|
||||
let receiver = lane("Provider", work);
|
||||
Self {
|
||||
project,
|
||||
owner,
|
||||
sender,
|
||||
receiver,
|
||||
work,
|
||||
exchange,
|
||||
}
|
||||
}
|
||||
fn occurrence(&mut self, parent: Id, name: &str, body: OccurrenceBody) -> Id {
|
||||
let mut element = self
|
||||
.project
|
||||
.make_element(
|
||||
Kind::Occurrence,
|
||||
self.project.elements[&parent].layer(),
|
||||
name,
|
||||
Some(parent),
|
||||
)
|
||||
.unwrap();
|
||||
let id = element.id;
|
||||
*element.shared_mut().unwrap() = SharedBody::Occurrence {
|
||||
body,
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
};
|
||||
self.project.elements.insert(id, element);
|
||||
let parent = self.project.elements.get_mut(&parent).unwrap();
|
||||
if let Some(steps) = parent.steps_mut() {
|
||||
steps.push(id);
|
||||
} else if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Fragment { branches, .. },
|
||||
..
|
||||
}) = parent.shared_mut()
|
||||
{
|
||||
branches.push(id);
|
||||
} else {
|
||||
panic!("fixture parent must own ordered occurrences or operands");
|
||||
}
|
||||
id
|
||||
}
|
||||
fn message(&mut self, parent: Id, kind: MessageKind) -> Id {
|
||||
self.occurrence(
|
||||
parent,
|
||||
"Message",
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(self.exchange),
|
||||
sender: Some(self.sender),
|
||||
receiver: Some(self.receiver),
|
||||
message_kind: kind,
|
||||
operation: None,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
},
|
||||
)
|
||||
}
|
||||
fn reply(&mut self, parent: Id, call: Id) -> Id {
|
||||
self.occurrence(
|
||||
parent,
|
||||
"Reply",
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(self.exchange),
|
||||
sender: Some(self.receiver),
|
||||
receiver: Some(self.sender),
|
||||
message_kind: MessageKind::Reply,
|
||||
operation: None,
|
||||
reply_to: Some(call),
|
||||
values: vec![],
|
||||
},
|
||||
)
|
||||
}
|
||||
fn fragment(&mut self, parent: Id, operator: FragmentOperator) -> Id {
|
||||
self.occurrence(
|
||||
parent,
|
||||
"Paths",
|
||||
OccurrenceBody::Fragment {
|
||||
operator,
|
||||
branches: vec![],
|
||||
},
|
||||
)
|
||||
}
|
||||
fn branch(&mut self, fragment: Id) -> Id {
|
||||
self.occurrence(
|
||||
fragment,
|
||||
"Operand",
|
||||
OccurrenceBody::Branch {
|
||||
guard: "condition".into(),
|
||||
occurrences: vec![],
|
||||
},
|
||||
)
|
||||
}
|
||||
fn document(&self) -> Document {
|
||||
validate_candidate(&self.project).expect("ordinary core admission must succeed");
|
||||
let mut bundle = source_project::format_bundle(&self.project);
|
||||
for text in bundle.files.values_mut() {
|
||||
*text = format!(
|
||||
"// Retain exact authored CRLF and Unicode Ω\r\n{}",
|
||||
text.replace('\n', "\r\n")
|
||||
);
|
||||
}
|
||||
let document =
|
||||
Document::from_bundle(bundle.clone(), source_project::ProjectLimits::default())
|
||||
.unwrap();
|
||||
assert_eq!(document.project(), &self.project);
|
||||
assert_eq!(document.bundle(), &bundle);
|
||||
document
|
||||
}
|
||||
fn scene(&self) -> Scene {
|
||||
let document = self.document();
|
||||
let before = document.snapshot();
|
||||
let scene = Scene::sequence(document.project(), self.owner, self.owner);
|
||||
assert_eq!(document.project(), &before.decoded.project);
|
||||
assert_eq!(document.bundle(), &before.bundle);
|
||||
assert_eq!(document.revision(), 0);
|
||||
assert!(!document.can_undo() && !document.can_redo() && !document.dirty());
|
||||
assert_eq!(
|
||||
scene.lifecycle,
|
||||
Scene::sequence(document.project(), self.owner, self.owner).lifecycle
|
||||
);
|
||||
assert!(
|
||||
scene
|
||||
.lines
|
||||
.iter()
|
||||
.flat_map(|line| &line.points)
|
||||
.all(|p| p.x.is_finite() && p.y.is_finite())
|
||||
);
|
||||
scene
|
||||
}
|
||||
}
|
||||
|
||||
fn intervals(scene: &Scene, lifeline: Id, scope: Id) -> Vec<(f32, f32, Life)> {
|
||||
scene
|
||||
.lifecycle
|
||||
.lifetimes
|
||||
.iter()
|
||||
.find(|lane| lane.lifeline == lifeline)
|
||||
.unwrap()
|
||||
.intervals
|
||||
.iter()
|
||||
.filter(|i| i.scope == scope)
|
||||
.map(|i| (i.span.start, i.span.end, i.life))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_long_header_title_and_detail_keep_names_and_have_bounded_clips() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for name in [
|
||||
"W".repeat(18),
|
||||
"界".repeat(24),
|
||||
"e\u{301}".repeat(24),
|
||||
"👩\u{200d}🔧".repeat(12),
|
||||
] {
|
||||
let mut f = Fixture::new(layer);
|
||||
f.project.elements.get_mut(&f.receiver).unwrap().name = name.clone();
|
||||
f.project.elements.get_mut(&f.work).unwrap().name = name.clone();
|
||||
let scene = f.scene();
|
||||
let header = scene.boxes.iter().find(|b| b.id == f.receiver).unwrap();
|
||||
assert_eq!(header.title, name);
|
||||
assert_eq!(header.detail, name);
|
||||
assert_eq!(header.rect.size(), Size::new(204., 70.));
|
||||
assert_eq!(scene.hit(header.rect.center()), Some(f.receiver));
|
||||
// Scroll offset is handled by the existing outer canvas/scrollable;
|
||||
// these are the local drawing frame's coordinates, not screen input.
|
||||
for scale in [0.05, 0.5, 0.512, 0.64, 0.8, 1., 1.953125, 2.] {
|
||||
let clip = crate::behavior::box_text_clip(
|
||||
iced::Vector::ZERO,
|
||||
header.rect,
|
||||
scale,
|
||||
Size::new(2000., 2000.),
|
||||
)
|
||||
.unwrap();
|
||||
let world = Rectangle::new(
|
||||
Point::new(clip.x / scale, clip.y / scale),
|
||||
Size::new(clip.width / scale, clip.height / scale),
|
||||
);
|
||||
assert!((world.x - header.rect.x - 10.).abs() < 0.001);
|
||||
assert!((world.y - header.rect.y - 8.).abs() < 0.001);
|
||||
assert!((world.width - 184.).abs() < 0.001);
|
||||
assert!((world.height - 54.).abs() < 0.001);
|
||||
assert!(header.rect.contains(world.position()));
|
||||
assert!(
|
||||
header
|
||||
.rect
|
||||
.contains(Point::new(world.x + world.width, world.y + world.height,))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn box_text_clip_intersects_parent_viewport_and_rejects_empty_interiors() {
|
||||
use crate::behavior::box_text_clip;
|
||||
let card = Rectangle::new(Point::new(100., 200.), Size::new(204., 70.));
|
||||
assert_eq!(
|
||||
box_text_clip(iced::Vector::ZERO, card, 2., Size::new(300., 500.)),
|
||||
Some(Rectangle::new(Point::new(220., 416.), Size::new(80., 84.)))
|
||||
);
|
||||
assert_eq!(
|
||||
box_text_clip(
|
||||
iced::Vector::ZERO,
|
||||
Rectangle::new(Point::new(-20., -10.), card.size()),
|
||||
2.,
|
||||
Size::new(300., 500.),
|
||||
),
|
||||
Some(Rectangle::new(Point::ORIGIN, Size::new(300., 104.)))
|
||||
);
|
||||
for viewport in [Size::new(200., 500.), Size::new(300., 400.), Size::ZERO] {
|
||||
assert_eq!(box_text_clip(iced::Vector::ZERO, card, 2., viewport), None);
|
||||
}
|
||||
for scale in [0., -1., f32::NAN, f32::INFINITY] {
|
||||
assert_eq!(
|
||||
box_text_clip(iced::Vector::ZERO, card, scale, Size::new(1000., 1000.)),
|
||||
None
|
||||
);
|
||||
}
|
||||
for size in [Size::new(20., 70.), Size::new(204., 16.), Size::ZERO] {
|
||||
assert_eq!(
|
||||
box_text_clip(
|
||||
iced::Vector::ZERO,
|
||||
Rectangle::new(Point::ORIGIN, size),
|
||||
1.,
|
||||
card.size()
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_wide_latin_header_exceeds_old_character_budget() {
|
||||
use iced::advanced::text::{self, Paragraph as _};
|
||||
let mut f = Fixture::new(Layer::Oa);
|
||||
f.project.elements.get_mut(&f.receiver).unwrap().name = "W".repeat(18);
|
||||
f.project.elements.get_mut(&f.work).unwrap().name = "W".repeat(24);
|
||||
let scene = f.scene();
|
||||
let header = scene.boxes.iter().find(|b| b.id == f.receiver).unwrap();
|
||||
for (content, size, divisor) in [(&header.title, 13., 7.), (&header.detail, 11., 6.)] {
|
||||
let label = iced::widget::canvas::Text {
|
||||
content: crate::views::short(content, ((header.rect.width - 25.) / divisor) as usize),
|
||||
size: size.into(),
|
||||
..Default::default()
|
||||
};
|
||||
// Use the same real paragraph/font/shaping defaults as canvas Text.
|
||||
// The native defect survives short(): glyph width, not scalar count,
|
||||
// is what exceeds the card. This is a font witness, not a pixel gate.
|
||||
let paragraph = <iced::Renderer as text::Renderer>::Paragraph::with_text(text::Text {
|
||||
content: label.content.as_str(),
|
||||
bounds: Size::INFINITE,
|
||||
size: label.size,
|
||||
line_height: label.line_height,
|
||||
font: label.font,
|
||||
align_x: label.align_x,
|
||||
align_y: label.align_y,
|
||||
shaping: label.shaping,
|
||||
wrapping: text::Wrapping::default(),
|
||||
});
|
||||
let old_available = header.rect.width - 12.;
|
||||
assert!(
|
||||
paragraph.min_width() > old_available,
|
||||
"wide glyph fixture must overflow the old unbounded card: {} <= {old_available}",
|
||||
paragraph.min_width()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn birth_label_clip_keeps_padded_world_bounds_at_each_scene_scale() {
|
||||
let capsule = Rectangle::new(Point::new(100., 200.), Size::new(140., 28.));
|
||||
for (scale, expected) in [
|
||||
(
|
||||
0.05,
|
||||
Rectangle::new(Point::new(5.4, 10.2), Size::new(6.2, 1.)),
|
||||
),
|
||||
(
|
||||
0.5,
|
||||
Rectangle::new(Point::new(54., 102.), Size::new(62., 10.)),
|
||||
),
|
||||
(
|
||||
1.,
|
||||
Rectangle::new(Point::new(108., 204.), Size::new(124., 20.)),
|
||||
),
|
||||
(
|
||||
2.,
|
||||
Rectangle::new(Point::new(216., 408.), Size::new(248., 40.)),
|
||||
),
|
||||
] {
|
||||
let clip =
|
||||
birth_label_clip(iced::Vector::ZERO, capsule, scale, Size::new(1000., 1000.)).unwrap();
|
||||
for (actual, expected) in [
|
||||
(clip.x, expected.x),
|
||||
(clip.y, expected.y),
|
||||
(clip.width, expected.width),
|
||||
(clip.height, expected.height),
|
||||
] {
|
||||
assert!((actual - expected).abs() < 0.0001);
|
||||
}
|
||||
assert!(capsule.contains(Point::new(clip.x / scale, clip.y / scale)));
|
||||
assert!(capsule.contains(Point::new(
|
||||
(clip.x + clip.width) / scale,
|
||||
(clip.y + clip.height) / scale,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn birth_label_clip_preserves_parent_viewport_bounds() {
|
||||
let capsule = Rectangle::new(Point::new(100., 200.), Size::new(140., 28.));
|
||||
assert_eq!(
|
||||
birth_label_clip(iced::Vector::ZERO, capsule, 2., Size::new(300., 420.)),
|
||||
Some(Rectangle::new(Point::new(216., 408.), Size::new(84., 12.)))
|
||||
);
|
||||
let crossing_origin = Rectangle::new(Point::new(-20., -10.), Size::new(140., 28.));
|
||||
assert_eq!(
|
||||
birth_label_clip(
|
||||
iced::Vector::ZERO,
|
||||
crossing_origin,
|
||||
2.,
|
||||
Size::new(300., 420.)
|
||||
),
|
||||
Some(Rectangle::new(Point::ORIGIN, Size::new(224., 28.)))
|
||||
);
|
||||
for viewport in [Size::new(200., 420.), Size::new(300., 400.), Size::ZERO] {
|
||||
assert_eq!(
|
||||
birth_label_clip(iced::Vector::ZERO, capsule, 2., viewport),
|
||||
None
|
||||
);
|
||||
}
|
||||
for scale in [0., -1., f32::NAN, f32::INFINITY] {
|
||||
assert_eq!(
|
||||
birth_label_clip(iced::Vector::ZERO, capsule, scale, Size::new(1000., 1000.)),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_empty_and_create_only_use_visible_root_extents_not_insertion_bounds() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let empty = f.scene();
|
||||
assert_eq!(empty.sequence_bounds[&f.owner], (106., 106.));
|
||||
assert_eq!(
|
||||
intervals(&empty, f.receiver, f.owner),
|
||||
vec![(85., empty.size.height - 20., Life::Alive)]
|
||||
);
|
||||
assert!(empty.lines.is_empty(), "lane guides are not message lines");
|
||||
let create = f.message(f.owner, MessageKind::Create);
|
||||
let scene = f.scene();
|
||||
let birth = scene.rows[&create].1;
|
||||
assert_eq!(birth, 148.);
|
||||
assert_eq!(scene.sequence_bounds[&f.owner], (106., 198.));
|
||||
assert_eq!(
|
||||
intervals(&scene, f.receiver, f.owner),
|
||||
vec![
|
||||
(85., birth, Life::Absent),
|
||||
(birth, scene.size.height - 20., Life::Alive),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&scene, f.sender, f.owner),
|
||||
vec![(85., scene.size.height - 20., Life::Alive)]
|
||||
);
|
||||
assert_eq!(scene.slots[0].y, 106.);
|
||||
assert_eq!(
|
||||
scene.lane_at(Point::new(scene.lanes[&f.receiver], 90.)),
|
||||
Some(f.receiver)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_birth_death_markers_align_endpoints_and_win_selection_without_mutating_source() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let create = f.message(f.owner, MessageKind::Create);
|
||||
f.occurrence(
|
||||
f.owner,
|
||||
"Work",
|
||||
OccurrenceBody::Action {
|
||||
action: Some(f.work),
|
||||
lifeline: Some(f.receiver),
|
||||
},
|
||||
);
|
||||
let delete = f.message(f.owner, MessageKind::Delete);
|
||||
let mut scene = f.scene();
|
||||
let birth = scene.rows[&create].1;
|
||||
let death = scene.rows[&delete].1;
|
||||
assert_eq!(
|
||||
intervals(&scene, f.receiver, f.owner),
|
||||
vec![
|
||||
(85., birth, Life::Absent),
|
||||
(birth, death, Life::Alive),
|
||||
(death, scene.size.height - 20., Life::Absent),
|
||||
]
|
||||
);
|
||||
let capsule = scene
|
||||
.lifecycle
|
||||
.markers
|
||||
.iter()
|
||||
.find(|m| m.event.occurrence == create)
|
||||
.unwrap()
|
||||
.capsule;
|
||||
let line = scene.lines.iter().find(|l| l.id == create).unwrap();
|
||||
let expected_x = if scene.lanes[&f.sender] < scene.lanes[&f.receiver] {
|
||||
capsule.x
|
||||
} else {
|
||||
capsule.x + capsule.width
|
||||
};
|
||||
assert_eq!(line.points.last(), Some(&Point::new(expected_x, birth)));
|
||||
assert!(
|
||||
line.label_at.y + 22. < capsule.y,
|
||||
"message caption clears birth capsule"
|
||||
);
|
||||
let line = scene.lines.iter().find(|l| l.id == delete).unwrap();
|
||||
assert_eq!(
|
||||
line.points.last(),
|
||||
Some(&Point::new(scene.lanes[&f.receiver], death))
|
||||
);
|
||||
for scale in [0.5, 1., 2.] {
|
||||
scene.scale = scale;
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(scene.lanes[&f.receiver], birth)),
|
||||
Some(create)
|
||||
);
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(scene.lanes[&f.receiver], death)),
|
||||
Some(delete)
|
||||
);
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(
|
||||
scene.lanes[&f.receiver],
|
||||
scene.size.height - 25.
|
||||
)),
|
||||
Some(f.receiver)
|
||||
);
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(scene.lanes[&f.receiver], 40.)),
|
||||
Some(f.receiver)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_branch_delete_clips_only_its_activation_and_keeps_unanswered_call_visible() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let call = f.message(f.owner, MessageKind::Call);
|
||||
let fragment = f.fragment(f.owner, FragmentOperator::Alternative);
|
||||
let left = f.branch(fragment);
|
||||
let delete = f.message(left, MessageKind::Delete);
|
||||
let right = f.branch(fragment);
|
||||
let scene = f.scene();
|
||||
let death = scene.rows[&delete].1;
|
||||
let (left_start, left_end) = scene.sequence_bounds[&left];
|
||||
let (right_start, _) = scene.sequence_bounds[&right];
|
||||
let bars: Vec<_> = scene
|
||||
.activations
|
||||
.iter()
|
||||
.filter(|b| b.call == call && b.receiver == f.receiver)
|
||||
.map(|b| b.rect)
|
||||
.collect();
|
||||
assert!(
|
||||
bars.iter()
|
||||
.any(|r| r.y == left_start && r.y + r.height == death)
|
||||
);
|
||||
assert!(
|
||||
!bars
|
||||
.iter()
|
||||
.any(|r| r.y < left_end && r.y + r.height > death && r.y >= left_start)
|
||||
);
|
||||
assert!(
|
||||
bars.iter().any(|r| r.y == right_start && r.height == 26.),
|
||||
"empty exclusive sibling retains its own open activation"
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&scene, f.receiver, right),
|
||||
vec![(right_start, right_start + 26., Life::Alive)]
|
||||
);
|
||||
let frame_end = scene.boxes.iter().find(|b| b.id == fragment).unwrap().rect;
|
||||
assert!(
|
||||
!bars.iter().any(|r| r.y >= frame_end.y + frame_end.height),
|
||||
"uncertain post-fragment lifetime cannot carry a definite activation bar"
|
||||
);
|
||||
assert!(
|
||||
scene
|
||||
.open_calls
|
||||
.iter()
|
||||
.any(|p| p.y < death && p.y > left_start)
|
||||
);
|
||||
assert!(scene.open_calls.iter().any(|p| p.y > right_start));
|
||||
assert_eq!(
|
||||
intervals(&scene, f.receiver, left),
|
||||
vec![
|
||||
(left_start, death, Life::Alive),
|
||||
(death, left_end, Life::Absent)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&scene, f.receiver, f.owner).last().unwrap().2,
|
||||
Life::Unresolved
|
||||
);
|
||||
assert!(!f.project.elements.values().any(|e| matches!(
|
||||
e.shared(),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
message_kind: MessageKind::Reply,
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_unassigned_lanes_and_explicit_incomplete_receiver_keep_real_marker_identity() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
*f.project
|
||||
.elements
|
||||
.get_mut(&f.receiver)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Lifeline { represented: None };
|
||||
let create = f.message(f.owner, MessageKind::Create);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { exchange, .. },
|
||||
..
|
||||
}) = f.project.elements.get_mut(&create).unwrap().shared_mut()
|
||||
{
|
||||
*exchange = None;
|
||||
}
|
||||
let scene = f.scene();
|
||||
assert!(
|
||||
!scene.lanes.contains_key(&f.receiver),
|
||||
"authoring eligibility remains separate"
|
||||
);
|
||||
let x = scene.lifecycle.lanes[&f.receiver];
|
||||
assert_eq!(scene.lifecycle.markers[0].lifeline, f.receiver);
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(x, scene.rows[&create].1)),
|
||||
Some(create)
|
||||
);
|
||||
assert_eq!(scene.hit(Point::new(x, 40.)), Some(f.receiver));
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(x, scene.size.height - 25.)),
|
||||
Some(f.receiver)
|
||||
);
|
||||
assert_eq!(scene.lane_at(Point::new(x, 90.)), None);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
receiver, exchange, ..
|
||||
},
|
||||
..
|
||||
}) = f.project.elements.get_mut(&create).unwrap().shared_mut()
|
||||
{
|
||||
*receiver = None;
|
||||
*exchange = Some(f.exchange);
|
||||
}
|
||||
let missing = f.scene();
|
||||
assert!(
|
||||
missing.lifecycle.markers.is_empty(),
|
||||
"do not infer receiver from global exchange"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_minimum_zoom_marker_halos_do_not_steal_adjacent_capsule_or_header_hits() {
|
||||
let mut f = Fixture::new(Layer::Sa);
|
||||
*f.project
|
||||
.elements
|
||||
.get_mut(&f.receiver)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::Lifeline { represented: None };
|
||||
let create = f.message(f.owner, MessageKind::Create);
|
||||
let delete = f.message(f.owner, MessageKind::Delete);
|
||||
for event in [create, delete] {
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { exchange, .. },
|
||||
..
|
||||
}) = f.project.elements.get_mut(&event).unwrap().shared_mut()
|
||||
{
|
||||
*exchange = None;
|
||||
}
|
||||
}
|
||||
let mut scene = f.scene();
|
||||
let x = scene.lifecycle.lanes[&f.receiver];
|
||||
assert_eq!(scene.rows[&delete].1 - scene.rows[&create].1, 92.);
|
||||
for scale in [0.05, 0.1, 0.5, 1., 2.] {
|
||||
scene.scale = scale;
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(x, scene.rows[&create].1)),
|
||||
Some(create)
|
||||
);
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(x, scene.rows[&delete].1)),
|
||||
Some(delete)
|
||||
);
|
||||
assert_eq!(scene.hit(Point::new(x, 40.)), Some(f.receiver));
|
||||
if scale <= 0.1 {
|
||||
for offset in [-25., -15., 15., 25.] {
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(x, scene.rows[&delete].1 + offset)),
|
||||
Some(delete),
|
||||
"a nearby death halo must beat an earlier message's wider tolerance"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_minimum_zoom_near_ordinary_message_wins_over_lifecycle_halos() {
|
||||
let mut f = Fixture::new(Layer::Sa);
|
||||
f.message(f.owner, MessageKind::Create);
|
||||
let ordinary = f.message(f.owner, MessageKind::Asynchronous);
|
||||
f.message(f.owner, MessageKind::Delete);
|
||||
let mut scene = f.scene();
|
||||
let x = (scene.lanes[&f.sender] + scene.lanes[&f.receiver]) / 2.;
|
||||
for scale in [0.05, 0.1] {
|
||||
scene.scale = scale;
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(x, scene.rows[&ordinary].1 + 2.)),
|
||||
Some(ordinary)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_self_lifecycle_elbows_end_on_recorded_marker_rows() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for kind in [MessageKind::Create, MessageKind::Delete] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let event = f.message(f.owner, kind);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
exchange, sender, ..
|
||||
},
|
||||
..
|
||||
}) = f.project.elements.get_mut(&event).unwrap().shared_mut()
|
||||
{
|
||||
*exchange = None;
|
||||
*sender = Some(f.receiver);
|
||||
}
|
||||
let scene = f.scene();
|
||||
let marker = &scene.lifecycle.markers[0];
|
||||
let line = scene.lines.iter().find(|l| l.id == event).unwrap();
|
||||
assert_eq!(line.points.len(), 4);
|
||||
assert_eq!(line.points[0].y, marker.center.y - 24.);
|
||||
assert_eq!(line.points[3].y, marker.center.y);
|
||||
assert_eq!(
|
||||
line.points[3].x,
|
||||
if marker.event.kind == MarkerKind::Birth {
|
||||
marker.capsule.x + marker.capsule.width
|
||||
} else {
|
||||
marker.center.x
|
||||
}
|
||||
);
|
||||
assert!(line.points[1].x > marker.capsule.x + marker.capsule.width);
|
||||
assert_eq!(scene.hit(marker.center), Some(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_self_delete_caption_clears_the_preceding_action_box() {
|
||||
let mut f = Fixture::new(Layer::Sa);
|
||||
let action = f.occurrence(
|
||||
f.owner,
|
||||
"Finish work",
|
||||
OccurrenceBody::Action {
|
||||
action: Some(f.work),
|
||||
lifeline: Some(f.receiver),
|
||||
},
|
||||
);
|
||||
let delete = f.message(f.owner, MessageKind::Delete);
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
exchange, sender, ..
|
||||
},
|
||||
..
|
||||
}) = f.project.elements.get_mut(&delete).unwrap().shared_mut()
|
||||
{
|
||||
*exchange = None;
|
||||
*sender = Some(f.receiver);
|
||||
}
|
||||
let scene = f.scene();
|
||||
let action = scene.boxes.iter().find(|b| b.id == action).unwrap();
|
||||
let line = scene.lines.iter().find(|l| l.id == delete).unwrap();
|
||||
assert!(line.label_at.y > action.rect.y + action.rect.height);
|
||||
assert!(line.label_at.y + 22. < line.points[0].y);
|
||||
assert_eq!(line.points[3].y, scene.rows[&delete].1);
|
||||
assert_eq!(
|
||||
scene
|
||||
.slots
|
||||
.iter()
|
||||
.find(|s| s.slot.before == Some(delete))
|
||||
.unwrap()
|
||||
.y,
|
||||
scene.rows[&delete].1 - 42.
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_conditional_repeated_and_parallel_scopes_do_not_claim_definite_continuation() {
|
||||
for operator in [
|
||||
FragmentOperator::Alternative,
|
||||
FragmentOperator::Optional,
|
||||
FragmentOperator::Loop,
|
||||
FragmentOperator::Parallel,
|
||||
] {
|
||||
let mut f = Fixture::new(Layer::Sa);
|
||||
let fragment = f.fragment(f.owner, operator);
|
||||
let first = f.branch(fragment);
|
||||
let create = f.message(first, MessageKind::Create);
|
||||
let scene = f.scene();
|
||||
assert_eq!(scene.lifecycle.markers[0].event.occurrence, create);
|
||||
assert_eq!(
|
||||
intervals(&scene, f.receiver, f.owner).first().unwrap().2,
|
||||
Life::Unresolved
|
||||
);
|
||||
assert_eq!(
|
||||
intervals(&scene, f.receiver, f.owner).last().unwrap().2,
|
||||
Life::Unresolved
|
||||
);
|
||||
assert!(
|
||||
scene
|
||||
.lifecycle
|
||||
.lifetimes
|
||||
.iter()
|
||||
.find(|l| l.lifeline == f.sender)
|
||||
.unwrap()
|
||||
.intervals
|
||||
.iter()
|
||||
.all(|i| i.life == Life::Alive)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_lifecycle_edit_undo_redo_restores_exact_source_and_changed_geometry() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let create = f.message(f.owner, MessageKind::Create);
|
||||
let mut document = f.document();
|
||||
let before = document.snapshot();
|
||||
let geometry = Scene::sequence(document.project(), f.owner, create).lifecycle;
|
||||
let mut edited = document.project().elements[&create].clone();
|
||||
if let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { message_kind, .. },
|
||||
..
|
||||
}) = edited.shared_mut()
|
||||
{
|
||||
*message_kind = MessageKind::Asynchronous;
|
||||
}
|
||||
document
|
||||
.execute(Command::EditElement(create, edited))
|
||||
.unwrap();
|
||||
let after = document.snapshot();
|
||||
let changed = Scene::sequence(document.project(), f.owner, create).lifecycle;
|
||||
assert_ne!(geometry, changed);
|
||||
assert!(changed.markers.is_empty());
|
||||
assert_ne!(before.bundle, after.bundle);
|
||||
assert!(document.undo());
|
||||
assert_eq!(document.project(), &before.decoded.project);
|
||||
assert_eq!(document.bundle(), &before.bundle);
|
||||
assert_eq!(
|
||||
Scene::sequence(document.project(), f.owner, create).lifecycle,
|
||||
geometry
|
||||
);
|
||||
assert!(!document.can_undo() && document.can_redo());
|
||||
assert!(document.redo());
|
||||
assert_eq!(document.project(), &after.decoded.project);
|
||||
assert_eq!(document.bundle(), &after.bundle);
|
||||
assert_eq!(
|
||||
Scene::sequence(document.project(), f.owner, create).lifecycle,
|
||||
changed
|
||||
);
|
||||
assert!(!document.can_redo());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panned_header_and_create_clips_use_screen_translation_and_actual_viewport() {
|
||||
use crate::behavior::box_text_clip;
|
||||
use iced::Vector;
|
||||
let header = Rectangle::new(Point::new(100., 200.), Size::new(204., 70.));
|
||||
let capsule = Rectangle::new(header.position(), Size::new(140., 28.));
|
||||
let rect = |x, y, w, h| Rectangle::new(Point::new(x, y), Size::new(w, h));
|
||||
for (pan, scale, viewport, header_expected, birth_expected) in [
|
||||
(
|
||||
Vector::new(37., -41.),
|
||||
0.5,
|
||||
Size::new(300., 400.),
|
||||
rect(92., 63., 92., 27.),
|
||||
rect(91., 61., 62., 10.),
|
||||
),
|
||||
(
|
||||
Vector::new(37., -41.),
|
||||
1.,
|
||||
Size::new(300., 400.),
|
||||
rect(147., 167., 153., 54.),
|
||||
rect(145., 163., 124., 20.),
|
||||
),
|
||||
(
|
||||
Vector::new(37., -41.),
|
||||
2.,
|
||||
Size::new(300., 400.),
|
||||
rect(257., 375., 43., 25.),
|
||||
rect(253., 367., 47., 33.),
|
||||
),
|
||||
(
|
||||
Vector::new(-200., -400.),
|
||||
2.,
|
||||
Size::new(300., 100.),
|
||||
rect(20., 16., 280., 84.),
|
||||
rect(16., 8., 248., 40.),
|
||||
),
|
||||
(
|
||||
Vector::new(-237., -411.),
|
||||
2.,
|
||||
Size::new(300., 100.),
|
||||
rect(0., 5., 300., 95.),
|
||||
rect(0., 0., 227., 37.),
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
box_text_clip(pan, header, scale, viewport),
|
||||
Some(header_expected)
|
||||
);
|
||||
assert_eq!(
|
||||
birth_label_clip(pan, capsule, scale, viewport),
|
||||
Some(birth_expected)
|
||||
);
|
||||
}
|
||||
for pan in [
|
||||
Vector::new(1000., 1000.),
|
||||
Vector::new(-1000., -1000.),
|
||||
Vector::new(f32::NAN, 0.),
|
||||
Vector::new(0., f32::INFINITY),
|
||||
] {
|
||||
assert_eq!(box_text_clip(pan, header, 2., Size::new(300., 100.)), None);
|
||||
assert_eq!(
|
||||
birth_label_clip(pan, capsule, 2., Size::new(300., 100.)),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_scene_panned_zoomed_program_keeps_header_birth_and_death_hit_identity() {
|
||||
use crate::{
|
||||
app::Message,
|
||||
behavior::{
|
||||
SequenceInput,
|
||||
scenario::{CanvasEvent, CanvasTool},
|
||||
},
|
||||
};
|
||||
use iced::{Vector, mouse, widget::canvas};
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
f.project.elements.get_mut(&f.receiver).unwrap().name = "界".repeat(24);
|
||||
let create = f.message(f.owner, MessageKind::Create);
|
||||
let delete = f.message(f.owner, MessageKind::Delete);
|
||||
let document = f.document();
|
||||
let before = document.snapshot();
|
||||
for scale in [0.5, 1., 2.] {
|
||||
for pan in [Vector::new(35., 40.), Vector::new(-35., -5.)] {
|
||||
let mut scene = f.scene();
|
||||
scene.scale = scale;
|
||||
scene.camera_pan = pan;
|
||||
let context = crate::behavior::scenario::direct_tests::context(f.owner);
|
||||
scene.interaction = Some((context, CanvasTool::Select, true));
|
||||
let header = scene.boxes.iter().find(|b| b.id == f.sender).unwrap();
|
||||
let targets = [
|
||||
(f.sender, header.rect.center()),
|
||||
(
|
||||
create,
|
||||
scene
|
||||
.lifecycle
|
||||
.markers
|
||||
.iter()
|
||||
.find(|m| m.event.occurrence == create)
|
||||
.unwrap()
|
||||
.center,
|
||||
),
|
||||
(
|
||||
delete,
|
||||
scene
|
||||
.lifecycle
|
||||
.markers
|
||||
.iter()
|
||||
.find(|m| m.event.occurrence == delete)
|
||||
.unwrap()
|
||||
.center,
|
||||
),
|
||||
];
|
||||
// Absolute widget origin models the outer layout/scroll translation;
|
||||
// camera pan remains local screen units and must be subtracted once.
|
||||
let bounds = Rectangle::new(Point::new(73., 119.), Size::new(2000., 1200.));
|
||||
for (expected, world) in targets {
|
||||
let screen =
|
||||
bounds.position() + pan + Vector::new(world.x * scale, world.y * scale);
|
||||
assert!(bounds.contains(screen));
|
||||
let mut state = SequenceInput::default();
|
||||
let mut published = Vec::new();
|
||||
for event in [
|
||||
mouse::Event::CursorMoved { position: screen },
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left),
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left),
|
||||
] {
|
||||
if let Some(message) = canvas::Program::update(
|
||||
&scene,
|
||||
&mut state,
|
||||
&canvas::Event::Mouse(event),
|
||||
bounds,
|
||||
mouse::Cursor::Available(screen),
|
||||
)
|
||||
.and_then(|a| a.into_inner().0)
|
||||
{
|
||||
published.push(message);
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
matches!(published.as_slice(), [Message::ScenarioCanvas(CanvasEvent::Select(actual_context, actual))] if *actual_context == context && *actual == expected),
|
||||
"Pan/zoom must retain the typed hit identity"
|
||||
);
|
||||
}
|
||||
assert_eq!(scene.lifecycle, f.scene().lifecycle);
|
||||
}
|
||||
}
|
||||
assert_eq!(document.project(), &before.decoded.project);
|
||||
assert_eq!(document.bundle(), &before.bundle);
|
||||
assert!(!document.can_undo() && !document.dirty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
use super::*;
|
||||
use crate::behavior::scenario_activation::{Finish, Segment};
|
||||
|
||||
fn line(scene: &Scene, id: Id) -> &crate::behavior::Line {
|
||||
scene.lines.iter().find(|line| line.id == id).unwrap()
|
||||
}
|
||||
fn segments(scene: &Scene, call: Id) -> Vec<&Segment> {
|
||||
scene
|
||||
.activations
|
||||
.iter()
|
||||
.filter(|a| a.call == call)
|
||||
.collect()
|
||||
}
|
||||
fn assert_visible_lifetime(scene: &Scene) {
|
||||
for a in &scene.activations {
|
||||
assert_eq!(
|
||||
scene
|
||||
.lifecycle
|
||||
.alive_spans(a.receiver, a.scope, a.rect.y, a.rect.y + a.rect.height),
|
||||
vec![Span {
|
||||
start: a.rect.y,
|
||||
end: a.rect.y + a.rect.height
|
||||
}]
|
||||
);
|
||||
if let Finish::Reply(reply) = a.finish {
|
||||
assert_eq!(a.rect.y + a.rect.height, line(scene, reply).points[0].y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admitted_delayed_create_nested_calls_replies_delete_share_one_geometry_pipeline() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let birth = f.message(f.owner, MessageKind::Create);
|
||||
let c1 = f.message(f.owner, MessageKind::Call);
|
||||
let c2 = f.message(f.owner, MessageKind::Call);
|
||||
let r2 = f.reply(f.owner, c2);
|
||||
let r1 = f.reply(f.owner, c1);
|
||||
let death = f.message(f.owner, MessageKind::Delete);
|
||||
let scene = f.scene();
|
||||
assert_eq!(scene.activations.len(), 2);
|
||||
for (call, reply, column) in [(c1, r1, 0), (c2, r2, 1)] {
|
||||
let a = segments(&scene, call)[0];
|
||||
assert_eq!(
|
||||
(a.receiver, a.scope, a.column, a.finish),
|
||||
(f.receiver, f.owner, column, Finish::Reply(reply))
|
||||
);
|
||||
let received = *line(&scene, call).points.last().unwrap();
|
||||
let sent = line(&scene, reply).points[0];
|
||||
assert_eq!((a.rect.y, a.rect.y + a.rect.height), (received.y, sent.y));
|
||||
assert_eq!(received.x, sent.x);
|
||||
assert!(
|
||||
a.rect.y > scene.rows[&birth].1 && a.rect.y + a.rect.height < scene.rows[&death].1
|
||||
);
|
||||
}
|
||||
assert_visible_lifetime(&scene);
|
||||
assert!(scene.open_calls.is_empty());
|
||||
for event in [birth, death] {
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(scene.lanes[&f.receiver], scene.rows[&event].1)),
|
||||
Some(event)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admitted_death_and_incomplete_later_reply_do_not_fabricate_an_answered_execution() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let call = f.message(f.owner, MessageKind::Call);
|
||||
let death = f.message(f.owner, MessageKind::Delete);
|
||||
let reply = f.reply(f.owner, call);
|
||||
if let SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
sender, exchange, ..
|
||||
},
|
||||
..
|
||||
} = f
|
||||
.project
|
||||
.elements
|
||||
.get_mut(&reply)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap()
|
||||
{
|
||||
*sender = None;
|
||||
*exchange = None;
|
||||
}
|
||||
let scene = f.scene();
|
||||
let a = segments(&scene, call)[0];
|
||||
assert_eq!(a.finish, Finish::Terminated(death));
|
||||
assert_eq!(a.rect.y + a.rect.height, scene.rows[&death].1);
|
||||
assert!(
|
||||
scene.boxes.iter().any(|b| b.id == reply),
|
||||
"The incomplete reply remains a repairable card"
|
||||
);
|
||||
assert_eq!(scene.open_calls.len(), 1);
|
||||
assert_visible_lifetime(&scene);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admitted_self_lifecycle_and_self_call_keep_distinct_send_receive_rows() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let action = f.occurrence(
|
||||
f.owner,
|
||||
"Sender preparation",
|
||||
OccurrenceBody::Action {
|
||||
action: None,
|
||||
lifeline: Some(f.sender),
|
||||
},
|
||||
);
|
||||
let birth = f.message(f.owner, MessageKind::Create);
|
||||
let call = f.message(f.owner, MessageKind::Call);
|
||||
let reply = f.reply(f.owner, call);
|
||||
let death = f.message(f.owner, MessageKind::Delete);
|
||||
for event in [birth, call, reply, death] {
|
||||
if let SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
sender,
|
||||
receiver,
|
||||
exchange,
|
||||
..
|
||||
},
|
||||
..
|
||||
} = f
|
||||
.project
|
||||
.elements
|
||||
.get_mut(&event)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap()
|
||||
{
|
||||
*sender = Some(f.receiver);
|
||||
*receiver = Some(f.receiver);
|
||||
*exchange = None;
|
||||
}
|
||||
}
|
||||
let scene = f.scene();
|
||||
let preparation = scene.boxes.iter().find(|b| b.id == action).unwrap();
|
||||
assert!(line(&scene, birth).label_at.y > preparation.rect.y + preparation.rect.height);
|
||||
for event in [birth, death] {
|
||||
let message = line(&scene, event);
|
||||
assert_eq!(message.points[0].y, scene.rows[&event].1 - 24.);
|
||||
assert_eq!(message.points.last().unwrap().y, scene.rows[&event].1);
|
||||
assert_eq!(
|
||||
scene.hit(Point::new(scene.lanes[&f.receiver], scene.rows[&event].1)),
|
||||
Some(event)
|
||||
);
|
||||
}
|
||||
let activation = segments(&scene, call)[0];
|
||||
assert_eq!(activation.rect.y, scene.rows[&call].1 + 24.);
|
||||
assert_eq!(
|
||||
activation.rect.y,
|
||||
line(&scene, call).points.last().unwrap().y
|
||||
);
|
||||
assert_eq!(
|
||||
activation.rect.y + activation.rect.height,
|
||||
line(&scene, reply).points[0].y
|
||||
);
|
||||
assert_eq!(activation.finish, Finish::Reply(reply));
|
||||
assert_visible_lifetime(&scene);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admitted_unresolved_operand_keeps_unanswered_cue_without_any_visible_segment() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let fragment = f.fragment(f.owner, FragmentOperator::Alternative);
|
||||
let left = f.branch(fragment);
|
||||
f.message(left, MessageKind::Create);
|
||||
let right = f.branch(fragment);
|
||||
let call = f.message(right, MessageKind::Call);
|
||||
let scene = f.scene();
|
||||
assert!(segments(&scene, call).is_empty());
|
||||
assert!(
|
||||
intervals(&scene, f.receiver, right)
|
||||
.iter()
|
||||
.all(|i| i.2 == Life::Unresolved)
|
||||
);
|
||||
assert_eq!(scene.open_calls.len(), 1);
|
||||
assert!(scene.open_calls[0].y > scene.rows[&call].1);
|
||||
assert_eq!(
|
||||
line(&scene, call).points.last().unwrap().x,
|
||||
scene.lanes[&f.receiver]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admitted_calls_without_sender_polylines_still_have_logical_correlation_metadata() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let c1 = f.message(f.owner, MessageKind::Call);
|
||||
let c2 = f.message(f.owner, MessageKind::Call);
|
||||
for call in [c1, c2] {
|
||||
if let SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
sender, exchange, ..
|
||||
},
|
||||
..
|
||||
} = f
|
||||
.project
|
||||
.elements
|
||||
.get_mut(&call)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap()
|
||||
{
|
||||
*sender = None;
|
||||
*exchange = None;
|
||||
}
|
||||
}
|
||||
let outgoing = f.occurrence(
|
||||
f.owner,
|
||||
"Outgoing",
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: None,
|
||||
sender: Some(f.receiver),
|
||||
receiver: Some(f.sender),
|
||||
message_kind: MessageKind::Asynchronous,
|
||||
operation: None,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
},
|
||||
);
|
||||
f.reply(f.owner, c2);
|
||||
f.reply(f.owner, c1);
|
||||
let scene = f.scene();
|
||||
assert!(scene.boxes.iter().any(|b| b.id == c1));
|
||||
assert!(scene.boxes.iter().any(|b| b.id == c2));
|
||||
let inner = segments(&scene, c2)[0];
|
||||
let expected = if scene.lanes[&f.sender] > scene.lanes[&f.receiver] {
|
||||
inner.rect.x + inner.rect.width
|
||||
} else {
|
||||
inner.rect.x
|
||||
};
|
||||
assert_eq!(line(&scene, outgoing).points[0].x, expected);
|
||||
}
|
||||
}
|
||||
|
||||
fn lifetime_rejected(f: &Fixture) {
|
||||
assert!(validate_candidate(&f.project).is_err());
|
||||
assert!(
|
||||
validation::validate(&f.project)
|
||||
.iter()
|
||||
.any(|issue| issue.severity == validation::Severity::Error
|
||||
&& issue.code == "lifeline_lifetime")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_prebirth_call_and_postdeath_reply_remain_rejected_with_defensive_scene_clipping() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for later_birth in [false, true] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let call = f.message(f.owner, MessageKind::Call);
|
||||
let death = f.message(f.owner, MessageKind::Delete);
|
||||
if later_birth {
|
||||
f.message(f.owner, MessageKind::Create);
|
||||
}
|
||||
let reply = f.reply(f.owner, call);
|
||||
lifetime_rejected(&f);
|
||||
// Deliberately bypass Document only for defensive renderer coverage
|
||||
// of rejected data. This is not an admitted editing workflow.
|
||||
let scene = Scene::sequence(&f.project, f.owner, f.owner);
|
||||
assert!(
|
||||
scene
|
||||
.activations
|
||||
.iter()
|
||||
.all(|a| a.rect.y + a.rect.height <= scene.rows[&death].1)
|
||||
);
|
||||
assert!(
|
||||
scene
|
||||
.activations
|
||||
.iter()
|
||||
.all(|a| a.finish != Finish::Reply(reply))
|
||||
);
|
||||
assert_eq!(line(&scene, reply).points[0].x, scene.lanes[&f.receiver]);
|
||||
assert!(!scene.open_calls.is_empty());
|
||||
assert_visible_lifetime(&scene);
|
||||
}
|
||||
let mut f = Fixture::new(layer);
|
||||
let call = f.message(f.owner, MessageKind::Call);
|
||||
let birth = f.message(f.owner, MessageKind::Create);
|
||||
let reply = f.reply(f.owner, call);
|
||||
lifetime_rejected(&f);
|
||||
let scene = Scene::sequence(&f.project, f.owner, f.owner);
|
||||
assert_eq!(segments(&scene, call)[0].rect.y, scene.rows[&birth].1);
|
||||
assert_eq!(segments(&scene, call)[0].finish, Finish::Reply(reply));
|
||||
assert_eq!(
|
||||
line(&scene, call).points.last().unwrap().x,
|
||||
scene.lanes[&f.receiver]
|
||||
);
|
||||
assert_visible_lifetime(&scene);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_death_then_recreate_never_revives_call_but_keeps_open_sibling() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
f.message(f.owner, MessageKind::Create);
|
||||
let call = f.message(f.owner, MessageKind::Call);
|
||||
let fragment = f.fragment(f.owner, FragmentOperator::Alternative);
|
||||
let left = f.branch(fragment);
|
||||
let death = f.message(left, MessageKind::Delete);
|
||||
let right = f.branch(fragment);
|
||||
let rebirth = f.message(f.owner, MessageKind::Create);
|
||||
let reply = f.reply(f.owner, call);
|
||||
lifetime_rejected(&f);
|
||||
let scene = Scene::sequence(&f.project, f.owner, f.owner);
|
||||
let bars = segments(&scene, call);
|
||||
assert!(
|
||||
bars.iter()
|
||||
.any(|a| a.scope == left && a.finish == Finish::Terminated(death))
|
||||
);
|
||||
assert!(
|
||||
bars.iter().any(|a| a.scope == right
|
||||
&& a.rect.height == 26.
|
||||
&& a.finish == Finish::Continuation)
|
||||
);
|
||||
assert!(
|
||||
bars.iter()
|
||||
.all(|a| a.rect.y + a.rect.height < scene.rows[&rebirth].1)
|
||||
);
|
||||
assert!(!bars.iter().any(|a| a.finish == Finish::Reply(reply)));
|
||||
assert_eq!(line(&scene, reply).points[0].x, scene.lanes[&f.receiver]);
|
||||
assert_eq!(
|
||||
scene.open_calls.len(),
|
||||
3,
|
||||
"Terminated, open sibling and uncertain parent cues survive"
|
||||
);
|
||||
assert_visible_lifetime(&scene);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipped_operand_preserves_original_nested_or_crossed_correlation_distinction() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for nested in [false, true] {
|
||||
let mut f = Fixture::new(layer);
|
||||
f.message(f.owner, MessageKind::Create);
|
||||
let c1 = f.message(f.owner, MessageKind::Call);
|
||||
let c2 = f.message(f.owner, MessageKind::Call);
|
||||
let fragment = f.fragment(f.owner, FragmentOperator::Alternative);
|
||||
let left = f.branch(fragment);
|
||||
f.message(left, MessageKind::Delete);
|
||||
let right = f.branch(fragment);
|
||||
let outgoing = f.occurrence(
|
||||
right,
|
||||
"Outgoing",
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: None,
|
||||
sender: Some(f.receiver),
|
||||
receiver: Some(f.sender),
|
||||
message_kind: MessageKind::Asynchronous,
|
||||
operation: None,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
},
|
||||
);
|
||||
let r1 = f.reply(f.owner, if nested { c2 } else { c1 });
|
||||
let r2 = f.reply(f.owner, if nested { c1 } else { c2 });
|
||||
lifetime_rejected(&f);
|
||||
let scene = Scene::sequence(&f.project, f.owner, f.owner);
|
||||
let send = line(&scene, outgoing).points[0];
|
||||
let expected = if nested {
|
||||
let inner = segments(&scene, c2)
|
||||
.into_iter()
|
||||
.find(|a| a.scope == right)
|
||||
.unwrap();
|
||||
if scene.lanes[&f.sender] > scene.lanes[&f.receiver] {
|
||||
inner.rect.x + inner.rect.width
|
||||
} else {
|
||||
inner.rect.x
|
||||
}
|
||||
} else {
|
||||
scene.lanes[&f.receiver]
|
||||
};
|
||||
assert_eq!(send.x, expected);
|
||||
for call in [c1, c2] {
|
||||
assert!(segments(&scene, call).iter().any(|a| a.scope == right
|
||||
&& a.rect.y <= send.y
|
||||
&& send.y < a.rect.y + a.rect.height));
|
||||
}
|
||||
assert!(scene.rows[&r1].1 < scene.rows[&r2].1);
|
||||
assert_visible_lifetime(&scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn with_offset<T>(id: Id, offset: f32, f: impl FnOnce() -> T) -> T {
|
||||
struct Restore(BTreeMap<Id, f32>);
|
||||
impl Drop for Restore {
|
||||
fn drop(&mut self) {
|
||||
TEST_ROW_OFFSETS.with(|current| *current.borrow_mut() = std::mem::take(&mut self.0));
|
||||
}
|
||||
}
|
||||
let _restore =
|
||||
Restore(TEST_ROW_OFFSETS.with(|current| current.replace(BTreeMap::from([(id, offset)]))));
|
||||
f()
|
||||
}
|
||||
fn snapshot(scene: &Scene) -> String {
|
||||
format!(
|
||||
"{:?}",
|
||||
(
|
||||
&scene.lifecycle,
|
||||
&scene.activations,
|
||||
&scene.open_calls,
|
||||
&scene.rows,
|
||||
&scene.sequence_bounds,
|
||||
scene
|
||||
.lines
|
||||
.iter()
|
||||
.map(|l| (l.id, &l.points, &l.label, l.label_at, l.dashed))
|
||||
.collect::<Vec<_>>(),
|
||||
scene
|
||||
.boxes
|
||||
.iter()
|
||||
.map(|b| (b.id, b.rect, &b.title, &b.detail, b.marker))
|
||||
.collect::<Vec<_>>(),
|
||||
scene
|
||||
.slots
|
||||
.iter()
|
||||
.map(|s| (s.slot, s.y, &s.label))
|
||||
.collect::<Vec<_>>(),
|
||||
scene.size,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_only_upstream_row_offsets_update_lifetime_segments_and_endpoints_without_authoring() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = Fixture::new(layer);
|
||||
let birth = f.message(f.owner, MessageKind::Create);
|
||||
let c1 = f.message(f.owner, MessageKind::Call);
|
||||
let c2 = f.message(f.owner, MessageKind::Call);
|
||||
f.reply(f.owner, c2);
|
||||
f.reply(f.owner, c1);
|
||||
let death = f.message(f.owner, MessageKind::Delete);
|
||||
let document = f.document();
|
||||
let before = document.snapshot();
|
||||
let baseline = Scene::sequence(document.project(), f.owner, c1);
|
||||
let moved = with_offset(birth, 60., || {
|
||||
Scene::sequence(document.project(), f.owner, c1)
|
||||
});
|
||||
assert_eq!(moved.rows[&birth].1, baseline.rows[&birth].1 + 60.);
|
||||
assert_eq!(moved.rows[&death].1, baseline.rows[&death].1 + 60.);
|
||||
for (a, b) in baseline.activations.iter().zip(&moved.activations) {
|
||||
assert_eq!(
|
||||
(a.call, a.receiver, a.scope, a.column, a.finish),
|
||||
(b.call, b.receiver, b.scope, b.column, b.finish)
|
||||
);
|
||||
assert_eq!(
|
||||
b.rect,
|
||||
Rectangle::new(
|
||||
a.rect.position() + iced::Vector::new(0., 60.),
|
||||
a.rect.size()
|
||||
)
|
||||
);
|
||||
}
|
||||
assert_eq!(baseline.activations.len(), moved.activations.len());
|
||||
assert_visible_lifetime(&moved);
|
||||
let reparsed = language::parse(&language::format(document.project())).unwrap();
|
||||
assert_eq!(reparsed, *document.project());
|
||||
assert_eq!(
|
||||
snapshot(&moved),
|
||||
with_offset(birth, 60., || snapshot(&Scene::sequence(
|
||||
&reparsed, f.owner, c1
|
||||
)))
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot(&baseline),
|
||||
snapshot(&Scene::sequence(document.project(), f.owner, c1))
|
||||
);
|
||||
assert_eq!(document.project(), &before.decoded.project);
|
||||
assert_eq!(document.bundle(), &before.bundle);
|
||||
assert_eq!(document.revision(), 0);
|
||||
assert!(!document.dirty() && !document.can_undo() && !document.can_redo());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user