From f501f7cb5f6f375e4d23d0f0a625b1effe702b62 Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 24 Oct 2026 23:33:00 +0000 Subject: [PATCH] Task panes: Dock Left, Dock Right and Float run ui.paneDock for every pane but Styles --- crates/ui-egui/src/lib.rs | 1 + crates/ui-egui/src/panes.rs | 68 ++++++++++++++++++++++++++++++++++--- docs/control-protocol.md | 2 +- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 0f44e8c0e..587ffed6e 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1660,6 +1660,7 @@ impl WordApp { self.canvas.want_focus = true; json!({"pending": "contextMenu"}) } + "ui.paneDock" => return Some(panes::dock_command(self, p)), "ui.collapseRibbon" => { self.ui.ribbon_collapsed = !self.ui.ribbon_collapsed; json!({"collapsed": self.ui.ribbon_collapsed}) diff --git a/crates/ui-egui/src/panes.rs b/crates/ui-egui/src/panes.rs index 62963bf44..12120265b 100644 --- a/crates/ui-egui/src/panes.rs +++ b/crates/ui-egui/src/panes.rs @@ -336,12 +336,12 @@ pub(crate) fn apply(app: &mut WordApp, request: PaneRequest) { PaneRequest::FloatRect(Pane::Styles, rect) => app.ui.styles_pane_float = Some(rect), PaneRequest::Place(pane, place) => { let dock = match place { - Place::Docked(Edge::Start) => SavedDock::Left, - Place::Docked(Edge::End) => SavedDock::Right, - Place::Floating => SavedDock::Float, + Place::Docked(Edge::Start) => "left", + Place::Docked(Edge::End) => "right", + Place::Floating => "float", Place::Docked(Edge::Bottom) => return, }; - app.ui.pane_places.entry(pane.key().into()).or_default().dock = Some(dock); + let _ = app.run("ui.paneDock", json!({"pane": pane.key(), "dock": dock})); } PaneRequest::Resize(pane, width) => app.ui.pane_places.entry(pane.key().into()).or_default().width = Some(width), PaneRequest::FloatRect(pane, rect) => app.ui.pane_places.entry(pane.key().into()).or_default().float = Some(rect), @@ -349,6 +349,37 @@ pub(crate) fn apply(app: &mut WordApp, request: PaneRequest) { } } +/// `ui.paneDock {"pane": key, "dock"?: "left" | "right" | "float"}` (#1756): docks or floats a +/// task pane, as its menu's Dock Left, Dock Right and Float do; without `dock` it reads the +/// pane's place. `pane` is the pane's `panePlaces` key; Styles goes through `view.stylesPaneDock`. +pub(crate) fn dock_command(app: &mut WordApp, p: &Value) -> Result { + let key = p.get("pane").and_then(Value::as_str).ok_or("`pane` must be a task pane's key, such as \"navigation\"")?; + let pane = Pane::ALL.into_iter().find(|pane| pane.key() == key).ok_or_else(|| format!("unknown task pane `{key}`"))?; + let dock = match p.get("dock") { + None => None, + Some(v) => Some(match v.as_str() { + Some("left") => SavedDock::Left, + Some("right") => SavedDock::Right, + Some("float") => SavedDock::Float, + _ => return Err("`dock` must be left, right or float".into()), + }), + }; + if pane == Pane::Styles { + let params = p.get("dock").map_or_else(|| json!({}), |d| json!({"dock": d})); + let result = app.run("view.stylesPaneDock", params)?; + return Ok(json!({"pane": key, "dock": result.get("dock")})); + } + if let Some(dock) = dock { + app.ui.pane_places.entry(key.into()).or_default().dock = Some(dock); + } + let name = match pane_state(app, pane).place { + Place::Docked(Edge::Start) => "left", + Place::Floating => "float", + Place::Docked(_) => "right", + }; + Ok(json!({"pane": key, "dock": name})) +} + fn close(app: &mut WordApp, pane: Pane) { match pane.close_command() { Some(id) => { @@ -1402,6 +1433,35 @@ mod tests { assert!(odd.ui.pane_places.is_empty() && odd.session.view.nav_pane); } + /// `ui.paneDock` docks or floats any pane and reads its place, as the pane's menu does + /// (#1756); Styles goes through `view.stylesPaneDock`; junk is refused and changes nothing. + #[test] + fn pane_dock_command_docks_floats_and_reads_each_pane() { + use craft_ui::panes::{Edge, Place}; + use serde_json::json; + let mut app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::from_text("")), crate::Services::default()); + assert_eq!(app.run("ui.paneDock", json!({"pane": "navigation"})).unwrap(), json!({"pane": "navigation", "dock": "left"})); + assert_eq!(app.run("ui.paneDock", json!({"pane": "comments"})).unwrap(), json!({"pane": "comments", "dock": "right"})); + assert_eq!(app.run("ui.paneDock", json!({"pane": "navigation", "dock": "float"})).unwrap()["dock"], "float"); + assert_eq!(super::pane_state(&app, Pane::Navigation).place, Place::Floating); + app.run("ui.paneDock", json!({"pane": "comments", "dock": "left"})).unwrap(); + assert_eq!(super::pane_state(&app, Pane::Comments).place, Place::Docked(Edge::Start)); + assert_eq!(app.run("ui.paneDock", json!({"pane": "styles", "dock": "float"})).unwrap(), json!({"pane": "styles", "dock": "float"})); + assert_eq!(app.session.view.styles_pane_dock, wordcraft_engine::StylesPaneDock::Floating); + assert!(!app.ui.pane_places.contains_key("styles"), "Styles keeps its place in the view state"); + for junk in [ + json!({}), + json!({"pane": "nope", "dock": "left"}), + json!({"pane": 3}), + json!({"pane": "navigation", "dock": "top"}), + json!({"pane": "navigation", "dock": 1}), + json!({"pane": "navigation", "dock": null}), + ] { + assert!(app.run("ui.paneDock", junk.clone()).is_err(), "{junk}"); + } + assert_eq!(super::pane_state(&app, Pane::Navigation).place, Place::Floating, "junk changed nothing"); + } + /// #3032: Delete is greyed out for built-in styles in the Styles pane (the engine refuses /// them — `crates/engine/src/cmd/para.rs`), while custom styles keep it enabled. #[test] diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 08c62c3d9..f0adffa3b 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -52,7 +52,7 @@ and set `WORDCRAFT_CONTROL_KEY` to that app's key. | Method | Params | Result | |---|---|---| -| `engine.execute` | `command`, `params` | the command's result (any id from `engine.commands`, including UI commands `ui.tab`, `ui.dialog` (`{"name": dialog, "part"?: table style part}`; `part` opens New / Modify Table Style (`newTableStyle` / `modifyTableStyle`) on that part: `wholeTable`, `headerRow`, `firstColumn`, `bandedRows`, `nwCell`… as `table.modifyStyle` names them), `ui.backstage`, `ui.zotero.*` (see `docs/zotero.md`), `ui.language` (`{"value": "auto"\|"en"\|"zh-hans"\|"zh-hant"\|"ja"\|"pt-br"\|"es"\|"uk"\|"sr"\|"sr-latn"\|"et"\|"tr"\|"ru"\|"pl"\|"de"\|"it"\|"cs"\|"hu"\|"fr"}`), `ui.theme` (`{"value": "system"\|"light"\|"dark"}`; `system` follows the OS appearance, light when it reports none), `ui.interfaceSize` (`{"value": "auto"}` or `{"percent": 50–300}`, clamped; no value reads it; `auto` keeps the system display scale, a percentage is absolute, so 100 is one screen pixel per point (#822); the result's `percent` is the size drawn at and `auto` whether it follows the system; document zoom stays `view.zoom`), `ui.animations` (`{"value": "system"\|"on"\|"off"}`; no value reads it; `system` follows the system's animation setting, which the desktop app reads on Windows, macOS, Linux and the BSDs and the web app from `prefers-reduced-motion`, and means on where the system doesn't say; the result's `enabled` is what the setting comes to and `system` what the system reports, `null` when it doesn't say), `ui.fileBrowser` (desktop: `{"purpose": "document"\|"picture"\|"recipients"\|"save"}` opens WordCraft's own file browser, for when the system file dialog doesn't show up, #576), `ui.saveAsTemplate` (File › Save As › Save as Word Template: the Save dialog for a `.dotx`, starting in the user templates folder; the result's `dir` is that folder, `null` on the web, #2852), `ui.caretAnimation` (`{"value": true\|false}`; no value reads it; off, the caret jumps and blinks on and off instead of gliding and fading), `ui.startScreen` (`{"value": true\|false}`; no value reads it; on, the default, the desktop and web apps start on File › Home when launched with nothing to open, #766), `ui.alignmentGuides` (`{"value": true\|false}`; no value reads it; on, the default, moving a picture or shape snaps it to the page, the margins and other objects, with guide lines while dragging; Alt moves freely), `ui.quickAccessToolbar` (`{"value": true\|false}`; no value toggles it; false takes the Quick Access Toolbar out of the title bar, as its menu's Hide Quick Access Toolbar does, and true brings it back, #1945), `ui.focusRibbon` (`{"value": true\|false}`; no value toggles it; in Focus, true drops the ribbon down over the page, as the bar's ⋯ does, and false puts it away, as a click on the page does; outside Focus it stays put away, #1678), `ui.contextMenu` (opens the page's context menu below the caret, as Shift+F10 does)…) | +| `engine.execute` | `command`, `params` | the command's result (any id from `engine.commands`, including UI commands `ui.tab`, `ui.dialog` (`{"name": dialog, "part"?: table style part}`; `part` opens New / Modify Table Style (`newTableStyle` / `modifyTableStyle`) on that part: `wholeTable`, `headerRow`, `firstColumn`, `bandedRows`, `nwCell`… as `table.modifyStyle` names them), `ui.backstage`, `ui.zotero.*` (see `docs/zotero.md`), `ui.language` (`{"value": "auto"\|"en"\|"zh-hans"\|"zh-hant"\|"ja"\|"pt-br"\|"es"\|"uk"\|"sr"\|"sr-latn"\|"et"\|"tr"\|"ru"\|"pl"\|"de"\|"it"\|"cs"\|"hu"\|"fr"}`), `ui.theme` (`{"value": "system"\|"light"\|"dark"}`; `system` follows the OS appearance, light when it reports none), `ui.interfaceSize` (`{"value": "auto"}` or `{"percent": 50–300}`, clamped; no value reads it; `auto` keeps the system display scale, a percentage is absolute, so 100 is one screen pixel per point (#822); the result's `percent` is the size drawn at and `auto` whether it follows the system; document zoom stays `view.zoom`), `ui.animations` (`{"value": "system"\|"on"\|"off"}`; no value reads it; `system` follows the system's animation setting, which the desktop app reads on Windows, macOS, Linux and the BSDs and the web app from `prefers-reduced-motion`, and means on where the system doesn't say; the result's `enabled` is what the setting comes to and `system` what the system reports, `null` when it doesn't say), `ui.fileBrowser` (desktop: `{"purpose": "document"\|"picture"\|"recipients"\|"save"}` opens WordCraft's own file browser, for when the system file dialog doesn't show up, #576), `ui.saveAsTemplate` (File › Save As › Save as Word Template: the Save dialog for a `.dotx`, starting in the user templates folder; the result's `dir` is that folder, `null` on the web, #2852), `ui.caretAnimation` (`{"value": true\|false}`; no value reads it; off, the caret jumps and blinks on and off instead of gliding and fading), `ui.startScreen` (`{"value": true\|false}`; no value reads it; on, the default, the desktop and web apps start on File › Home when launched with nothing to open, #766), `ui.alignmentGuides` (`{"value": true\|false}`; no value reads it; on, the default, moving a picture or shape snaps it to the page, the margins and other objects, with guide lines while dragging; Alt moves freely), `ui.quickAccessToolbar` (`{"value": true\|false}`; no value toggles it; false takes the Quick Access Toolbar out of the title bar, as its menu's Hide Quick Access Toolbar does, and true brings it back, #1945), `ui.focusRibbon` (`{"value": true\|false}`; no value toggles it; in Focus, true drops the ribbon down over the page, as the bar's ⋯ does, and false puts it away, as a click on the page does; outside Focus it stays put away, #1678), `ui.contextMenu` (opens the page's context menu below the caret, as Shift+F10 does), `ui.paneDock` (`{"pane": "navigation"\|"clipboard"\|"styles"\|"styleInspector"\|"accessibility"\|"altText"\|"formatShape"\|"restrictEditing"\|"comments"\|"smartArtText", "dock"?: "left"\|"right"\|"float"}`; no `dock` reads it; docks or floats a task pane as its menu does, kept between runs; Styles is `view.stylesPaneDock`, #1756)…) | | `engine.commands` | — | every command: id, label, location, shortcut, params, enabled | | `document.inspect` | `text?` | blocks (text, style, runs, lists, tables), parts, sections, selection, pages | | `ui.inspect` | — | UI state, view, dialog, window size, page rects on screen, caret, comment balloons (`balloons`: id + screen rect; `balloon`: the selected one), perf |