Keep state canvas focus tree stable when placement controls change

This commit is contained in:
2026-09-12 07:19:23 -07:00
parent b25065c442
commit e2c027e54c
2 changed files with 51 additions and 8 deletions
@@ -1046,6 +1046,16 @@ async fn mounted_state_delete_uses_state_placement_and_preserves_architecture_pl
assert_eq!(app.selected, state);
assert_eq!(app.behavior.diagram.placement, Some(node));
let before = app.document.snapshot();
assert!(crate::behavior::state_edit::unplaced(app.project(), diagram).is_empty());
let owner = app.state_context().unwrap();
let focused = |mounted: &mut Mounted, app: &App| {
mounted.geometry(app).focused.iter().any(|(id, _, focused)| {
*focused
&& id.as_ref()
== Some(&iced::advanced::widget::Id::from("diagram-keyboard-owner"))
})
};
assert!(focused(&mut mounted, &app));
let messages = delete(&mut mounted, &mut app, false).await;
assert!(messages.iter().any(|message| matches!(
message,
@@ -1053,13 +1063,35 @@ async fn mounted_state_delete_uses_state_placement_and_preserves_architecture_pl
)));
assert!(!app.project().contains(node));
assert!(app.project().contains(architecture_node) && app.project().contains(state));
assert_eq!(
crate::behavior::state_edit::unplaced(app.project(), diagram),
vec![state]
);
mounted.settle(&mut app).await;
let hidden = app.state_context().unwrap();
assert_eq!(
(hidden.document, hidden.diagram, hidden.epoch),
(owner.document, owner.diagram, owner.epoch),
"hiding one placement does not change the logical canvas owner"
);
assert!(
focused(&mut mounted, &app),
"showing the unplaced-element picker must retain the actual canvas focus tree"
);
consistent(&app);
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join(&app.document.bundle().entry);
save_with_busy_view(&mut mounted, &mut app, &path).await;
assert!(focused(&mut mounted, &app), "Save retains the same owner");
let task = app.update(Message::Undo);
mounted.perform(&mut app, task).await;
assert_eq!(app.document.bundle(), &before.bundle);
assert!(crate::behavior::state_edit::unplaced(app.project(), diagram).is_empty());
mounted.settle(&mut app).await;
assert!(
focused(&mut mounted, &app),
"Undo removes the picker without replacing the actual canvas focus tree"
);
delete(&mut mounted, &mut app, true).await;
assert_eq!(
app.delete_target.as_ref().unwrap().snapshot.request.object,
+19 -8
View File
@@ -141,12 +141,16 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem
label: format!("{} · {}", p.named(element), p.elements[&element].kind()),
})
.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();
if !missing.is_empty() {
let position = Position {
x: (48. - state.pan.x) / state.zoom,
y: (90. - state.pan.y) / state.zoom,
};
out = out.push(
missing_controls = missing_controls.push(
pick_list(missing, None::<Choice>, move |c| {
event(Event::Place {
context,
@@ -173,13 +177,20 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem
.width(Fill)
.height(Fill)
.into();
out = out.push(crate::diagram::focus::wrap(
canvas.map(event),
context,
crate::app::diagram_removal::focus_enabled(app),
app.busy,
Message::DiagramTraverse,
));
out = out.push(
column![
missing_controls,
crate::diagram::focus::wrap(
canvas.map(event),
context,
crate::app::diagram_removal::focus_enabled(app),
app.busy,
Message::DiagramTraverse,
)
]
.spacing(missing_spacing)
.height(Fill),
);
let base: Element<'_, Message> = container(out.spacing(8))
.padding(14)
.width(Fill)