diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index 8d811230a..7e1255dba 100644 --- a/crates/ui-egui/src/panel_docking.rs +++ b/crates/ui-egui/src/panel_docking.rs @@ -491,6 +491,11 @@ fn shared_command(app: &mut DesignApp, id: &str, params: &Value) -> Result return Err("DesignCraft does not move panels to their own windows".into()), _ => {} } + // A zone is a side of `anchor`. DesignCraft's docks are columns beside the canvas, so a dock at + // a window edge is a `side`, not a frame on the outer edge (craft-ui's `Placement::Edge`). + if let DockCommand::Dock { anchor: None, side: None, zone: Some(_), .. } = &command { + return Err("zone needs an anchor: dock beside a panel with anchor, or in a new dock column with side (Start or End)".into()); + } let rtl = crate::i18n::is_rtl(&app.ui.language); let space = float_space(app); let mut txn = Transaction::begin(app)?; diff --git a/crates/ui-egui/src/panel_docking_tests.rs b/crates/ui-egui/src/panel_docking_tests.rs index 4638c8320..66416ba6d 100644 --- a/crates/ui-egui/src/panel_docking_tests.rs +++ b/crates/ui-egui/src/panel_docking_tests.rs @@ -997,10 +997,15 @@ fn shared_panel_commands_float_dock_and_reject_atomically() { ("window.panel.window", json!({"panel": "pages"})), ("window.panel.move", json!({"panel": "stroke", "anchor": "pages", "zone": "left", "before": "pages"})), ("window.panel.dock", json!({"panel": "stroke", "side": "Middle"})), + ("window.panel.dock", json!({"panel": "stroke", "zone": "left"})), + ("window.dockPanel", json!({"panel": "properties", "zone": "bottom"})), ] { assert!(app.run(id, params).is_err(), "{id}"); assert_eq!((app.ui.docking.clone(), app.ui.docking_hidden.clone()), before, "{id}"); } + // A zone is a side of an anchor; the error names the two ways to say where. + let error = app.run("window.panel.dock", json!({"panel": "stroke", "zone": "right"})).unwrap_err(); + assert!(error.contains("anchor") && error.contains("side"), "{error}"); // Every shared id is registered for the command list and MCP. for id in craft_ui::docking::DockCommand::::IDS { assert!(crate::menus::UI_COMMANDS.iter().any(|c| c.0 == *id), "{id} is not registered"); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 7c1f946cd..0d36c021a 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -116,7 +116,9 @@ read as `anchor`. rect unless the params change it. - `window.panel.dock {panel, anchor?, zone?, before?, group?, column?, side?}` (and `window.dockPanel`) docks beside `anchor`, or in a new column of the dock at `side` - (`Start` or `End`; a new dock when there is none). Without either, a panel that floated from + (`Start` or `End`; a new dock when there is none). A panel that already leads that dock's + last column stays where it is. `zone` places the panel beside `anchor`, so a `zone` without + `anchor` or `side` is an error. Without `anchor` and `side`, a panel that floated from the dock goes back to that place, and another floating panel joins the first dock group. For the float-only Tabs panel (`tabs`), which has its own window outside the layout, float opens that window and dock closes it.