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:
@@ -21,8 +21,6 @@ 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;
|
||||
pub(crate) mod source_draft;
|
||||
pub(crate) mod state_canvas;
|
||||
@@ -1127,6 +1125,31 @@ struct BoxItem {
|
||||
detail: String,
|
||||
marker: Option<Kind>,
|
||||
}
|
||||
fn box_text_clip(
|
||||
pan: iced::Vector,
|
||||
rect: Rectangle,
|
||||
scale: f32,
|
||||
viewport: Size,
|
||||
) -> Option<Rectangle> {
|
||||
if !pan.x.is_finite()
|
||||
|| !pan.y.is_finite()
|
||||
|| !scale.is_finite()
|
||||
|| scale <= 0.
|
||||
|| rect.width <= 20.
|
||||
|| rect.height <= 16.
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Rectangle::new(
|
||||
Point::new(
|
||||
pan.x + (rect.x + 10.) * scale,
|
||||
pan.y + (rect.y + 8.) * scale,
|
||||
),
|
||||
Size::new((rect.width - 20.) * scale, (rect.height - 16.) * scale),
|
||||
)
|
||||
.intersection(&Rectangle::new(Point::ORIGIN, viewport))
|
||||
}
|
||||
|
||||
struct Line {
|
||||
id: Id,
|
||||
points: Vec<Point>,
|
||||
@@ -1147,6 +1170,7 @@ struct Scene {
|
||||
slots: Vec<SequenceSlot>,
|
||||
rows: BTreeMap<Id, (Id, f32)>,
|
||||
activations: Vec<scenario_activation::Segment>,
|
||||
lifecycle: scenario_lifecycle::render::Rendering,
|
||||
open_calls: Vec<Point>,
|
||||
sequence_bounds: BTreeMap<Id, (f32, f32)>,
|
||||
}
|
||||
@@ -1211,6 +1235,7 @@ impl Scene {
|
||||
slots: vec![],
|
||||
rows: BTreeMap::new(),
|
||||
activations: vec![],
|
||||
lifecycle: scenario_lifecycle::render::Rendering::default(),
|
||||
open_calls: vec![],
|
||||
sequence_bounds: BTreeMap::new(),
|
||||
};
|
||||
@@ -1299,22 +1324,33 @@ impl Scene {
|
||||
}
|
||||
let mut y = if scenario { 148. } else { 120. };
|
||||
scene.occurrences(p, owner, scenario, &lane_x, &mut y, 0);
|
||||
scene.size.height = (y + 45.).max(210.);
|
||||
if scenario {
|
||||
scene.lifecycle = scenario_lifecycle::render::Rendering::new(
|
||||
p,
|
||||
owner,
|
||||
&lane_x,
|
||||
&scene.rows,
|
||||
&scene.sequence_bounds,
|
||||
scene.size.height,
|
||||
);
|
||||
scene.lifecycle.align_messages(&mut scene.lines);
|
||||
scene.layout_activations(p, &lane_x);
|
||||
}
|
||||
scene.size.height = (y + 45.).max(210.);
|
||||
for (id, name, detail) in lanes {
|
||||
let x = lane_x[&id];
|
||||
scene.lines.insert(
|
||||
0,
|
||||
Line {
|
||||
id,
|
||||
points: vec![Point::new(x, 85.), Point::new(x, scene.size.height - 20.)],
|
||||
label: String::new(),
|
||||
label_at: Point::ORIGIN,
|
||||
dashed: false,
|
||||
},
|
||||
);
|
||||
if !scenario {
|
||||
scene.lines.insert(
|
||||
0,
|
||||
Line {
|
||||
id,
|
||||
points: vec![Point::new(x, 85.), Point::new(x, scene.size.height - 20.)],
|
||||
label: String::new(),
|
||||
label_at: Point::ORIGIN,
|
||||
dashed: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
scene.boxes.push(BoxItem {
|
||||
id,
|
||||
rect: Rectangle::new(Point::new(x - 102., 15.), Size::new(204., 70.)),
|
||||
@@ -1360,6 +1396,18 @@ impl Scene {
|
||||
let Some(SharedBody::Occurrence { body, .. }) = e.shared() else {
|
||||
continue;
|
||||
};
|
||||
if scenario
|
||||
&& matches!(body, OccurrenceBody::Exchange {
|
||||
sender: Some(sender), receiver: Some(receiver),
|
||||
message_kind: MessageKind::Create | MessageKind::Delete, ..
|
||||
} if sender == receiver)
|
||||
{
|
||||
// Record the lifecycle receive row only after making room for
|
||||
// the self-message elbow and caption above its marker.
|
||||
*y += 16.;
|
||||
}
|
||||
#[cfg(test)]
|
||||
scenario_lifecycle::render::apply_test_row_offset(*id, y);
|
||||
if ordered {
|
||||
let slot = scenario::Slot {
|
||||
parent: owner,
|
||||
@@ -1607,6 +1655,12 @@ impl Scene {
|
||||
))
|
||||
}
|
||||
fn hit(&self, point: Point) -> Option<Id> {
|
||||
if !point.x.is_finite() || !point.y.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if let Some(id) = self.lifecycle.marker_at(point) {
|
||||
return Some(id);
|
||||
}
|
||||
self.boxes
|
||||
.iter()
|
||||
.rev()
|
||||
@@ -1652,18 +1706,8 @@ impl Scene {
|
||||
})
|
||||
// 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.near_sequence_item(point))
|
||||
.or_else(|| self.lifecycle.lane_at(point, self.scale))
|
||||
.or_else(|| self.lane_at(point))
|
||||
.or_else(|| {
|
||||
self.lines
|
||||
@@ -1678,6 +1722,33 @@ impl Scene {
|
||||
.map(|line| line.id)
|
||||
})
|
||||
}
|
||||
|
||||
fn near_sequence_item(&self, point: Point) -> Option<Id> {
|
||||
if !self.scale.is_finite() || self.scale <= 0. {
|
||||
return None;
|
||||
}
|
||||
let messages = self
|
||||
.lines
|
||||
.iter()
|
||||
.filter(|line| !line.label.is_empty())
|
||||
.map(|line| {
|
||||
let distance = line
|
||||
.points
|
||||
.windows(2)
|
||||
.map(|p| crate::diagram::distance_to_segment(point, p[0], p[1]))
|
||||
.fold(f32::INFINITY, f32::min);
|
||||
(line.id, distance)
|
||||
});
|
||||
// Compare broad message and lifecycle-marker tolerance together after
|
||||
// exact shapes, so dense minimum-Fit rows choose the nearer item.
|
||||
self.lifecycle
|
||||
.nearest_marker(point)
|
||||
.into_iter()
|
||||
.chain(messages)
|
||||
.filter(|(_, distance)| *distance <= 8. / self.scale)
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0)))
|
||||
.map(|(id, _)| id)
|
||||
}
|
||||
}
|
||||
impl canvas::Program<Message> for Scene {
|
||||
type State = SequenceInput;
|
||||
@@ -1962,6 +2033,7 @@ impl canvas::Program<Message> for Scene {
|
||||
f.fill_rectangle(Point::ORIGIN, bounds.size(), Color::WHITE);
|
||||
f.translate(self.camera_pan);
|
||||
f.scale(self.scale);
|
||||
self.lifecycle.draw_lanes(&mut f, self.selected);
|
||||
// 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()) {
|
||||
@@ -2084,24 +2156,32 @@ impl canvas::Program<Message> for Scene {
|
||||
}
|
||||
}
|
||||
for b in &self.boxes {
|
||||
f.fill_text(Text {
|
||||
content: short(&b.title, ((b.rect.width - 25.) / 7.) as usize),
|
||||
position: Point::new(b.rect.x + 12., b.rect.y + 12.),
|
||||
size: 13.into(),
|
||||
color: if b.id == self.selected {
|
||||
style::TEAL
|
||||
} else {
|
||||
style::INK
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
if !b.detail.is_empty() {
|
||||
f.fill_text(Text {
|
||||
content: short(&b.detail, ((b.rect.width - 25.) / 6.) as usize),
|
||||
position: Point::new(b.rect.x + 12., b.rect.y + 39.),
|
||||
size: 11.into(),
|
||||
color: style::MUTED,
|
||||
..Default::default()
|
||||
if let Some(clip) = box_text_clip(self.camera_pan, b.rect, self.scale, f.size()) {
|
||||
// WGPU child clips start with an identity transform, as in
|
||||
// the lifecycle capsule label painter.
|
||||
f.with_clip(clip, |label| {
|
||||
label.translate(self.camera_pan);
|
||||
label.scale(self.scale);
|
||||
label.fill_text(Text {
|
||||
content: short(&b.title, ((b.rect.width - 25.) / 7.) as usize),
|
||||
position: Point::new(b.rect.x + 12., b.rect.y + 12.),
|
||||
size: 13.into(),
|
||||
color: if b.id == self.selected {
|
||||
style::TEAL
|
||||
} else {
|
||||
style::INK
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
if !b.detail.is_empty() {
|
||||
label.fill_text(Text {
|
||||
content: short(&b.detail, ((b.rect.width - 25.) / 6.) as usize),
|
||||
position: Point::new(b.rect.x + 12., b.rect.y + 39.),
|
||||
size: 11.into(),
|
||||
color: style::MUTED,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2114,6 +2194,8 @@ impl canvas::Program<Message> for Scene {
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
self.lifecycle
|
||||
.draw_markers(&mut f, self.selected, self.scale, self.camera_pan);
|
||||
if let Some((context, tool, editable)) = self.interaction {
|
||||
let active = editable && state.context == Some(context) && state.tool == tool;
|
||||
let point = cursor.position_in(bounds).map(|p| {
|
||||
|
||||
Reference in New Issue
Block a user