Add exact State multiselection, atomic movement and placement Hide

Preserve the desktop-2 captured source (1286 passed, 6 existing ignored). Three menu payload blobs are staged from that verified capture without replacing working files; the strict-lint size correction follows in a separate commit. Native acceptance remains pending.
This commit is contained in:
2026-09-23 03:37:01 -07:00
parent 7e348d967d
commit 2a3b9ba822
21 changed files with 3833 additions and 95 deletions
+120 -24
View File
@@ -49,7 +49,12 @@ pub(crate) fn placements(snapshot: &Snapshot) -> Option<std::collections::BTreeS
.collect()
}
fn visible(view: &projection::ProjectedView, target: &Target) -> bool {
fn visible(
project: &Project,
diagram: Id,
view: &projection::ProjectedView,
target: &Target,
) -> bool {
match target {
Target::Placement(id) => view.nodes.iter().any(|n| n.id == *id),
Target::Port { port, placement } => view
@@ -57,6 +62,12 @@ fn visible(view: &projection::ProjectedView, target: &Target) -> bool {
.iter()
.any(|p| p.element == *port && p.owner == *placement),
Target::Edge(id) => view.edges.iter().any(|e| e.id == *id),
Target::StateRow {
placement,
relation,
} => crate::behavior::state_canvas::visible_row_target(
project, diagram, view, *placement, *relation,
),
_ => false,
}
}
@@ -70,14 +81,41 @@ impl App {
self.active_diagram,
self.selected,
self.selected_node,
(self.page == Page::Behavior).then(|| self.state_view_identity()),
(self.page == Page::Behavior).then_some(self.behavior.diagram.placement),
)
}
pub(crate) fn multiselect_context(&self) -> Option<Context> {
// Other surfaces retain their existing adapters until their typed group
// operations are implemented; never reuse an Architecture selection there.
(self.page == Page::Diagram)
.then(|| self.route_context().map(context))
.flatten()
match self.page {
Page::Diagram => self.route_context().map(context),
Page::Behavior => {
let current = self.state_context()?;
crate::behavior::state_edit::validate_context(self.project(), current, current)
.ok()?;
Some(context(current))
}
_ => None,
}
}
pub(crate) fn multiselect_target_visible(&self, target: &Target) -> bool {
let Some(Context {
scope:
Scope {
surface: Surface::Saved(diagram),
..
},
..
}) = self.multiselect_context()
else {
return false;
};
let view = projection::project_view_with_index(
self.project(),
diagram,
self.document.view_index(),
);
visible(self.project(), diagram, &view, target)
}
pub(crate) fn multiselect_snapshot(&self) -> Option<Snapshot> {
@@ -100,14 +138,44 @@ impl App {
}
}
fn legacy_target(&self, view: &projection::ProjectedView) -> Option<Target> {
if let Some(id) = self.selected_node
fn legacy_target(&self, diagram: Id, view: &projection::ProjectedView) -> Option<Target> {
let placement = if self.page == Page::Behavior {
self.behavior.diagram.placement
} else {
self.selected_node
};
if let Some(id) = placement
&& view.nodes.iter().any(|node| {
node.id == id && (node.element == self.selected || node.id == self.selected)
})
{
return Some(Target::Placement(id));
}
if self.page == Page::Behavior {
// Keep a row's exact occurrence while inspecting its relation.
// Its owner placement is never a node-inspection or Hide target.
if let Some(target @ Target::StateRow { relation, .. }) =
self.diagram_selection.as_ref().and_then(|s| s.primary())
&& *relation == self.selected
&& visible(self.project(), diagram, view, target)
{
return Some(target.clone());
}
if let Some(node) = view.nodes.iter().find(|node| {
crate::behavior::state_canvas::visible_row_target(
self.project(),
diagram,
view,
node.id,
self.selected,
)
}) {
return Some(Target::StateRow {
placement: node.id,
relation: self.selected,
});
}
}
if let Some(port) = view.ports.iter().find(|port| port.element == self.selected) {
return Some(Target::Port {
port: port.element,
@@ -133,28 +201,39 @@ impl App {
diagram,
self.document.view_index(),
);
let legacy = self.legacy_target(&view);
let legacy = self.legacy_target(diagram, &view);
let lost_primary = self
.diagram_selection
.as_ref()
.filter(|s| s.scope() == current.scope)
.and_then(|s| s.primary())
.is_some_and(|target| !visible(&view, target));
.is_some_and(|target| !visible(self.project(), diagram, &view, target));
let primary_placement = if self.page == Page::Behavior {
self.behavior.diagram.placement
} else {
self.selected_node
};
let held_primary = self
.diagram_selection
.as_ref()
.filter(|s| s.scope() == current.scope)
.and_then(|s| s.primary())
.is_some_and(|target| match target {
Target::Placement(id) => self.selected_node == Some(*id),
Target::Placement(id) => primary_placement == Some(*id),
Target::Port { port, .. } => self.selected == *port,
Target::Edge(id) => self.selected == *id,
Target::StateRow { relation, .. } => {
self.selected == *relation && primary_placement.is_none()
}
_ => false,
});
let project = self.document.project();
let selection = self
.diagram_selection
.get_or_insert_with(|| Selection::new(current.scope));
selection.reconcile(current.scope, |target| visible(&view, target));
selection.reconcile(current.scope, |target| {
visible(project, diagram, &view, target)
});
// Navigation/property selection remains explicit. A removed exact target
// is never rebound through its semantic identity to a different placement.
if held_primary || lost_primary {
@@ -163,7 +242,7 @@ impl App {
}
} else if selection.primary() != legacy.as_ref() {
if let Some(target) = legacy {
selection.replace(target, |target| visible(&view, target));
selection.replace(target, |target| visible(project, diagram, &view, target));
} else {
selection.clear();
}
@@ -171,26 +250,39 @@ impl App {
}
fn inspect_multiselect_primary(&mut self) {
let diagram = self
.multiselect_context()
.and_then(|c| match c.scope.surface {
Surface::Saved(diagram) => Some(diagram),
_ => None,
});
let primary = self
.diagram_selection
.as_ref()
.and_then(|s| s.primary())
.cloned();
self.selected_node = None;
if self.page == Page::Behavior {
self.behavior.diagram.placement = None;
}
match primary {
Some(Target::Placement(id)) => {
if let Some(node) = self
.active_diagram
if let Some(node) = diagram
.and_then(|d| self.project().diagrams.get(&d))
.and_then(|d| d.nodes.get(&id))
{
self.selected = node.element;
self.selected_node = Some(id);
if self.page == Page::Behavior {
self.behavior.diagram.placement = Some(id);
} else {
self.selected_node = Some(id);
}
}
}
Some(Target::Port { port, .. }) => self.selected = port,
Some(Target::Edge(id)) => self.selected = id,
_ => self.selected = self.active_diagram.unwrap_or(self.project().id),
Some(Target::StateRow { relation, .. }) => self.selected = relation,
_ => self.selected = diagram.unwrap_or(self.project().id),
}
self.selected_bend = None;
self.selected_attachment = None;
@@ -204,6 +296,8 @@ impl App {
if !diagram_removal::enabled(self)
|| self.architecture.is_some()
|| self.connection.is_some()
|| (self.page == Page::Behavior
&& self.behavior.diagram.tool != crate::behavior::state_edit::Tool::Select)
{
self.status =
"Finish or cancel the active edit before changing the diagram selection.".into();
@@ -220,22 +314,24 @@ impl App {
);
match event {
Event::Collapse(_, target) => {
if !visible(&view, &target) {
if !visible(self.project(), diagram, &view, &target) {
return;
}
let project = self.document.project();
self.diagram_selection
.as_mut()
.unwrap()
.replace(target, |target| visible(&view, target));
.replace(target, |target| visible(project, diagram, &view, target));
self.inspect_multiselect_primary();
}
Event::Choose(_, target, choice) => {
if !visible(&view, &target) {
if !visible(self.project(), diagram, &view, &target) {
return;
}
self.reconcile_multiselect();
let project = self.document.project();
let selection = self.diagram_selection.as_mut().unwrap();
let valid = |target: &Target| visible(&view, target);
let valid = |target: &Target| visible(project, diagram, &view, target);
match choice {
Choice::Replace => {
selection.replace(target, valid);
@@ -264,13 +360,13 @@ impl App {
Event::Refuse(_, reason) => self.status = reason,
Event::Move(snapshot, delta) => {
let Some(ids) = placements(&snapshot) else {
self.status = "Move requires only placed elements. Remove ports and connections from the selection.".into();
self.status = "Move requires only placed elements. Remove ports, rows and connections from the selection.".into();
return;
};
if !snapshot
.targets()
.iter()
.all(|target| visible(&view, target))
.all(|target| visible(self.project(), diagram, &view, target))
{
return;
}
@@ -318,7 +414,7 @@ impl App {
if !snapshot
.targets()
.iter()
.all(|target| visible(&view, target))
.all(|target| visible(self.project(), diagram, &view, target))
{
return;
}