Browse / github.com/storytold/cadcraft / patch
feat(ui): add explicit palette side docking actions
feat(ui): add explicit palette side docking actions ## Change Add Dock Left and Dock Right to palette tab context menus, including floating palettes. Each action moves the selected palette to its own side column through the existing ui.dock command, preserving the drawing view and sibling palettes. Covers Tool Sets, Layers, Properties, QuickCalc and External References. CC-018 is a focused extraction using upstream shared docking. AutoCAD 2027 Properties inspection confirmed Allow Docking, Anchor Left/Right and Auto-hide. These fixed-column actions do not claim anchoring or hover auto-hide; those remain separate. ## Dependencies and ov
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- smacdmac101 (imported from pull request #2226, open)
- Base
d3ae8a6af0b76185e33f6dc2735679fa5c2be77c- Size
- +110 −4 in 7 files
- Review
- pending
- Shared
- 2026-10-28
- Id
59076572645f79efe5fb9616
How it went
No other operator has reported using it yet.
Diff
raw · crates/ui-egui/src/docking.rs crates/ui-egui/src/i18n/uk.tsv crates/ui-egui/tests/it/docking_workflows.rs docs/control-protocol.md docs/ui-parity.md xtask/src/parity.rs xtask/src/stats.rs
@@ -252,8 +252,20 @@ fn set_visible(workspace: &mut Workspace, panel: Panel, visible: bool) -> Result } /// `ui.dock` params: `{action}` or `{operation, …}` in craft-ui's shared grammar.+/// `operation: "dock"` delegates to the shared side/anchor docking command. fn apply_command(workspace: &mut Workspace, params: &Value) -> Result<(), String> { validate(workspace)?;+ if params.get("operation").and_then(Value::as_str) == Some("dock") && params.get("action").is_none() {+ let mut command = params.clone();+ command["command"] = json!("window.panel.dock");+ let command: DockCommand<Panel> = serde_json::from_value(command).map_err(|error| error.to_string())?;+ let mut next = workspace.clone();+ let space = FloatSpace::new(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1280.0, 800.0)));+ command.apply(&mut next, false, &space, permissions).map_err(|error| error.to_string())?;+ validate(&next)?;+ *workspace = next;+ return Ok(());+ } let operation = if let Some(action) = params.get("action") { UiDock::Action { action: serde_json::from_value(action.clone()).map_err(|error| error.to_string())? } } else {@@ -416,6 +428,21 @@ impl DockContent<Panel> for Panels<'_> { } } }+ fn panel_menu(&mut self, ui: &mut egui::Ui, panel: &Panel) {+ if !permissions(panel).movable {+ return;+ }+ for (label, side) in [(crate::tl!("Dock Left"), Side::Start), (crate::tl!("Dock Right"), Side::End)] {+ if ui.button(label).clicked() {+ let result = self.app.run("ui.dock", json!({"operation": "dock", "panel": panel, "side": side}));+ if let Err(error) = result {+ self.app.set_status(format!("Panel move was not applied: {error}"));+ }+ ui.close();+ }+ }+ ui.separator();+ } fn logical_label(&self, panel: &Panel) -> String { crate::i18n::t(panel.label()).into() }@@ -1371,3 +1371,5 @@ Check for Updates… Перевірити оновлення… CADCraft is up to date. Встановлено найновішу версію CADCraft. Could not check for updates. Не вдалося перевірити оновлення.+ Dock Left Прикріпити ліворуч+ Dock Right Прикріпити праворуч@@ -593,3 +593,57 @@ fn group_drag_bar_moves_the_whole_group_into_the_other_dock() { assert_eq!(frames(layout), [[Panel::Canvas]]); assert_eq!(h.state().session.doc().unwrap(), &document); }++/// Side choices move the selected palette into a real dock column, without grouping it into+/// another palette or moving the document; floating and persisted layouts use the same path.+#[test]+fn palette_side_menu_redocks_each_panel_without_grouping_the_drawing() {+ let mut h = harness(None, 1600.0, 1.0);+ h.state_mut().session.start("line").unwrap();+ let before = h.state().session.doc().unwrap().model.len();+ for panel in [Panel::ToolSets, Panel::Layers, Panel::Properties, Panel::QuickCalc, Panel::ExternalRefs] {+ h.state_mut().run("ui.dock", json!({"operation":"open", "panel":panel})).unwrap();+ h.state_mut().run("ui.dock", json!({"operation":"float", "panel":panel, "rect":[400,150,280,380]})).unwrap();+ h.run_steps(5);+ for (label, side) in [("Dock Left", Side::Start), ("Dock Right", Side::End)] {+ let pos = hit(&h, Hit::Tab(panel)).center();+ h.event(Event::PointerMoved(pos));+ h.event(Event::PointerButton { pos, button: PointerButton::Secondary, pressed: true, modifiers: Modifiers::NONE });+ h.event(Event::PointerButton { pos, button: PointerButton::Secondary, pressed: false, modifiers: Modifiers::NONE });+ h.run_steps(4);+ h.get_by_label(label).click();+ h.run_steps(5);+ assert!(+ dock_panels(&h.state().ui.docking.layout, side).iter().flatten().any(|group| group == &[panel]),+ "{panel:?} {side:?}: {:?}; status {:?}",+ h.state().ui.docking,+ h.state().status+ );+ assert_eq!(frames(&h.state().ui.docking.layout), [[Panel::Canvas]]);+ assert!(!h.state().ui.docking.layout.floating.iter().any(|window| window.panels().any(|p| *p == panel)));+ assert_eq!(h.state().session.running.as_ref().map(|r| r.id.as_str()), Some("line"));+ assert_eq!(h.state().session.doc().unwrap().model.len(), before);+ }+ }+ let saved = serde_json::to_value(&h.state().ui).unwrap();+ let restored: UiState = serde_json::from_value(saved).unwrap();+ assert_eq!(restored.docking, h.state().ui.docking);+}++#[test]+fn palette_side_command_preserves_siblings_and_rejects_protected_or_locked_moves() {+ let mut h = harness(None, 1600.0, 1.0);+ h.state_mut().run("ui.dock", json!({"operation":"move", "panel":"layers", "target":"properties", "zone":"center"})).unwrap();+ h.state_mut().run("ui.dock", json!({"operation":"dock", "panel":"layers", "side":"Start"})).unwrap();+ assert!(dock_panels(&h.state().ui.docking.layout, Side::Start).iter().flatten().any(|group| group == &[Panel::Layers]));+ assert!(dock_panels(&h.state().ui.docking.layout, Side::End).iter().flatten().any(|group| group == &[Panel::Properties]));+ for params in [json!({"operation":"dock", "panel":"canvas", "side":"Start"}), json!({"operation":"dock", "panel":"layers", "side":"invalid"})] {+ let before = h.state().ui.docking.clone();+ assert!(h.state_mut().run("ui.dock", params).is_err());+ assert_eq!(h.state().ui.docking, before);+ }+ h.state_mut().ui.docking.locked = true;+ let before = h.state().ui.docking.clone();+ assert!(h.state_mut().run("ui.dock", json!({"operation":"dock", "panel":"layers", "side":"End"})).is_err());+ assert_eq!(h.state().ui.docking, before);+}@@ -146,6 +146,12 @@ closing/reopening within the same session; they are not saved across application Invoke these UI commands through `engine.execute`, for example `{"command":"ui.dock","params":{"operation":"float","panel":"layers","rect":[100,100,320,440]}}`. +`{"operation":"dock","panel":"layers","side":"Start"}` moves the selected palette into+its own left-side column; `"End"` selects the right side. The shared docking command creates+the side dock when absent and adds a column when it already exists. This is also the route+used by the palette context menu's Dock Left and Dock Right choices. Protected drawing+placement and workspace locking still apply; an invalid request leaves the layout unchanged.+ Advanced `ui.dock` operations use the same validated transaction as pointer gestures: `moveFloating` updates `rect`, `resizeSplit` accepts a boolean child `path` and `size` (`{"Ratio":0.5}`, `{"FixedFirst":280}`, or `{"FixedSecond":280}`),@@ -1,6 +1,6 @@ # UI parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (distinct polyline midpoint grips) · **Target:** Autodesk AutoCAD 2027 for Mac (26.0)+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (explicit palette side docking) · **Target:** Autodesk AutoCAD 2027 for Mac (26.0) How CADCraft's interaction compares with AutoCAD's: point entry, object snaps, tracking, dynamic input, grips, selection, the command line, chrome and navigation. A drafter's speed in AutoCAD@@ -199,10 +199,25 @@ down-arrow options menu (DYNPROMPT), and DYNDIM. have menus too (#512). Still missing: in-place layout tab rename, Move or Copy, Select All Layouts. +## Explicit palette side docking++Palette tab context menus offer Dock Left and Dock Right in addition to the shared floating+and grouping actions. Each choice moves that palette to a real side dock column through+`ui.dock`; it does not turn the drawing into a tab group or move the palette's other tabs.+The same menu works for floating palettes. Native workspace persistence stores the resulting+layout across restarts. The browser host does not persist workspace layouts; reloading restores+the default arrangement. The document view has no side-placement menu and retains its protected permissions.+This CC-018 extraction uses the upstream shared docking implementation; auto-hide and+Tool Sets-to-ribbon placement are separate follow-ups.+AutoCAD 2027's Properties palette menu was observed on Windows: it offers Allow Docking,+Anchor Left, Anchor Right and Auto-hide. These fixed-column actions are an incremental+placement/recovery improvement, not a claim to reproduce anchoring or hover auto-hide.+ ## Revision history | Date | Change | Summary | |---|---|---|+| 2026-10-26 | minor | CC-018: palette tab menu Dock Left/Right actions use shared side columns; independent of ribbon and auto-hide | | 2026-10-22 | minor | Polyline segment midpoint grips use rectangles, distinct from square vertex grips, with matching hover and pick bounds (#1950) | | 2026-10-13 | minor | Filled-region highlight boundaries are traversed in deterministic coordinate order, fixing dash-phase jumps on CPU hover/selection redraws and GPU highlight rebuilds; CPU dash-position, pixel and GPU mesh regressions | | 2026-10-14 | minor | Selection cycling (#1143): the list and badge of SELECTIONCYCLING, Shift+Space through overlapping objects (#1145) |@@ -136,7 +136,9 @@ mod tests { assert_eq!(day(20_736), "2026-10-10"); assert_eq!(date(std::time::UNIX_EPOCH - std::time::Duration::from_secs(86_400)), "1970-01-01"); // Past chrono's range keeps the proleptic Gregorian date, not the epoch (old `civil_from_days`).- assert_eq!(day(115_740_740), "318857-05-20");+ // Checked on the day count: Windows' SystemTime ends in the year 30828, so building that+ // SystemTime panics there.+ assert_eq!(civil_from_days(115_740_740), (318_857, 5, 20)); // Windows' SystemTime can't hold u64::MAX / 2 seconds, so it is only checked where it exists. if let Some(far) = std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(u64::MAX / 2)) { assert_eq!(date(far), "292277026596-12-04");@@ -134,7 +134,7 @@ pub fn run(_root: &Path, exact: bool) -> Result<(), String> { #[cfg(test)] mod tests {- use super::{Counts, count_tests, walk};+ use super::{Counts, count_tests}; #[test] fn counts_attributes_and_proptest_blocks() {@@ -155,7 +155,7 @@ mod tests { // A directory symlink pointing back at `real` would be re-scanned if followed. symlink(&real, base.join("alias")).unwrap(); let mut c = Counts::default();- walk(&base, &mut c).unwrap();+ super::walk(&base, &mut c).unwrap(); assert_eq!(c.files, 1, "the aliased directory was followed and counted twice"); std::fs::remove_dir_all(&base).ok(); }