208 lines
7.5 KiB
Rust
208 lines
7.5 KiB
Rust
//! Navigation is derived from the same opaque-reference scene that is painted.
|
|
use super::*;
|
|
use crate::diagram_usage::{Surface, Target, Usage};
|
|
|
|
/// The renderer collapses whitespace before shortening. Require at least one
|
|
/// actual character of this field, rather than an ellipsis at its start.
|
|
fn label_field_visible(full: &str, prefix: &str, value: &str) -> bool {
|
|
if value.trim().is_empty() {
|
|
return false;
|
|
}
|
|
let normalized = full.split_whitespace().collect::<Vec<_>>().join(" ");
|
|
let normalized_prefix = prefix.split_whitespace().collect::<Vec<_>>().join(" ");
|
|
let start = normalized_prefix.len()
|
|
+ usize::from(prefix.ends_with(char::is_whitespace) && !normalized_prefix.is_empty());
|
|
let painted = crate::views::short(full, 32);
|
|
let retained = normalized
|
|
.chars()
|
|
.zip(painted.chars())
|
|
.take_while(|(a, b)| a == b)
|
|
.map(|(c, _)| c.len_utf8())
|
|
.sum::<usize>();
|
|
start < retained
|
|
}
|
|
|
|
fn usages(p: &Project, owner: Id, subject: Id, scene: &Scene) -> Vec<Usage> {
|
|
let mut result = Vec::new();
|
|
let mut add = |id, bounds, role: &str| {
|
|
// Activation bars share the occurrence identity with a labelled message;
|
|
// they must not inflate its target count.
|
|
if !result
|
|
.iter()
|
|
.any(|u: &Usage| u.target == Target::ScenarioObject(id))
|
|
{
|
|
result.push(Usage {
|
|
subject,
|
|
surface: Surface::Scenario(owner),
|
|
target: Target::ScenarioObject(id),
|
|
role: role.into(),
|
|
bounds,
|
|
});
|
|
}
|
|
};
|
|
for item in scene.boxes.iter().filter(|item| !item.title.is_empty()) {
|
|
let Some(element) = p.elements.get(&item.id) else {
|
|
continue;
|
|
};
|
|
if subject == item.id {
|
|
// A fragment's selectable header is useful even when its frame is
|
|
// much larger than the viewport.
|
|
let mut bounds = item.rect;
|
|
if item.marker == Some(Kind::Occurrence) {
|
|
bounds.height = bounds.height.min(38.);
|
|
}
|
|
add(item.id, bounds, "scenario object");
|
|
}
|
|
let represented = match element.shared() {
|
|
Some(SharedBody::Lifeline { represented }) => represented.map(|id| (id, "lifeline")),
|
|
Some(SharedBody::Occurrence { body, .. }) => match body {
|
|
OccurrenceBody::Action { action, .. } => action.map(|id| (id, "action occurrence")),
|
|
OccurrenceBody::State { state, .. } => state.map(|id| (id, "state observation")),
|
|
OccurrenceBody::SequenceReference { sequence } => {
|
|
sequence.map(|id| (id, "process reference"))
|
|
}
|
|
OccurrenceBody::ScenarioUse { scenario, .. } => {
|
|
scenario.map(|id| (id, "scenario reference"))
|
|
}
|
|
// An incomplete message fallback box only paints the occurrence
|
|
// name and a repair hint, not its exchange or operation.
|
|
_ => None,
|
|
},
|
|
_ => None,
|
|
};
|
|
if let Some((id, role)) = represented.filter(|(id, _)| *id == subject) {
|
|
let _ = id;
|
|
add(item.id, item.rect, role);
|
|
}
|
|
}
|
|
for line in scene.lines.iter().filter(|line| !line.label.is_empty()) {
|
|
let Some(element) = p.elements.get(&line.id) else {
|
|
continue;
|
|
};
|
|
let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { exchange, .. },
|
|
..
|
|
}) = element.shared()
|
|
else {
|
|
continue;
|
|
};
|
|
let bounds = line.label_bounds();
|
|
if subject == line.id || *exchange == Some(subject) {
|
|
add(line.id, bounds, "message occurrence");
|
|
}
|
|
// Replies derive their operation from the exact call, matching the
|
|
// operation label formatter rather than scanning raw reference fields.
|
|
if let Ok(operations::OperationResolution::Selected(operation)) =
|
|
operations::resolve(p, line.id)
|
|
&& let Some(label) = crate::data_values::operation_label(p, element)
|
|
&& let Some(prefix) = line.label.strip_suffix(&label)
|
|
{
|
|
let allocation = &p.relations[&operation.allocation];
|
|
let operation_prefix = if matches!(
|
|
element.shared(),
|
|
Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange {
|
|
message_kind: MessageKind::Reply,
|
|
..
|
|
},
|
|
..
|
|
})
|
|
) {
|
|
"Reply operation from Call: "
|
|
} else {
|
|
"Operation: "
|
|
};
|
|
let fields = [
|
|
(allocation.source(), format!("{prefix}{operation_prefix}")),
|
|
(
|
|
allocation.target(),
|
|
format!(
|
|
"{prefix}{operation_prefix}{} / ",
|
|
p.named(allocation.source())
|
|
),
|
|
),
|
|
(
|
|
allocation.id,
|
|
format!(
|
|
"{prefix}{operation_prefix}{} / {} · ",
|
|
p.named(allocation.source()),
|
|
p.named(allocation.target())
|
|
),
|
|
),
|
|
];
|
|
for (id, prefix) in fields {
|
|
if id == subject && label_field_visible(&line.label, &prefix, p.named(id)) {
|
|
add(line.id, bounds, "operation label");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if subject == owner {
|
|
result.insert(
|
|
0,
|
|
Usage {
|
|
subject,
|
|
surface: Surface::Scenario(owner),
|
|
target: Target::Whole,
|
|
role: "scenario".into(),
|
|
bounds: Rectangle::new(Point::ORIGIN, scene.size),
|
|
},
|
|
);
|
|
}
|
|
result
|
|
}
|
|
|
|
impl State {
|
|
pub(crate) fn diagram_usages(&self, p: &Project, owner: Id, subject: Id) -> Vec<Usage> {
|
|
if !p
|
|
.elements
|
|
.get(&owner)
|
|
.is_some_and(|e| e.kind() == Kind::Scenario)
|
|
{
|
|
return vec![];
|
|
}
|
|
usages(p, owner, subject, &Scene::sequence(p, owner, subject))
|
|
}
|
|
|
|
pub(crate) fn reveal_usage(
|
|
&mut self,
|
|
p: &Project,
|
|
document: Id,
|
|
owner: Id,
|
|
target: &Target,
|
|
) -> bool {
|
|
if self.draft.is_some()
|
|
|| !p
|
|
.elements
|
|
.get(&owner)
|
|
.is_some_and(|e| e.kind() == Kind::Scenario)
|
|
{
|
|
return false;
|
|
}
|
|
let id = match target {
|
|
Target::Whole => owner,
|
|
Target::ScenarioObject(id) => *id,
|
|
_ => return false,
|
|
};
|
|
let Some(usage) = self
|
|
.diagram_usages(p, owner, id)
|
|
.into_iter()
|
|
.find(|u| &u.target == target)
|
|
else {
|
|
return false;
|
|
};
|
|
// Preserve an existing mounted viewport across enter's owner reset.
|
|
let viewport = self.presentation.viewport.unwrap_or(Size::new(500., 300.));
|
|
self.enter(document, owner);
|
|
self.presentation.viewport = Some(viewport);
|
|
self.canvas_tool = Some((owner, CanvasTool::Select));
|
|
let (zoom, pan) = crate::diagram_usage::camera_for(target, usage.bounds, viewport);
|
|
self.presentation.zoom(Some(zoom));
|
|
self.presentation.pan = pan;
|
|
true
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|