//! 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::>().join(" "); let normalized_prefix = prefix.split_whitespace().collect::>().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::(); start < retained } fn usages(p: &Project, owner: Id, subject: Id, scene: &Scene) -> Vec { 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 { 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;