diff --git a/crates/oa-desktop/src/app/diagram_removal_tests.rs b/crates/oa-desktop/src/app/diagram_removal_tests.rs index 2fc7b279..7ef73306 100644 --- a/crates/oa-desktop/src/app/diagram_removal_tests.rs +++ b/crates/oa-desktop/src/app/diagram_removal_tests.rs @@ -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, diff --git a/crates/oa-desktop/src/behavior/state_view.rs b/crates/oa-desktop/src/behavior/state_view.rs index eb46407d..9334efea 100644 --- a/crates/oa-desktop/src/behavior/state_view.rs +++ b/crates/oa-desktop/src/behavior/state_view.rs @@ -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::>(); + // 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::, 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)