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
+62 -12
View File
@@ -12,6 +12,7 @@ pub struct Request {
pub anchor: Point,
pub object: Id,
pub placement: Option<Id>,
pub selection_target: Option<crate::diagram_usage::Target>,
pub bend: Option<usize>,
pub route_point: Option<Position>,
pub route_segment: Option<(usize, Position)>,
@@ -25,6 +26,7 @@ impl Request {
anchor,
object,
placement: None,
selection_target: None,
bend: None,
route_point: None,
route_segment: None,
@@ -55,6 +57,7 @@ pub struct Opened {
impl Opened {
pub fn new(app: &App, request: Request, token: u64) -> Option<Self> {
if !app.project().contains(request.object)
|| !target_matches(app, &request)
|| request
.canvas
.is_some_and(|c| Some(c) != current_canvas(app))
@@ -80,6 +83,7 @@ impl Opened {
&& self.layer == app.layer
&& self.diagram == current_diagram(app)
&& app.project().contains(self.request.object)
&& target_matches(app, &self.request)
&& self
.request
.canvas
@@ -92,6 +96,32 @@ impl Opened {
}
}
fn target_matches(app: &App, request: &Request) -> bool {
use crate::diagram_usage::Target;
let Some(target) = &request.selection_target else {
return true;
};
if request.canvas.is_none() || !app.multiselect_target_visible(target) {
return false;
}
match target {
Target::Placement(id) => {
request.placement == Some(*id)
&& request
.canvas
.and_then(|context| app.project().diagrams.get(&context.diagram))
.and_then(|diagram| diagram.nodes.get(id))
.is_some_and(|node| node.element == request.object || node.id == request.object)
}
Target::Port { port, .. } => request.placement.is_none() && *port == request.object,
Target::Edge(id) => request.placement.is_none() && *id == request.object,
Target::StateRow { relation, .. } => {
request.placement.is_none() && *relation == request.object
}
_ => false,
}
}
fn current_canvas(app: &App) -> Option<route_edit::Context> {
if app.page == Page::Behavior {
app.state_context()
@@ -126,7 +156,22 @@ pub fn trigger(app: &App) -> iced::Element<'_, Message> {
request.canvas = app.route_context();
} else if app.page == Page::Behavior {
request.canvas = app.state_context();
if let Some(context) = request.canvas
if let Some(target) = app.diagram_selection.as_ref().and_then(|s| s.primary()) {
request.selection_target = Some(target.clone());
request.placement = match target {
crate::diagram_usage::Target::Placement(id) => {
if let Some(node) = request
.canvas
.and_then(|c| app.project().diagrams.get(&c.diagram))
.and_then(|d| d.nodes.get(id))
{
request.object = node.element;
}
Some(*id)
}
_ => None,
};
} else if let Some(context) = request.canvas
&& let Some(node) = app.project().diagrams[&context.diagram]
.nodes
.values()
@@ -187,24 +232,20 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
on_close: Message::Menu(Event::Close),
});
}
if app.page == Page::Behavior
&& let Some(menu) = crate::behavior::state_menu::popup(app, opened, clean)
{
return Some(menu);
}
let mut entries = vec![Entry::title(crate::views::short(p.named(id), 38))];
if app.page == Page::Diagram
&& request.canvas.is_some()
if request.canvas.is_some()
&& let Some(context) = app.multiselect_context()
{
use crate::app::multiselect::{Choice, Event as SelectionEvent};
use crate::diagram_usage::Target;
let view = app
.active_diagram
let view = request
.canvas
.map(|context| context.diagram)
.map(|d| projection::project_view_with_index(p, d, app.document.view_index()));
let target = request
.placement
.map(Target::Placement)
.selection_target
.clone()
.or_else(|| request.placement.map(Target::Placement))
.or_else(|| {
view.as_ref()?
.ports
@@ -287,6 +328,15 @@ pub fn popup(app: &App) -> Option<Menu<Message>> {
entries.push(Entry::separator());
}
}
if app.page == Page::Behavior
&& let Some(mut menu) = crate::behavior::state_menu::popup(app, opened, clean)
{
if entries.len() > 1 {
entries.extend(menu.entries.into_iter().skip(1));
menu.entries = entries;
}
return Some(menu);
}
if !clean {
entries.push(Entry::title("Apply or revert pending edits first"));
}