Files
ARCADIA-Studio/crates/oa-desktop/src/behavior/scenario/usage.rs
T

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;