Compose saved Scenario authoring with current Text and input authority
This commit is contained in:
@@ -21,6 +21,11 @@ pub(crate) mod creation;
|
||||
mod form_session;
|
||||
pub(crate) mod scenario;
|
||||
mod scenario_activation;
|
||||
#[cfg(test)]
|
||||
mod scenario_hit_tests;
|
||||
// Pure saved-geometry bridge; workspace integration is a separate checkpoint.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) mod scenario_layout;
|
||||
mod scenario_lifecycle;
|
||||
pub(crate) mod source_draft;
|
||||
pub(crate) mod state_canvas;
|
||||
@@ -59,9 +64,18 @@ mod scenario_scroll {
|
||||
process: bool,
|
||||
restore: Option<scenario::ScrollRestore>,
|
||||
}
|
||||
type Report = Option<(bool, Id, Id, Vector)>;
|
||||
type Identity = (Id, Id, scenario_layout::Representation, Id);
|
||||
fn identity(context: scenario::Context) -> Identity {
|
||||
(
|
||||
context.document,
|
||||
context.scenario,
|
||||
context.representation,
|
||||
context.visit,
|
||||
)
|
||||
}
|
||||
type Report = Option<(bool, Identity, Vector)>;
|
||||
struct ObservedScroll {
|
||||
identity: (Id, Id),
|
||||
identity: Identity,
|
||||
report: Report,
|
||||
restored: Option<Id>,
|
||||
}
|
||||
@@ -112,8 +126,12 @@ mod scenario_scroll {
|
||||
) {
|
||||
let restore = self.restore.filter(|request| {
|
||||
!self.process
|
||||
&& (request.document, request.owner)
|
||||
== (self.context.document, self.context.scenario)
|
||||
&& (
|
||||
request.document,
|
||||
request.owner,
|
||||
request.representation,
|
||||
request.visit,
|
||||
) == identity(self.context)
|
||||
&& tree.state.downcast_ref::<ObservedScroll>().restored != Some(request.token)
|
||||
});
|
||||
let receipt = restore.and_then(|request| {
|
||||
@@ -137,12 +155,7 @@ mod scenario_scroll {
|
||||
&mut offset,
|
||||
);
|
||||
if let Some(offset) = offset.0 {
|
||||
let next = Some((
|
||||
self.process,
|
||||
self.context.document,
|
||||
self.context.scenario,
|
||||
offset,
|
||||
));
|
||||
let next = Some((self.process, identity(self.context), offset));
|
||||
let state = tree.state.downcast_mut::<ObservedScroll>();
|
||||
if let Some(token) = receipt {
|
||||
state.restored = Some(token);
|
||||
@@ -186,7 +199,7 @@ mod scenario_scroll {
|
||||
}
|
||||
fn state(&self) -> widget::tree::State {
|
||||
widget::tree::State::new(ObservedScroll {
|
||||
identity: (self.context.document, self.context.scenario),
|
||||
identity: identity(self.context),
|
||||
report: None,
|
||||
restored: None,
|
||||
})
|
||||
@@ -195,9 +208,7 @@ mod scenario_scroll {
|
||||
vec![widget::Tree::new(self.content.as_widget())]
|
||||
}
|
||||
fn diff(&self, tree: &mut widget::Tree) {
|
||||
if tree.state.downcast_ref::<ObservedScroll>().identity
|
||||
!= (self.context.document, self.context.scenario)
|
||||
{
|
||||
if tree.state.downcast_ref::<ObservedScroll>().identity != identity(self.context) {
|
||||
// A new owner/document needs fresh real scroll state, not merely
|
||||
// cleared mirrored offsets. Edits and tool/form changes retain it.
|
||||
tree.state = self.state();
|
||||
@@ -486,17 +497,25 @@ mod scenario_scroll {
|
||||
}
|
||||
#[test]
|
||||
fn direct_scenario_restore_cannot_scroll_another_document_owner_or_process_view() {
|
||||
for mismatch in 0..3 {
|
||||
for mismatch in 0..5 {
|
||||
let context = scenario::direct_tests::context(Id::new_v4());
|
||||
let mut request = scenario::ScrollRestore {
|
||||
document: context.document,
|
||||
owner: context.scenario,
|
||||
representation: context.representation,
|
||||
visit: context.visit,
|
||||
token: Id::new_v4(),
|
||||
offset: Vector::new(0., 180.),
|
||||
};
|
||||
match mismatch {
|
||||
0 => request.document = Id::new_v4(),
|
||||
1 => request.owner = Id::new_v4(),
|
||||
3 => {
|
||||
request.representation = scenario_layout::Representation::Saved {
|
||||
diagram: Id::new_v4(),
|
||||
}
|
||||
}
|
||||
4 => request.visit = Id::new_v4(),
|
||||
_ => {}
|
||||
}
|
||||
let mut view = observer(600., context);
|
||||
@@ -583,7 +602,7 @@ mod scenario_scroll {
|
||||
use widget::operation::scrollable::{AbsoluteOffset, scroll_to};
|
||||
for process in [false, true] {
|
||||
let area = scenario::ScrollArea::Page;
|
||||
for change in 0..4 {
|
||||
for change in 0..6 {
|
||||
let context = scenario::direct_tests::context(Id::new_v4());
|
||||
let mut view = observer(600., context);
|
||||
view.process = process;
|
||||
@@ -612,6 +631,12 @@ mod scenario_scroll {
|
||||
next.revision += 1;
|
||||
next.epoch += 5;
|
||||
}
|
||||
4 => {
|
||||
next.representation = scenario_layout::Representation::Saved {
|
||||
diagram: Id::new_v4(),
|
||||
}
|
||||
}
|
||||
5 => next.visit = Id::new_v4(),
|
||||
_ => {}
|
||||
}
|
||||
view = observer(if change == 3 { 800. } else { 600. }, next);
|
||||
@@ -659,7 +684,7 @@ mod scenario_scroll {
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![if change < 2 {
|
||||
vec![if !(2..4).contains(&change) {
|
||||
Vector::ZERO
|
||||
} else {
|
||||
Vector::new(0., 180.)
|
||||
@@ -973,6 +998,19 @@ pub fn view(app: &App) -> Element<'_, Message> {
|
||||
return container(out).padding(16).width(Fill).height(Fill).into();
|
||||
}
|
||||
}
|
||||
// A requested saved view remains the destination even after its model is removed.
|
||||
// The shared display adapter reports unavailable instead of choosing another behavior.
|
||||
if matches!(
|
||||
app.behavior.scenario.representation(),
|
||||
Some(scenario_layout::Representation::Saved { .. })
|
||||
) && let Some(owner) = app.behavior.scenario.owner()
|
||||
&& !p
|
||||
.elements
|
||||
.get(&owner)
|
||||
.is_some_and(|element| element.kind() == Kind::Scenario && element.layer() == app.layer)
|
||||
{
|
||||
return scenario::view(app, owner, column![].spacing(6));
|
||||
}
|
||||
let choices = p
|
||||
.elements
|
||||
.values()
|
||||
@@ -984,7 +1022,14 @@ pub fn view(app: &App) -> Element<'_, Message> {
|
||||
label: format!("{} · {}", e.kind(), e.name),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let owner = selected_owner(app);
|
||||
let owner = if matches!(
|
||||
app.behavior.scenario.representation(),
|
||||
Some(scenario_layout::Representation::Saved { .. })
|
||||
) {
|
||||
app.behavior.scenario.owner()
|
||||
} else {
|
||||
selected_owner(app)
|
||||
};
|
||||
let selected = owner
|
||||
.and_then(|id| choices.iter().find(|c| c.id == id))
|
||||
.or(choices.first())
|
||||
@@ -1118,8 +1163,27 @@ fn in_machine(p: &Project, id: Id, machine: Id) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Identifies the drawn primitive that won selection, not an authoring permission.
|
||||
/// In particular an activation and its message can share an occurrence UUID.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum SceneHitRole {
|
||||
LifelineHeader,
|
||||
MessageBody,
|
||||
MessageLabel,
|
||||
Activation,
|
||||
LifecycleMarker,
|
||||
LifelineGuide,
|
||||
Box,
|
||||
OtherLine,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct SceneHit {
|
||||
id: Id,
|
||||
role: SceneHitRole,
|
||||
}
|
||||
struct BoxItem {
|
||||
id: Id,
|
||||
role: SceneHitRole,
|
||||
rect: Rectangle,
|
||||
title: String,
|
||||
detail: String,
|
||||
@@ -1152,6 +1216,7 @@ fn box_text_clip(
|
||||
|
||||
struct Line {
|
||||
id: Id,
|
||||
role: SceneHitRole,
|
||||
points: Vec<Point>,
|
||||
label: String,
|
||||
label_at: Point,
|
||||
@@ -1172,12 +1237,17 @@ struct Scene {
|
||||
boxes: Vec<BoxItem>,
|
||||
lines: Vec<Line>,
|
||||
size: Size,
|
||||
layout_origin: Point,
|
||||
selected: Id,
|
||||
scale: f32,
|
||||
camera_pan: iced::Vector,
|
||||
interaction: Option<(scenario::Context, scenario::CanvasTool, bool)>,
|
||||
spacing_stamp: Option<scenario::spacing::Stamp>,
|
||||
coordinate_input: bool,
|
||||
pending_placement: Option<(scenario::Slot, Option<Id>)>,
|
||||
lanes: BTreeMap<Id, f32>,
|
||||
// Exact render coordinates include unassigned lanes; do not derive from cards.
|
||||
render_lanes: BTreeMap<Id, f32>,
|
||||
slots: Vec<SequenceSlot>,
|
||||
rows: BTreeMap<Id, (Id, f32)>,
|
||||
activations: Vec<scenario_activation::Segment>,
|
||||
@@ -1201,6 +1271,10 @@ struct SequenceInput {
|
||||
}
|
||||
#[derive(Clone, Copy)]
|
||||
enum SequencePress {
|
||||
Spacing {
|
||||
stamp: scenario::spacing::Stamp,
|
||||
token: Id,
|
||||
},
|
||||
Occurrence {
|
||||
tool: scenario::Tool,
|
||||
slot: scenario::Slot,
|
||||
@@ -1221,6 +1295,19 @@ enum SequencePress {
|
||||
},
|
||||
}
|
||||
impl SequenceInput {
|
||||
fn cancel_spacing(&mut self) -> Option<Action<Message>> {
|
||||
let cancel = match self.press {
|
||||
Some(SequencePress::Spacing { stamp, token }) => Some(
|
||||
Action::publish(Message::ScenarioCanvas(scenario::CanvasEvent::Spacing(
|
||||
scenario::spacing::Event::Cancel { stamp, token },
|
||||
)))
|
||||
.and_capture(),
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
self.cancel();
|
||||
cancel
|
||||
}
|
||||
fn cancel(&mut self) {
|
||||
self.source = None;
|
||||
self.press = None;
|
||||
@@ -1232,17 +1319,31 @@ impl Scene {
|
||||
&& (self.boxes.iter().any(|item| item.id == id)
|
||||
|| self.lines.iter().any(|item| item.id == id))
|
||||
}
|
||||
// The integrated removal guard also needs the derived semantic hit set.
|
||||
fn sequence(p: &Project, owner: Id, selected: Id) -> Self {
|
||||
Self::sequence_geometry(p, owner, selected, None, None)
|
||||
}
|
||||
fn sequence_geometry(
|
||||
p: &Project,
|
||||
owner: Id,
|
||||
selected: Id,
|
||||
lane_override: Option<&BTreeMap<Id, f32>>,
|
||||
plan: Option<&scenario_layout::Plan>,
|
||||
) -> Self {
|
||||
let mut scene = Self {
|
||||
scale: 1.,
|
||||
camera_pan: iced::Vector::ZERO,
|
||||
boxes: vec![],
|
||||
lines: vec![],
|
||||
size: Size::new(700., 200.),
|
||||
layout_origin: Point::ORIGIN,
|
||||
selected,
|
||||
interaction: None,
|
||||
spacing_stamp: None,
|
||||
coordinate_input: true,
|
||||
pending_placement: None,
|
||||
lanes: BTreeMap::new(),
|
||||
render_lanes: BTreeMap::new(),
|
||||
slots: vec![],
|
||||
rows: BTreeMap::new(),
|
||||
activations: vec![],
|
||||
@@ -1313,12 +1414,32 @@ impl Scene {
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
let lane_x = lanes
|
||||
let mut lane_x = lanes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (id, ..))| (*id, 160. + i as f32 * 240.))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
if let Some(values) = lane_override {
|
||||
for (id, x) in values {
|
||||
if let Some(lane) = lane_x.get_mut(id) {
|
||||
*lane = *x;
|
||||
}
|
||||
}
|
||||
}
|
||||
scene.render_lanes = lane_x.clone();
|
||||
scene.size.width = (lanes.len() as f32 * 240. + 80.).max(680.);
|
||||
let layout_left = if lane_override.is_some() {
|
||||
lane_x.values().fold(0_f32, |left, x| left.min(*x - 160.))
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
scene.layout_origin.x = layout_left;
|
||||
if lane_override.is_some() {
|
||||
scene.size.width = lane_x
|
||||
.values()
|
||||
.fold(scene.size.width, |right, x| right.max(*x + 240.))
|
||||
- layout_left;
|
||||
}
|
||||
if scenario {
|
||||
scene.lanes = lane_x
|
||||
.iter()
|
||||
@@ -1334,8 +1455,26 @@ impl Scene {
|
||||
.collect();
|
||||
}
|
||||
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.);
|
||||
scene.occurrences(p, owner, scenario, &lane_x, &mut y, 0, plan);
|
||||
if layout_left != 0. {
|
||||
for item in &mut scene.boxes {
|
||||
let body = scenario_layout::occurrence(p, item.id);
|
||||
if matches!(
|
||||
body,
|
||||
Some(
|
||||
OccurrenceBody::Branch { .. }
|
||||
| OccurrenceBody::ScenarioUse { .. }
|
||||
| OccurrenceBody::Control { .. }
|
||||
| OccurrenceBody::SequenceReference { .. }
|
||||
| OccurrenceBody::Exchange { .. }
|
||||
)
|
||||
) || (plan.is_none() && matches!(body, Some(OccurrenceBody::Fragment { .. })))
|
||||
{
|
||||
item.rect.x += layout_left;
|
||||
}
|
||||
}
|
||||
}
|
||||
scene.size.height = plan.map_or((y + 45.).max(210.), |plan| plan.height);
|
||||
if scenario {
|
||||
scene.lifecycle = scenario_lifecycle::render::Rendering::new(
|
||||
p,
|
||||
@@ -1346,6 +1485,9 @@ impl Scene {
|
||||
scene.size.height,
|
||||
);
|
||||
scene.lifecycle.align_messages(&mut scene.lines);
|
||||
if let Some(plan) = plan {
|
||||
plan.align_message_anchors(&mut scene.lines);
|
||||
}
|
||||
scene.layout_activations(p, &lane_x);
|
||||
}
|
||||
for (id, name, detail) in lanes {
|
||||
@@ -1355,6 +1497,7 @@ impl Scene {
|
||||
0,
|
||||
Line {
|
||||
id,
|
||||
role: SceneHitRole::LifelineGuide,
|
||||
points: vec![Point::new(x, 85.), Point::new(x, scene.size.height - 20.)],
|
||||
label: String::new(),
|
||||
label_at: Point::ORIGIN,
|
||||
@@ -1364,6 +1507,11 @@ impl Scene {
|
||||
}
|
||||
scene.boxes.push(BoxItem {
|
||||
id,
|
||||
role: if scenario {
|
||||
SceneHitRole::LifelineHeader
|
||||
} else {
|
||||
SceneHitRole::Box
|
||||
},
|
||||
rect: Rectangle::new(Point::new(x - 102., 15.), Size::new(204., 70.)),
|
||||
title: name,
|
||||
detail,
|
||||
@@ -1372,6 +1520,7 @@ impl Scene {
|
||||
}
|
||||
scene
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)] // Explicit complete plan replaces ambient geometry state.
|
||||
fn occurrences(
|
||||
&mut self,
|
||||
p: &Project,
|
||||
@@ -1380,6 +1529,7 @@ impl Scene {
|
||||
lanes: &BTreeMap<Id, f32>,
|
||||
y: &mut f32,
|
||||
depth: usize,
|
||||
plan: Option<&scenario_layout::Plan>,
|
||||
) {
|
||||
let steps = p
|
||||
.elements
|
||||
@@ -1419,6 +1569,9 @@ impl Scene {
|
||||
}
|
||||
#[cfg(test)]
|
||||
scenario_lifecycle::render::apply_test_row_offset(*id, y);
|
||||
if let Some(plan) = plan {
|
||||
*y = plan.anchors[id].y;
|
||||
}
|
||||
if ordered {
|
||||
let slot = scenario::Slot {
|
||||
parent: owner,
|
||||
@@ -1435,7 +1588,7 @@ impl Scene {
|
||||
OccurrenceBody::Fragment { operator, .. } => {
|
||||
let top = *y;
|
||||
*y += 55.;
|
||||
self.occurrences(p, *id, scenario, lanes, y, depth + 1);
|
||||
self.occurrences(p, *id, scenario, lanes, y, depth + 1, plan);
|
||||
let rect = Rectangle::new(
|
||||
Point::new(18. + depth as f32 * 10., top),
|
||||
Size::new(
|
||||
@@ -1443,10 +1596,12 @@ impl Scene {
|
||||
(*y - top).max(95.),
|
||||
),
|
||||
);
|
||||
let rect = plan.map_or(rect, |plan| plan.frames[id]);
|
||||
self.boxes.insert(
|
||||
0,
|
||||
BoxItem {
|
||||
id: *id,
|
||||
role: SceneHitRole::Box,
|
||||
rect,
|
||||
title: format!("{operator:?} · {}", e.name),
|
||||
detail: String::new(),
|
||||
@@ -1458,6 +1613,7 @@ impl Scene {
|
||||
OccurrenceBody::Branch { guard, .. } => {
|
||||
self.boxes.push(BoxItem {
|
||||
id: *id,
|
||||
role: SceneHitRole::Box,
|
||||
rect: Rectangle::new(
|
||||
Point::new(36. + depth as f32 * 10., *y),
|
||||
Size::new(self.size.width - 85. - depth as f32 * 20., 43.),
|
||||
@@ -1474,7 +1630,7 @@ impl Scene {
|
||||
marker: Some(Kind::Occurrence),
|
||||
});
|
||||
*y += if scenario { 90. } else { 62. };
|
||||
self.occurrences(p, *id, scenario, lanes, y, depth + 1);
|
||||
self.occurrences(p, *id, scenario, lanes, y, depth + 1, plan);
|
||||
}
|
||||
OccurrenceBody::Exchange {
|
||||
exchange,
|
||||
@@ -1502,7 +1658,7 @@ impl Scene {
|
||||
source.and_then(|id| lanes.get(&id)),
|
||||
target.and_then(|id| lanes.get(&id)),
|
||||
) {
|
||||
let points = if a == b {
|
||||
let points = if source == target {
|
||||
vec![
|
||||
Point::new(*a, *y),
|
||||
Point::new(a + 75., *y),
|
||||
@@ -1541,6 +1697,7 @@ impl Scene {
|
||||
}
|
||||
self.lines.push(Line {
|
||||
id: *id,
|
||||
role: SceneHitRole::MessageBody,
|
||||
points,
|
||||
label: format!("{}. {kind}{}", index + 1, message_label),
|
||||
label_at: Point::new(a.min(*b) + 12., *y - 26.),
|
||||
@@ -1549,6 +1706,7 @@ impl Scene {
|
||||
} else {
|
||||
self.boxes.push(BoxItem {
|
||||
id: *id,
|
||||
role: SceneHitRole::Box,
|
||||
rect: Rectangle::new(
|
||||
Point::new(65., *y - 20.),
|
||||
Size::new(self.size.width - 130., 65.),
|
||||
@@ -1571,6 +1729,7 @@ impl Scene {
|
||||
.unwrap_or(180.);
|
||||
self.boxes.push(BoxItem {
|
||||
id: *id,
|
||||
role: SceneHitRole::Box,
|
||||
rect: Rectangle::new(Point::new(x - 110., *y - 20.), Size::new(220., 70.)),
|
||||
title: e.name.clone(),
|
||||
detail: action
|
||||
@@ -1583,6 +1742,7 @@ impl Scene {
|
||||
OccurrenceBody::Control { kind } => {
|
||||
self.boxes.push(BoxItem {
|
||||
id: *id,
|
||||
role: SceneHitRole::Box,
|
||||
rect: Rectangle::new(Point::new(65., *y - 20.), Size::new(240., 70.)),
|
||||
title: format!("{kind:?} · {}", e.name),
|
||||
detail: "Contextual process control".into(),
|
||||
@@ -1593,6 +1753,7 @@ impl Scene {
|
||||
OccurrenceBody::SequenceReference { sequence } => {
|
||||
self.boxes.push(BoxItem {
|
||||
id: *id,
|
||||
role: SceneHitRole::Box,
|
||||
rect: Rectangle::new(Point::new(65., *y - 20.), Size::new(280., 70.)),
|
||||
title: e.name.clone(),
|
||||
detail: sequence
|
||||
@@ -1605,6 +1766,7 @@ impl Scene {
|
||||
OccurrenceBody::ScenarioUse { scenario, bindings } => {
|
||||
self.boxes.push(BoxItem {
|
||||
id: *id,
|
||||
role: SceneHitRole::Box,
|
||||
rect: Rectangle::new(
|
||||
Point::new(65., *y - 20.),
|
||||
Size::new(self.size.width - 130., 70.),
|
||||
@@ -1627,6 +1789,10 @@ impl Scene {
|
||||
}
|
||||
if ordered {
|
||||
self.sequence_bounds.insert(owner, (sequence_top, *y - 42.));
|
||||
if let Some(plan) = plan {
|
||||
self.sequence_bounds.insert(owner, plan.scopes[&owner]);
|
||||
*y = plan.scopes[&owner].1 + 42.;
|
||||
}
|
||||
let slot = scenario::Slot {
|
||||
parent: owner,
|
||||
before: None,
|
||||
@@ -1666,11 +1832,21 @@ impl Scene {
|
||||
))
|
||||
}
|
||||
fn hit(&self, point: Point) -> Option<Id> {
|
||||
self.hit_item(point).map(|hit| hit.id)
|
||||
}
|
||||
|
||||
/// Preserve the semantic selection order and tolerance while retaining the
|
||||
/// primitive's identity. Future spacing admission must also validate the
|
||||
/// saved view, owner and core occurrence kind; a hit alone grants no edit.
|
||||
fn hit_item(&self, point: Point) -> Option<SceneHit> {
|
||||
if !point.x.is_finite() || !point.y.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if let Some(id) = self.lifecycle.marker_at(point) {
|
||||
return Some(id);
|
||||
return Some(SceneHit {
|
||||
id,
|
||||
role: SceneHitRole::LifecycleMarker,
|
||||
});
|
||||
}
|
||||
self.boxes
|
||||
.iter()
|
||||
@@ -1680,7 +1856,10 @@ impl Scene {
|
||||
&& b.rect.contains(point)
|
||||
&& (b.marker != Some(Kind::Occurrence) || point.y < b.rect.y + 38.)
|
||||
})
|
||||
.map(|b| b.id)
|
||||
.map(|b| SceneHit {
|
||||
id: b.id,
|
||||
role: b.role,
|
||||
})
|
||||
.or_else(|| {
|
||||
self.lines
|
||||
.iter()
|
||||
@@ -1690,26 +1869,51 @@ impl Scene {
|
||||
crate::diagram::distance_to_segment(point, p[0], p[1]) < 1.5
|
||||
})
|
||||
})
|
||||
.map(|line| line.id)
|
||||
.map(|line| SceneHit {
|
||||
id: line.id,
|
||||
role: line.role,
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
self.activations
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|b| b.rect.contains(point))
|
||||
.map(|b| b.call)
|
||||
.map(|b| SceneHit {
|
||||
id: b.call,
|
||||
role: SceneHitRole::Activation,
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
self.lines
|
||||
.iter()
|
||||
.find(|line| !line.label.is_empty() && line.label_bounds().contains(point))
|
||||
.map(|line| line.id)
|
||||
.map(|line| SceneHit {
|
||||
id: line.id,
|
||||
role: if line.role == SceneHitRole::MessageBody {
|
||||
SceneHitRole::MessageLabel
|
||||
} else {
|
||||
SceneHitRole::OtherLine
|
||||
},
|
||||
})
|
||||
})
|
||||
// The generous message tolerance must not steal the exposed
|
||||
// shoulder of another call's execution when the view is zoomed out.
|
||||
.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.lifecycle
|
||||
.lane_at(point, self.scale)
|
||||
.map(|id| SceneHit {
|
||||
id,
|
||||
role: SceneHitRole::LifelineGuide,
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
self.lane_at(point).map(|id| SceneHit {
|
||||
id,
|
||||
role: SceneHitRole::LifelineGuide,
|
||||
})
|
||||
})
|
||||
.or_else(|| {
|
||||
self.lines
|
||||
.iter()
|
||||
@@ -1720,11 +1924,14 @@ impl Scene {
|
||||
< 8. / self.scale
|
||||
})
|
||||
})
|
||||
.map(|line| line.id)
|
||||
.map(|line| SceneHit {
|
||||
id: line.id,
|
||||
role: line.role,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn near_sequence_item(&self, point: Point) -> Option<Id> {
|
||||
fn near_sequence_item(&self, point: Point) -> Option<SceneHit> {
|
||||
if !self.scale.is_finite() || self.scale <= 0. {
|
||||
return None;
|
||||
}
|
||||
@@ -1738,17 +1945,32 @@ impl Scene {
|
||||
.windows(2)
|
||||
.map(|p| crate::diagram::distance_to_segment(point, p[0], p[1]))
|
||||
.fold(f32::INFINITY, f32::min);
|
||||
(line.id, distance)
|
||||
(
|
||||
SceneHit {
|
||||
id: line.id,
|
||||
role: line.role,
|
||||
},
|
||||
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)
|
||||
.map(|(id, distance)| {
|
||||
(
|
||||
SceneHit {
|
||||
id,
|
||||
role: SceneHitRole::LifecycleMarker,
|
||||
},
|
||||
distance,
|
||||
)
|
||||
})
|
||||
.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)
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.id.cmp(&b.0.id)))
|
||||
.map(|(hit, _)| hit)
|
||||
}
|
||||
}
|
||||
impl canvas::Program<Message> for Scene {
|
||||
@@ -1776,15 +1998,33 @@ impl canvas::Program<Message> for Scene {
|
||||
// captured insertion slot belongs to the previous view, even if the
|
||||
// pointer itself did not move and the semantic context is unchanged.
|
||||
if state.bounds.is_some_and(|previous| previous != bounds) {
|
||||
state.cancel();
|
||||
let cancelled = state.cancel_spacing();
|
||||
state.pointer = None;
|
||||
state.bounds = Some(bounds);
|
||||
if cancelled.is_some() {
|
||||
return cancelled;
|
||||
}
|
||||
}
|
||||
state.bounds = Some(bounds);
|
||||
if !editable {
|
||||
state.cancel();
|
||||
if (!editable || !self.coordinate_input)
|
||||
&& let Some(cancelled) = state.cancel_spacing()
|
||||
{
|
||||
return Some(cancelled);
|
||||
}
|
||||
// The viewport masks input outside its visible canvas bounds.
|
||||
let visible = cursor.position().is_some();
|
||||
if !visible
|
||||
&& matches!(state.press, Some(SequencePress::Spacing { .. }))
|
||||
&& matches!(
|
||||
event,
|
||||
canvas::Event::Mouse(
|
||||
mouse::Event::CursorMoved { .. }
|
||||
| mouse::Event::ButtonReleased(mouse::Button::Left)
|
||||
)
|
||||
)
|
||||
{
|
||||
return state.cancel_spacing();
|
||||
}
|
||||
if !visible
|
||||
&& matches!(
|
||||
event,
|
||||
@@ -1803,7 +2043,7 @@ impl canvas::Program<Message> for Scene {
|
||||
| SequencePress::Reorder { start, moved, .. } => {
|
||||
*moved |= position.distance(*start) >= 3.
|
||||
}
|
||||
SequencePress::Occurrence { .. } => {}
|
||||
SequencePress::Occurrence { .. } | SequencePress::Spacing { .. } => {}
|
||||
}
|
||||
}
|
||||
mouse::Cursor::Available(*position)
|
||||
@@ -1812,9 +2052,9 @@ impl canvas::Program<Message> for Scene {
|
||||
| canvas::Event::Window(iced::window::Event::Resized(_))
|
||||
| canvas::Event::Mouse(mouse::Event::CursorLeft)
|
||||
| canvas::Event::Window(iced::window::Event::Unfocused) => {
|
||||
state.cancel();
|
||||
let cancelled = state.cancel_spacing();
|
||||
state.pointer = None;
|
||||
return Some(Action::request_redraw());
|
||||
return cancelled.or_else(|| Some(Action::request_redraw()));
|
||||
}
|
||||
canvas::Event::Mouse(
|
||||
mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_),
|
||||
@@ -1850,8 +2090,9 @@ impl canvas::Program<Message> for Scene {
|
||||
key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape),
|
||||
..
|
||||
}) if state.source.is_some() || state.press.is_some() => {
|
||||
state.cancel();
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
return state
|
||||
.cancel_spacing()
|
||||
.or_else(|| Some(Action::request_redraw().and_capture()));
|
||||
}
|
||||
_ => cursor,
|
||||
};
|
||||
@@ -1871,6 +2112,14 @@ impl canvas::Program<Message> for Scene {
|
||||
if !editable {
|
||||
return None;
|
||||
}
|
||||
if !self.coordinate_input {
|
||||
return self.hit(point).map(|id| {
|
||||
Action::publish(Message::ScenarioCanvas(CanvasEvent::Select(
|
||||
context, id,
|
||||
)))
|
||||
.and_capture()
|
||||
});
|
||||
}
|
||||
if let CanvasTool::Message(_) = tool {
|
||||
let first = state.source.is_none();
|
||||
let (source, slot) = state.source.or_else(|| {
|
||||
@@ -1897,6 +2146,48 @@ impl canvas::Program<Message> for Scene {
|
||||
});
|
||||
return Some(Action::request_redraw().and_capture());
|
||||
}
|
||||
if matches!(
|
||||
context.representation,
|
||||
scenario_layout::Representation::Saved { .. }
|
||||
) {
|
||||
let hit = self.hit_item(point)?;
|
||||
let target = match hit.role {
|
||||
SceneHitRole::LifelineHeader => {
|
||||
Some(scenario::spacing::Target::Lane(hit.id))
|
||||
}
|
||||
SceneHitRole::MessageBody | SceneHitRole::MessageLabel => {
|
||||
Some(scenario::spacing::Target::Message(hit.id))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some((target, stamp)) = target.zip(self.spacing_stamp)
|
||||
&& stamp.context == context
|
||||
{
|
||||
let token = Id::new_v4();
|
||||
state.press = Some(SequencePress::Spacing { stamp, token });
|
||||
return Some(
|
||||
Action::publish(Message::ScenarioCanvas(CanvasEvent::Spacing(
|
||||
scenario::spacing::Event::Begin {
|
||||
stamp,
|
||||
token,
|
||||
target,
|
||||
press_screen: cursor.position()?,
|
||||
canvas_bounds: bounds,
|
||||
scale: self.scale,
|
||||
pan: self.camera_pan,
|
||||
},
|
||||
)))
|
||||
.and_capture(),
|
||||
);
|
||||
}
|
||||
// Saved spacing never silently changes the shared Scenario order.
|
||||
return Some(
|
||||
Action::publish(Message::ScenarioCanvas(CanvasEvent::Select(
|
||||
context, hit.id,
|
||||
)))
|
||||
.and_capture(),
|
||||
);
|
||||
}
|
||||
if let Some(occurrence) = self
|
||||
.hit(point)
|
||||
.filter(|id| self.rows.contains_key(id))
|
||||
@@ -1926,6 +2217,22 @@ impl canvas::Program<Message> for Scene {
|
||||
let press = state.press.take()?;
|
||||
let context = context?;
|
||||
match press {
|
||||
SequencePress::Spacing { stamp, token } => {
|
||||
state.source = None;
|
||||
let event = if world.is_some() {
|
||||
scenario::spacing::Event::Finish {
|
||||
stamp,
|
||||
token,
|
||||
screen: cursor.position()?,
|
||||
}
|
||||
} else {
|
||||
scenario::spacing::Event::Cancel { stamp, token }
|
||||
};
|
||||
return Some(
|
||||
Action::publish(Message::ScenarioCanvas(CanvasEvent::Spacing(event)))
|
||||
.and_capture(),
|
||||
);
|
||||
}
|
||||
SequencePress::Occurrence {
|
||||
tool: captured,
|
||||
slot,
|
||||
@@ -2017,6 +2324,18 @@ impl canvas::Program<Message> for Scene {
|
||||
Some(Action::request_redraw().and_capture())
|
||||
}
|
||||
canvas::Event::Mouse(mouse::Event::CursorMoved { .. }) if context.is_some() => {
|
||||
if let Some(SequencePress::Spacing { stamp, token }) = state.press {
|
||||
return Some(
|
||||
Action::publish(Message::ScenarioCanvas(CanvasEvent::Spacing(
|
||||
scenario::spacing::Event::Move {
|
||||
stamp,
|
||||
token,
|
||||
screen: cursor.position()?,
|
||||
},
|
||||
)))
|
||||
.and_capture(),
|
||||
);
|
||||
}
|
||||
Some(Action::request_redraw())
|
||||
}
|
||||
_ => None,
|
||||
@@ -2198,7 +2517,10 @@ impl canvas::Program<Message> for Scene {
|
||||
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 active = editable
|
||||
&& self.coordinate_input
|
||||
&& state.context == Some(context)
|
||||
&& state.tool == tool;
|
||||
let point = cursor.position_in(bounds).map(|p| {
|
||||
Point::new(
|
||||
(p.x - self.camera_pan.x) / self.scale,
|
||||
@@ -2214,7 +2536,12 @@ impl canvas::Program<Message> for Scene {
|
||||
}
|
||||
});
|
||||
if editable
|
||||
&& self.coordinate_input
|
||||
&& tool == scenario::CanvasTool::Select
|
||||
&& matches!(
|
||||
context.representation,
|
||||
scenario_layout::Representation::Derived { .. }
|
||||
)
|
||||
&& let Some(handle) = self.reorder_handle()
|
||||
{
|
||||
f.fill_rectangle(handle.position(), handle.size(), style::TEAL);
|
||||
|
||||
Reference in New Issue
Block a user