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
+51 -4
View File
@@ -41,7 +41,10 @@ pub fn diagram(app: &App, machine: Id) -> Option<Id> {
}
fn event(e: Event) -> Message {
Message::StateDiagram(e)
match e {
Event::Selection(selection) => Message::DiagramSelection(selection),
other => Message::StateDiagram(other),
}
}
pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Element<'a, Message> {
@@ -143,8 +146,9 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem
.collect::<Vec<_>>();
// Keep the optional picker in its own stable slot. Inserting it as a
// sibling before the canvas would replace the positional Iced focus tree.
let missing_spacing = if missing.is_empty() { 0.0_f32 } else { 8.0 };
let mut missing_controls = Column::new();
// Reserve the picker height even when all elements are placed. Group Hide
// and Undo must not resize the canvas and silently refit its camera.
let mut missing_controls = Column::new().height(34);
if !missing.is_empty() {
let position = Position {
x: (48. - state.pan.x) / state.zoom,
@@ -169,6 +173,7 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem
diagram: id,
context,
selected: app.selected,
selection: app.diagram_selection.as_ref(),
navigation_focus: app
.diagram_focus(crate::diagram_usage::Surface::Saved(id))
.map(|usage| usage.bounds),
@@ -183,6 +188,7 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem
out = out.push(
column![
missing_controls,
selection_controls(app),
crate::diagram::focus::wrap(
canvas.map(event),
context,
@@ -191,7 +197,7 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem
Message::DiagramTraverse,
)
]
.spacing(missing_spacing)
.spacing(8)
.height(Fill),
);
let base: Element<'_, Message> = container(out.spacing(8))
@@ -202,6 +208,47 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem
with_form(app, machine, base)
}
fn selection_controls(app: &App) -> Element<'_, Message> {
use crate::app::multiselect::{Event as SelectionEvent, placements};
let snapshot = app.multiselect_snapshot();
let count = snapshot.as_ref().map_or(0, |s| s.targets().len());
let mut controls = row![label(format!("{count} selected"))]
.spacing(7)
.align_y(iced::Center)
.height(32);
if let Some(snapshot) = snapshot.filter(|s| s.targets().len() > 1) {
if placements(&snapshot).is_some() {
controls = controls.push(action(
"Hide selected",
Message::DiagramSelection(SelectionEvent::Hide(snapshot.clone())),
));
}
controls = controls
.push(action(
"Clear selection",
Message::DiagramSelection(SelectionEvent::Clear(snapshot.context())),
))
.push(
label(format!(
"Properties edit only {}",
crate::views::short(app.project().named(app.selected), 18)
))
.wrapping(iced::widget::text::Wrapping::None),
);
}
let hint = if app.behavior.diagram.tool == Tool::Select {
"Shift+click adds or removes from selection"
} else {
"Use Select / move to select multiple elements"
};
column![
controls,
label(hint).wrapping(iced::widget::text::Wrapping::None)
]
.height(56)
.into()
}
fn transition_details(machine: Id, selected: Id) -> Element<'static, Message> {
action(
"New transition with details…",