From 315ff52ffdeaf5a8bd679526e08a9a435cdcb3b6 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 5 Nov 2026 05:57:32 -0800 Subject: [PATCH] Workspace: make keeping Tools beside the canvas an option, off by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docked Tools stays at the window's left edge again, outside any panels docked on the left. Window › Toolbars › Keep Tools Beside the Canvas (`window.toolsBesideCanvas {on?}`) docks it between those panels and the canvas instead. - The setting is its own field in the UI state and in workspaces, and a missing field reads as off, so saved state and workspaces keep Tools at the window's edge. Built-in workspaces have it off. - Off, app.rs draws docked Tools before the docking layout, the canvas reserves no width for it, and only the full-height window-edge strip docks a floating Tools panel. On, the canvas panel draws it, the canvas keeps room for it, and both the window edge and the canvas edge dock it beside the canvas. - Turning the setting off forgets the canvas-edge strip, so a strip laid out while it was on does not dock Tools. A floating Tools panel stays floating when the setting changes. - `window.panel.dock {panel: "tools"}` docks Tools where the setting says. - The menu label has a row in every catalog. Docs: docs/mcp.md, docs/control-protocol.md, the ROADMAP progress log and docs/target-app-parity.md. The headline-numbers row and the paragraphs under the status lines are gone, and scores are unchanged. --- ROADMAP.md | 1 + crates/ui-egui/src/app.rs | 7 +- crates/ui-egui/src/custom_toolbars.rs | 11 +- crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/bs.tsv | 1 + crates/ui-egui/src/i18n/ca.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/da.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fi.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/id.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ka.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/sv.tsv | 1 + crates/ui-egui/src/i18n/tr.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/menus/run.rs | 5 + crates/ui-egui/src/menus/state.rs | 1 + crates/ui-egui/src/menus/ui_commands.rs | 8 +- crates/ui-egui/src/panel_docking/tests.rs | 221 +++++++++++++++++----- crates/ui-egui/src/state.rs | 8 +- crates/ui-egui/src/toolbar.rs | 21 +- crates/ui-egui/src/tools_float.rs | 30 ++- crates/ui-egui/src/workspaces.rs | 5 + docs/control-protocol.md | 4 +- docs/mcp.md | 15 ++ docs/target-app-parity.md | 1 + 40 files changed, 295 insertions(+), 69 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 1f43b2144..f7d59a3da 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -137,6 +137,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap ## Progress log +- 2026-11-05: Window › Toolbars › Keep Tools Beside the Canvas, off by default, docks the Tools panel between left-side panels and the canvas instead of at the window's left edge, and the canvas's left edge then also docks a floating Tools panel (`window.toolsBesideCanvas`, #5395). Workspaces keep the setting; workspaces saved without it keep Tools at the window's edge. An optional VectorCraft layout choice; headline numbers unchanged. - 2026-11-05: Release Type on a Path joins the Type Tools add-on (Object › Plug-ins › Type Tools): each selected type on a path splits into point type with the same text and attributes, on one straight line starting where its first character sat, and the plain path it was set on, with its exact shape and place in the stacking order and no paint; or keeps only the path or only the text, optionally leaving the original. A dialog with Preview, one undo step (`addon.type.releasePath`, #5532). A VectorCraft addition: headline numbers unchanged. - 2026-11-04: New Document: the units chosen in Preset Details now belong to the chosen document, as the pane's options are listed for the selected preset; the chosen card shows them, every other Recent and Saved card keeps the units its document or preset was made in, and the built-in tabs still follow the chosen units, so switching between inches and millimetres no longer restyles every recent template. Scores and estimates unchanged. - 2026-11-02: Mouse wheel over a closed font-family field in Character, Properties or Control changes selected text through the family list using saved font filters (excluding search text), one undo step per notch, stopping at the ends (#5253). Scores and estimates unchanged. diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs index db736fb6f..5b53073a3 100644 --- a/crates/ui-egui/src/app.rs +++ b/crates/ui-egui/src/app.rs @@ -545,7 +545,12 @@ impl VectorcraftApp { if self.ui.toolbar && self.ui.screen_mode < 3 { tools_float::set_window_dock_zone(&ctx, ui.available_rect_before_wrap()); } - if self.ui.toolbar && self.ui.screen_mode < 3 && self.ui.toolbar_pos.is_some() { + if !self.ui.tools_beside_canvas { + // A canvas-edge target left from before the setting was turned off docks nothing. + tools_float::clear_canvas_dock_zone(&ctx); + } + // Beside the canvas, docked Tools draws inside the canvas panel (`toolbar::show_beside_canvas`). + if self.ui.toolbar && self.ui.screen_mode < 3 && (!self.ui.tools_beside_canvas || self.ui.toolbar_pos.is_some()) { toolbar::show(self, ui); } else if !self.ui.toolbar && self.ui.screen_mode < 3 && self.ui.tab_hid.is_none() { // Window › Tools hides the Tools panel alone; Tab hides the toolbars made with New diff --git a/crates/ui-egui/src/custom_toolbars.rs b/crates/ui-egui/src/custom_toolbars.rs index adbf1223f..e5008644b 100644 --- a/crates/ui-egui/src/custom_toolbars.rs +++ b/crates/ui-egui/src/custom_toolbars.rs @@ -523,12 +523,17 @@ pub fn sync(ui: &UiState) { } } -/// Window › Toolbars: Advanced and Double Column, the toolbars made with New Toolbar (each checked -/// while it shows), New Toolbar… and Manage Toolbars…. +/// Window › Toolbars: Advanced, Double Column and Keep Tools Beside the Canvas, the toolbars made +/// with New Toolbar (each checked while it shows), New Toolbar… and Manage Toolbars…. pub fn menu_items() -> Vec { use crate::menus::Item; let c = |label: &'static str, id: &'static str| Item::Cmd(label, id, Value::Null); - let mut v = vec![c("Advanced", "window.toolbarAdvanced"), c("Double Column", "window.toolbarColumns"), Item::Sep]; + let mut v = vec![ + c("Advanced", "window.toolbarAdvanced"), + c("Double Column", "window.toolbarColumns"), + c("Keep Tools Beside the Canvas", "window.toolsBesideCanvas"), + Item::Sep, + ]; let names = names_store().read().map(|v| v.clone()).unwrap_or_default(); if !names.is_empty() { v.extend(names.into_iter().map(|n| Item::Cmd(n, "window.toolbar.custom", json!({ "name": n })))); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index e4d558ca1..981975808 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -3120,6 +3120,7 @@ digits Malayalam Малаялам Simulate Colored Paper Симулиране на цветна хартия Simulate Paper Симулиране на хартия Double Column Две колони + Keep Tools Beside the Canvas Инструментите до платното Single Left Quotation Mark Лява единична кавичка Single Quotes: Единични кавички: Single Right Quotation Mark Дясна единична кавичка diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index a4a185058..8da20248c 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -670,6 +670,7 @@ Toolbars Trake s alatima Advanced Napredno Double Column Dvostruka kolona + Keep Tools Beside the Canvas Alati pored platna Actions Akcije Asset Export Izvoz resursa Attributes Atributi diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 12457d313..3ad992e57 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -1206,6 +1206,7 @@ Double Bar Barra doble Double Click To Isolate Doble clic per aïllar Double Column Columna doble + Keep Tools Beside the Canvas Eines al costat del llenç Double Left Quotation Marks Cometes dobles esquerres Double Quotes: Cometes dobles: Double Right Quotation Marks Cometes dobles dretes diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index e20f83c2b..8cc2a6fba 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -678,6 +678,7 @@ Toolbars Panely nástrojů Advanced Rozšířené Double Column Dva sloupce + Keep Tools Beside the Canvas Nástroje vedle plátna Actions Akce Asset Export Export prvků Attributes Atributy diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 114c3fbdb..978a0971a 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -627,6 +627,7 @@ Toolbars Værktøjslinjer Advanced Avanceret Double Column Dobbelt kolonne + Keep Tools Beside the Canvas Værktøjer ved siden af canvaset Actions Handlinger Asset Export Aktiveksport Attributes Attributter diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 64dcb319a..374c63918 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -3164,6 +3164,7 @@ digits Malayalam Malayalam Simulate Colored Paper Farbiges Papier simulieren Simulate Paper Papier simulieren Double Column Zweispaltig + Keep Tools Beside the Canvas Werkzeuge neben der Arbeitsfläche Single Left Quotation Mark Einfaches öffnendes Anführungszeichen Single Quotes: Einfache Anführungszeichen: Single Right Quotation Mark Einfaches schließendes Anführungszeichen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 68d70e8f1..b42c06626 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3618,6 +3618,7 @@ digits Malayalam Μαλαγιάλαμ Simulate Colored Paper Προσομοίωση χρωματιστού χαρτιού Simulate Paper Προσομοίωση χαρτιού Double Column Διπλή στήλη + Keep Tools Beside the Canvas Εργαλεία δίπλα στον καμβά Single Left Quotation Mark Μονό αριστερό εισαγωγικό Single Quotes: Μονά εισαγωγικά: Single Right Quotation Mark Μονό δεξί εισαγωγικό diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 84dda1ce8..67a5a84d2 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3204,6 +3204,7 @@ digits Malayalam Malayálam Simulate Colored Paper Simular papel de color Simulate Paper Simular papel Double Column Columna doble + Keep Tools Beside the Canvas Herramientas junto al lienzo Single Left Quotation Mark Comilla simple de apertura Single Quotes: Comillas simples: Single Right Quotation Mark Comilla simple de cierre diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index b781ab310..3f8658a21 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -669,6 +669,7 @@ Toolbars Työkalurivit Advanced Lisäasetukset Double Column Kaksi saraketta + Keep Tools Beside the Canvas Työkalut kankaan vieressä Actions Toiminnot Asset Export Resurssien vienti Attributes Määritteet diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 3e2eb9c01..9ba17bcc4 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3196,6 +3196,7 @@ digits Malayalam Malayalam Simulate Colored Paper Simuler un papier de couleur Simulate Paper Simuler le papier Double Column Deux colonnes + Keep Tools Beside the Canvas Outils à côté de la zone de travail Single Left Quotation Mark Guillemet simple ouvrant Single Quotes: Guillemets simples : Single Right Quotation Mark Guillemet simple fermant diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 36c7a5a6f..aa3094599 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -1111,6 +1111,7 @@ font-technology Variable Változó Double Bar Kettős vonal Double Click To Isolate Elkülönítés dupla kattintással Double Column Két oszlop + Keep Tools Beside the Canvas Eszközök a vászon mellett Double Left Quotation Marks Bal oldali dupla idézőjel Double Quotes: Dupla idézőjelek: Double Right Quotation Marks Jobb oldali dupla idézőjel diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index e32b28107..bffe798c5 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -627,6 +627,7 @@ Toolbars Bilah alat Advanced Lanjutan Double Column Kolom ganda + Keep Tools Beside the Canvas Alat di samping kanvas Actions Tindakan Asset Export Ekspor aset Attributes Atribut diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 4e0b5f341..8f6693d46 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3240,6 +3240,7 @@ digits Malayalam Malayalam Simulate Colored Paper Simula carta colorata Simulate Paper Simula carta Double Column Colonna doppia + Keep Tools Beside the Canvas Strumenti accanto all'area di disegno Single Left Quotation Mark Virgoletta singola di apertura Single Quotes: Virgolette singole: Single Right Quotation Mark Virgoletta singola di chiusura diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index dc845fa7a..ea4ffac06 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -684,6 +684,7 @@ digits Malayalam マラヤーラム数字 Toolbars ツールバー Advanced 詳細 Double Column 2 列 + Keep Tools Beside the Canvas ツールをキャンバスの横に配置 Actions アクション Asset Export アセットの書き出し Attributes 属性 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 5e3262f82..e64fd2fa3 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -580,6 +580,7 @@ Toolbars ხელსაწყოთა ზოლები Advanced დამატებითი Double Column ორი სვეტი + Keep Tools Beside the Canvas ხელსაწყოები ტილოს გვერდით Actions მოქმედებები Asset Export რესურსების ექსპორტირება Attributes ატრიბუტები diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 8f9e3e286..336218c32 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -677,6 +677,7 @@ digits Malayalam 말라얄람어 Toolbars 도구 모음 Advanced 고급 Double Column 2열 + Keep Tools Beside the Canvas 캔버스 옆에 도구 배치 Actions 액션 Asset Export 에셋 내보내기 Attributes 속성 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 8dccad87c..bfbdef06c 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -641,6 +641,7 @@ Toolbars Werkbalken Advanced Geavanceerd Double Column Dubbele kolom + Keep Tools Beside the Canvas Gereedschappen naast het canvas Actions Acties Asset Export Assetexport Attributes Kenmerken diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index c5513ad3e..f9fdfd743 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -628,6 +628,7 @@ Toolbars Paski narzędzi Advanced Zaawansowany Double Column Dwie kolumny + Keep Tools Beside the Canvas Narzędzia obok płótna Actions Operacje Asset Export Eksport zasobów Attributes Atrybuty diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index f49c6aac2..92b5aa0c1 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2687,6 +2687,7 @@ digits Malayalam Malaiala Simulate Colored Paper Simular Papel Colorido Simulate Paper Simular Papel Double Column Coluna Dupla + Keep Tools Beside the Canvas Ferramentas ao Lado da Tela Single Left Quotation Mark Aspa Esquerda Única Single Quotes: Aspas Simples: Single Right Quotation Mark Aspa Direita Única diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 85cf2e0e6..5d5252ee3 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3235,6 +3235,7 @@ digits Malayalam Малаялам Simulate Colored Paper Имитировать цветную бумагу Simulate Paper Имитировать бумагу Double Column Двойной столбец + Keep Tools Beside the Canvas Инструменты рядом с холстом Single Left Quotation Mark Одинарная открывающая кавычка Single Quotes: Одинарные кавычки: Single Right Quotation Mark Одинарная закрывающая кавычка diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index d19c210b9..6ef144c58 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -3126,6 +3126,7 @@ digits Malayalam Malayalam Simulate Colored Paper Simulera färgat papper Simulate Paper Simulera papper Double Column Dubbel kolumn + Keep Tools Beside the Canvas Verktyg bredvid arbetsytan Single Left Quotation Mark Enkelt vänster citattecken Single Quotes: Enkla citattecken: Single Right Quotation Mark Enkelt höger citattecken diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index db8509636..d38231b3c 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -3029,6 +3029,7 @@ digits Malayalam Malayalam Simulate Colored Paper Renkli Kağıdı Simüle Et Simulate Paper Kağıdı Simüle Et Double Column Çift Sütun + Keep Tools Beside the Canvas Araçları Tuvalin Yanında Tut Single Left Quotation Mark Tek Sol Tırnak İşareti Single Quotes: Tek Alıntılar: Single Right Quotation Mark Tek Sağ Tırnak İşareti diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 12fafaa8d..353cbacd8 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3232,6 +3232,7 @@ digits Malayalam Малаялам Simulate Colored Paper Імітувати кольоровий папір Simulate Paper Імітувати папір Double Column Подвійний стовпець + Keep Tools Beside the Canvas Інструменти поруч із полотном Single Left Quotation Mark Одинарна лапка, що відкриває Single Quotes: Одинарні лапки: Single Right Quotation Mark Одинарна лапка, що закриває diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 259881305..f1e49ed9b 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -997,6 +997,7 @@ font-technology Variable Biến thiên Dots Chấm Double Click To Isolate Bấm đúp để cô lập Double Column Hai cột + Keep Tools Beside the Canvas Công cụ cạnh vùng vẽ Double Left Quotation Marks Dấu ngoặc kép trái đôi Double Quotes: Dấu ngoặc kép: Double Right Quotation Marks Dấu ngoặc kép phải đôi diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 22c78c5ac..8b4599a16 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3073,6 +3073,7 @@ digits Malayalam 马拉雅拉姆数字 Simulate Colored Paper 模拟有色纸张 Simulate Paper 模拟纸张 Double Column 双列 + Keep Tools Beside the Canvas 工具置于画布旁 Single Left Quotation Mark 左单引号 Single Quotes: 单引号: Single Right Quotation Mark 右单引号 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 34506cb1e..822483936 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -4106,6 +4106,7 @@ digits Malayalam 馬來亞拉姆數字 Simulate Colored Paper 模擬有色紙張 Simulate Paper 模擬紙張 Double Column 雙欄 + Keep Tools Beside the Canvas 工具置於畫布旁 Single Left Quotation Mark 左單引號 Single Quotes: 單引號: Single Right Quotation Mark 右單引號 diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs index f44359973..10a0015fe 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -477,6 +477,11 @@ fn run_ui_part5(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option flag(&mut app.ui.toolbar_advanced), + "window.toolsBesideCanvas" => opt_bool(p, "on").map(|on| { + let on = on.unwrap_or(!app.ui.tools_beside_canvas); + app.ui.tools_beside_canvas = on; + json!(on) + }), "imageTrace.preview" => opt_bool(p, "on").map(|on| { let on = on.unwrap_or(!app.ui.image_trace_preview); app.ui.image_trace_preview = on; diff --git a/crates/ui-egui/src/menus/state.rs b/crates/ui-egui/src/menus/state.rs index ecaca0c6e..0c392d191 100644 --- a/crates/ui-egui/src/menus/state.rs +++ b/crates/ui-egui/src/menus/state.rs @@ -64,6 +64,7 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option { } "window.toolbar" => app.ui.toolbar, "window.toolbarAdvanced" => app.ui.toolbar_advanced, + "window.toolsBesideCanvas" => app.ui.tools_beside_canvas, "window.toolbarColumns" => match p.get("toolbar").and_then(Value::as_str) { None => app.ui.toolbar_double, Some(name) => crate::custom_toolbars::find(&app.ui, name).is_some_and(|b| b.double), diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 6883f48b0..b3e091493 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -461,6 +461,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "{double?: bool, toolbar?: name} show the toolbar's tools (with `toolbar`, that toolbar made with New Toolbar's) in two columns (true), one (false) or toggle (omitted), as the double arrow at the top of the toolbar does; returns the new state", ), ("window.toolbarAdvanced", "Toolbar: Advanced / Basic", "", "{}"), + ( + "window.toolsBesideCanvas", + "Keep Tools Beside the Canvas", + "", + "{on?: bool} dock the Tools panel between the left-side panels and the canvas (true) or at the window's left edge (false, the default), or toggle (omitted); a floating Tools panel stays floating and docks at the chosen place when it is docked again. Kept with the workspace; a workspace saved without it docks Tools at the window's edge. Returns the new state", + ), ( "window.floatTools", "Float Tool Group", @@ -596,7 +602,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "window.panel.dock", "Dock Panel", "", - "{panel: id or \"tools\", anchor?: id, zone?: tab|center|left|right|top|bottom|above|below, before?: id, group?: bool, column?: bool or n, side?: Start|End, onto?: id, above?: id, below?: id, icon?: bool, collapsed?: bool} dock a panel (with `group`, its whole group): with no destination a floating panel goes back where it floated from (else joins the first dock group) and a hidden one shows where it was; with `anchor` (or `onto`) in the group holding that panel, `zone` as for window.panel.move; with `side`, as a new innermost column of that dock (`column: true` then moves its whole dock column or floating window); `column: n` with no other destination docks its group, or the whole floating window it floats in, as a new dock column at that place (1: next to the icon column, counting inwards; `true`: innermost); `icon: true` puts it in a row of its own at the end of the icon column; `above` / `below` as a group of its own above or below the group holding that panel. `collapsed` then minimizes or restores its group. \"tools\" docks the Tools panel beside the canvas, inside any left-side dock. Returns {layout, closed, floating}", + "{panel: id or \"tools\", anchor?: id, zone?: tab|center|left|right|top|bottom|above|below, before?: id, group?: bool, column?: bool or n, side?: Start|End, onto?: id, above?: id, below?: id, icon?: bool, collapsed?: bool} dock a panel (with `group`, its whole group): with no destination a floating panel goes back where it floated from (else joins the first dock group) and a hidden one shows where it was; with `anchor` (or `onto`) in the group holding that panel, `zone` as for window.panel.move; with `side`, as a new innermost column of that dock (`column: true` then moves its whole dock column or floating window); `column: n` with no other destination docks its group, or the whole floating window it floats in, as a new dock column at that place (1: next to the icon column, counting inwards; `true`: innermost); `icon: true` puts it in a row of its own at the end of the icon column; `above` / `below` as a group of its own above or below the group holding that panel. `collapsed` then minimizes or restores its group. \"tools\" docks the Tools panel at the left edge of the window, or beside the canvas inside any left-side dock while window.toolsBesideCanvas is on. Returns {layout, closed, floating}", ), ( "window.collapseDock", diff --git a/crates/ui-egui/src/panel_docking/tests.rs b/crates/ui-egui/src/panel_docking/tests.rs index 419bd2ff5..98c625743 100644 --- a/crates/ui-egui/src/panel_docking/tests.rs +++ b/crates/ui-egui/src/panel_docking/tests.rs @@ -274,7 +274,7 @@ fn switching_workspace_during_a_tab_drag_keeps_the_new_workspace_layout() { #[test] fn the_tools_panel_floats_by_its_title_bar_and_docks_on_the_left_edge() { let mut h = Harness::new(); - let zone = h.ctx.data(|d| d.get_temp::(crate::tools_float::tools_zone_id())).expect("the Tools panel's docking strip"); + let zone = docked_tools(&h); let title = zone.min + vec2(20.0, 12.0); h.drag(title, pos2(600.0, 400.0)); let [x, y] = h.app.ui.toolbar_pos.expect("the Tools panel floats"); @@ -284,70 +284,186 @@ fn the_tools_panel_floats_by_its_title_bar_and_docks_on_the_left_edge() { assert_eq!(h.app.ui.toolbar_pos, None, "dropped on the left edge, it docks"); } -/// A left-side panel dock leaves the Tools panel between that dock and the document. -#[test] -fn docked_tools_stay_beside_the_canvas_with_left_side_panels() { - let mut h = Harness::new(); - h.app.run("window.panel.dock", json!({"panel": "layers", "side": "Start"})).unwrap(); +/// The docked Tools panel as last drawn. +fn docked_tools(h: &Harness) -> Rect { + h.ctx.data(|d| d.get_temp::(crate::toolbar::docked_rect_id())).expect("the docked Tools panel") +} + +fn settle(h: &mut Harness) { for _ in 0..3 { h.frame(vec![]); } - let tools = h.ctx.data(|d| d.get_temp::(crate::tools_float::tools_zone_id())).unwrap(); - let layers = h.tab("layers").unwrap(); - let canvas = h.app.canvas_rect.unwrap(); - assert!(layers.right() <= tools.left(), "the left dock precedes Tools: {layers:?}, {tools:?}"); - assert!(tools.right() <= canvas.left() && canvas.left() - tools.right() < 40.0, "Tools stays beside the canvas: {tools:?}, {canvas:?}"); +} + +fn keep_tools_beside_canvas(h: &mut Harness, on: bool) { + assert_eq!(h.app.run("window.toolsBesideCanvas", json!({"on": on})).unwrap(), json!(on)); + settle(h); +} + +/// Where docked Tools sits with a left-side dock: `Some(true)` between the dock and the canvas, +/// `Some(false)` at the window's edge outside the dock. +fn tools_place(h: &Harness) -> Option { + let tools = docked_tools(h); + let layers = h.tab("layers")?; + let canvas = h.app.canvas_rect?; + if layers.right() <= tools.left() && tools.right() <= canvas.left() && canvas.left() - tools.right() < 40.0 { + Some(true) + } else if tools.left() < 10.0 && tools.right() <= layers.left() { + Some(false) + } else { + None + } +} + +/// Off by default: docked Tools stays at the window's edge, outside a left-side dock, and the +/// setting moves it between the dock and the canvas and back. +#[test] +fn docked_tools_keep_the_window_edge_unless_kept_beside_the_canvas() { + let mut h = Harness::new(); + assert!(!h.app.ui.tools_beside_canvas); + assert_eq!(crate::menus::checked(&h.app, "window.toolsBesideCanvas", &json!({})), Some(false)); + h.app.run("window.panel.dock", json!({"panel": "layers", "side": "Start"})).unwrap(); + settle(&mut h); + assert_eq!(tools_place(&h), Some(false), "Tools, then the left dock, then the canvas"); + assert_eq!(crate::toolbar::docked_width(&h.app.ui), 0.0, "the canvas reserves no room for Tools"); + keep_tools_beside_canvas(&mut h, true); + assert_eq!(crate::menus::checked(&h.app, "window.toolsBesideCanvas", &json!({})), Some(true)); + assert_eq!(tools_place(&h), Some(true), "the left dock, then Tools, then the canvas"); + keep_tools_beside_canvas(&mut h, false); + assert_eq!(tools_place(&h), Some(false), "back at the window's edge"); } #[test] -fn tools_redock_beside_left_panels_from_either_drop_target() { +fn keep_tools_beside_the_canvas_toggles_and_rejects_a_value_that_is_not_true_or_false() { + let mut h = Harness::new(); + assert_eq!(h.app.run("window.toolsBesideCanvas", json!({})).unwrap(), json!(true)); + assert_eq!(h.app.run("window.toolsBesideCanvas", json!({})).unwrap(), json!(false)); + assert!(h.app.run("window.toolsBesideCanvas", json!({"on": "yes"})).is_err()); + assert!(!h.app.ui.tools_beside_canvas, "a bad value changes nothing"); +} + +/// Off, only the window's edge docks a floating Tools panel; on, the canvas's edge does too, and +/// both dock it beside the canvas. +#[test] +fn tools_redock_at_the_targets_of_the_chosen_placement() { + for beside in [false, true] { + let mut h = Harness::new(); + keep_tools_beside_canvas(&mut h, beside); + h.app.run("window.panel.dock", json!({"panel": "layers", "side": "Start"})).unwrap(); + for at_window_edge in [false, true] { + h.app.run("window.panel.float", json!({"panel": "tools", "x": 600, "y": 300})).unwrap(); + settle(&mut h); + let canvas = h.app.canvas_rect.unwrap(); + let canvas_edge = h.ctx.data(|d| d.get_temp::(crate::tools_float::tools_zone_id())); + assert_eq!(canvas_edge.is_some(), beside, "a canvas-edge target only while kept beside the canvas"); + let canvas_edge = canvas_edge.map_or(pos2(canvas.left() + 12.0, canvas.center().y), |r| r.center()); + let target = if at_window_edge { pos2(12.0, canvas.center().y) } else { canvas_edge }; + h.drag(pos2(620.0, 307.0), target); + if !beside && !at_window_edge { + assert!(h.app.ui.toolbar_pos.is_some(), "the canvas's edge does not dock Tools while the setting is off"); + continue; + } + assert_eq!(h.app.ui.toolbar_pos, None, "beside: {beside}, at the window edge: {at_window_edge}"); + settle(&mut h); + assert_eq!(tools_place(&h), Some(beside), "beside: {beside}, at the window edge: {at_window_edge}"); + } + } +} + +/// Turning the setting off while Tools floats forgets the canvas-edge target, and turning it on +/// leaves a floating Tools panel floating. +#[test] +fn toggling_while_tools_floats_keeps_it_floating_and_drops_the_canvas_target() { let mut h = Harness::new(); h.app.run("window.panel.dock", json!({"panel": "layers", "side": "Start"})).unwrap(); - for at_window_edge in [false, true] { - h.app.run("window.panel.float", json!({"panel": "tools", "x": 600, "y": 300})).unwrap(); - for _ in 0..3 { - h.frame(vec![]); - } - let zone = h.ctx.data(|d| d.get_temp::(crate::tools_float::tools_zone_id())).unwrap(); - let target = if at_window_edge { pos2(12.0, zone.center().y) } else { zone.center() }; - h.drag(pos2(620.0, 307.0), target); - assert_eq!(h.app.ui.toolbar_pos, None, "redock at window edge: {at_window_edge}"); - let tools = h.ctx.data(|d| d.get_temp::(crate::tools_float::tools_zone_id())).unwrap(); - assert!(h.tab("layers").unwrap().right() <= tools.left()); - assert!(tools.right() <= h.app.canvas_rect.unwrap().left()); - } + keep_tools_beside_canvas(&mut h, true); + h.app.run("window.panel.float", json!({"panel": "tools", "x": 600, "y": 300})).unwrap(); + settle(&mut h); + let zone = h.ctx.data(|d| d.get_temp::(crate::tools_float::tools_zone_id())).expect("the canvas-edge target"); + keep_tools_beside_canvas(&mut h, false); + assert!(h.app.ui.toolbar_pos.is_some(), "turning it off leaves Tools floating"); + assert!(h.ctx.data(|d| d.get_temp::(crate::tools_float::tools_zone_id())).is_none(), "the canvas-edge target is gone"); + h.drag(pos2(620.0, 307.0), zone.center()); + assert!(h.app.ui.toolbar_pos.is_some(), "the old canvas-edge strip docks nothing"); + let [x, y] = h.app.ui.toolbar_pos.unwrap(); + keep_tools_beside_canvas(&mut h, true); + assert_eq!(h.app.ui.toolbar_pos, Some([x, y]), "turning it on leaves Tools floating where it was"); + keep_tools_beside_canvas(&mut h, false); + h.drag(pos2(x + 20.0, y + 7.0), pos2(12.0, zone.center().y)); + assert_eq!(h.app.ui.toolbar_pos, None); + settle(&mut h); + assert_eq!(tools_place(&h), Some(false)); +} + +/// The setting is kept with a workspace; state and workspaces saved without it keep Tools at the +/// window's edge. +#[test] +fn keep_tools_beside_the_canvas_is_saved_and_off_in_state_saved_without_it() { + let mut saved = serde_json::to_value(crate::state::UiState { tools_beside_canvas: true, ..Default::default() }).unwrap(); + assert_eq!(saved["tools_beside_canvas"], json!(true)); + let reloaded: crate::state::UiState = serde_json::from_value(saved.clone()).unwrap(); + assert!(reloaded.tools_beside_canvas); + saved.as_object_mut().unwrap().remove("tools_beside_canvas"); + let old: crate::state::UiState = serde_json::from_value(saved).unwrap(); + assert!(!old.tools_beside_canvas && old.toolbar_pos.is_none(), "older state docks Tools at the window's edge"); + + let mut h = Harness::new(); + h.app.run("window.panel.dock", json!({"panel": "layers", "side": "Start"})).unwrap(); + keep_tools_beside_canvas(&mut h, true); + let mut workspace = serde_json::to_value(crate::workspaces::capture(&h.app.ui, "Mine")).unwrap(); + assert_eq!(workspace["toolsBesideCanvas"], json!(true)); + workspace.as_object_mut().unwrap().remove("toolsBesideCanvas"); + let old: crate::workspaces::Workspace = serde_json::from_value(workspace).unwrap(); + assert!(!old.tools_beside_canvas, "a workspace saved without it docks Tools at the window's edge"); + + h.app.run("window.workspace.new", json!({"name": "Mine"})).unwrap(); + h.app.run("window.workspace", json!({"name": "Essentials"})).unwrap(); + settle(&mut h); + assert!(!h.app.ui.tools_beside_canvas, "built-in workspaces keep Tools at the window's edge"); + h.app.run("window.workspace", json!({"name": "Mine"})).unwrap(); + settle(&mut h); + assert!(h.app.ui.tools_beside_canvas); + assert_eq!(tools_place(&h), Some(true), "the saved workspace brings Tools back beside the canvas"); } #[test] fn tools_follow_left_panels_when_resized_hidden_and_restored() { - let mut h = Harness::new(); - h.app.run("window.panel.dock", json!({"panel": "layers", "side": "Start"})).unwrap(); - gesture(&mut h.app, Action::ResizeColumn { panel: "layers".into(), width: 430.0 }).unwrap(); - h.app.ui.toolbar_double = true; - for hidden in [false, true, false] { - if hidden == h.app.ui.dock { - h.app.run("window.panels", json!({})).unwrap(); + for (beside, double) in [(false, false), (false, true), (true, false), (true, true)] { + let mut h = Harness::new(); + keep_tools_beside_canvas(&mut h, beside); + h.app.run("window.panel.dock", json!({"panel": "layers", "side": "Start"})).unwrap(); + gesture(&mut h.app, Action::ResizeColumn { panel: "layers".into(), width: 430.0 }).unwrap(); + h.app.ui.toolbar_double = double; + let width = if double { 80.0 } else { 48.0 }; + for hidden in [false, true, false] { + if hidden == h.app.ui.dock { + h.app.run("window.panels", json!({})).unwrap(); + } + settle(&mut h); + let tools = docked_tools(&h); + assert!((tools.width() - (width + 6.0)).abs() < 1.0, "{} columns: {tools:?}", if double { 2 } else { 1 }); + if hidden { + assert!(h.tab("layers").is_none()); + assert!(tools.left() < 10.0); + } else { + assert_eq!(tools_place(&h), Some(beside), "beside: {beside}, double: {double}"); + assert!(h.tab("layers").unwrap().width() > 0.0); + } + let canvas = h.app.canvas_rect.unwrap(); + assert!(canvas.left() - tools.left() >= width, "Tools has its own room: {tools:?}, {canvas:?}"); + assert!(canvas.width() > 200.0); } - for _ in 0..3 { - h.frame(vec![]); - } - let tools = h.ctx.data(|d| d.get_temp::(crate::tools_float::tools_zone_id())).unwrap(); - if hidden { - assert!(h.tab("layers").is_none()); - assert!(tools.left() < 10.0); - } else { - assert!(h.tab("layers").unwrap().right() <= tools.left()); - } - let canvas = h.app.canvas_rect.unwrap(); - assert!(canvas.left() - tools.left() >= 80.0, "double-width Tools has its own room"); - assert!(canvas.width() > 200.0); } } +/// A layout with panels above or below the canvas: the full-height strip along the window's left +/// edge docks Tools in both placements; off, Tools runs the window's height beside those panels, +/// and on, it stays in the canvas's row. #[test] fn tools_redock_at_window_edge_above_and_below_canvas() { - for above in [true, false] { + for (beside, above) in [(false, true), (false, false), (true, true), (true, false)] { let mut h = Harness::new(); + keep_tools_beside_canvas(&mut h, beside); let tabs = |panel: &str| Box::new(Node::Tabs { panels: vec![panel.to_string()], active: 0 }); h.app.ui.docking = Some(Layout { root: Some(Node::Split { @@ -372,7 +488,17 @@ fn tools_redock_at_window_edge_above_and_below_canvas() { let canvas = h.app.canvas_rect.unwrap(); assert!(panel.center().y < canvas.top() || panel.center().y > canvas.bottom()); h.drag(pos2(620.0, 307.0), pos2(12.0, panel.center().y)); - assert_eq!(h.app.ui.toolbar_pos, None, "window-edge drop above canvas: {above}"); + assert_eq!(h.app.ui.toolbar_pos, None, "window-edge drop, beside: {beside}, above the canvas: {above}"); + settle(&mut h); + let tools = docked_tools(&h); + let panel = h.tab(if above { "layers" } else { "properties" }).unwrap(); + assert!(tools.left() < 10.0 && tools.right() <= panel.left() + 60.0, "{tools:?}"); + match (beside, above) { + (false, true) => assert!(tools.top() <= panel.top(), "Tools runs past the panel above: {tools:?}, {panel:?}"), + (false, false) => assert!(tools.bottom() >= panel.bottom(), "Tools runs past the panel below: {tools:?}, {panel:?}"), + (true, true) => assert!(tools.top() >= panel.bottom(), "Tools stays below the panel above: {tools:?}, {panel:?}"), + (true, false) => assert!(tools.bottom() <= panel.top(), "Tools stays above the panel below: {tools:?}, {panel:?}"), + } } } @@ -382,6 +508,7 @@ fn capture_tools_beside_left_panels() { let dir = std::path::PathBuf::from(std::env::var_os("LEFT_TOOLS_SCREENSHOTS").unwrap()); let mut app = VectorcraftApp::new(Session::new(), Default::default()); app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + app.run("window.toolsBesideCanvas", json!({"on": true})).unwrap(); app.run("window.panel.dock", json!({"panel": "layers", "side": "Start"})).unwrap(); let mut h = egui_kittest::Harness::builder() .with_size(vec2(1200.0, 800.0)) diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 3ab62e449..64ead689f 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -460,9 +460,14 @@ pub struct UiState { #[serde(default, skip_serializing_if = "Option::is_none")] pub dock_width: Option, /// The Tools panel floats with its top-left corner here (dragged out by its title bar); `None`: - /// docked beside the canvas's left edge, inside any left-side panel dock. + /// docked at the window's left edge, or beside the canvas with [`Self::tools_beside_canvas`]. #[serde(default)] pub toolbar_pos: Option<[f32; 2]>, + /// The docked Tools panel sits between the left-side panels and the canvas instead of at the + /// window's left edge (Window › Toolbars › Keep Tools Beside the Canvas, + /// `window.toolsBesideCanvas`). Off by default, and off in state saved without it. + #[serde(default)] + pub tools_beside_canvas: bool, pub status_bar: bool, pub dock: bool, /// The bars that Tab (`window.dock`) hid, which the next Tab shows again; none when Tab has not @@ -992,6 +997,7 @@ impl Default for UiState { docking_generation: 0, dock_width: None, toolbar_pos: None, + tools_beside_canvas: false, status_bar: true, dock: true, tab_hid: None, diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 82014ddee..dce5912ba 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -95,19 +95,26 @@ pub fn remember(app: &mut VectorcraftApp, id: &str) { crate::custom_toolbars::remember(&mut app.ui, id); } +/// The docked Tools panel's rect as last laid out; removed while it floats. +pub(crate) fn docked_rect_id() -> egui::Id { + egui::Id::unique("toolbar-docked-rect") +} + fn panel_width(ui: &crate::state::UiState) -> f32 { if ui.toolbar_double { 80.0 } else { WIDTH } } -/// Space reserved inside the protected canvas panel for a visible docked Tools panel. +/// Space reserved inside the protected canvas panel for a visible Tools panel docked beside the +/// canvas (`UiState::tools_beside_canvas`); none while Tools docks at the window's edge. pub(crate) fn docked_width(ui: &crate::state::UiState) -> f32 { - if ui.toolbar && ui.screen_mode < 3 && ui.toolbar_pos.is_none() { panel_width(ui) + 6.0 } else { 0.0 } + if ui.tools_beside_canvas && ui.toolbar && ui.screen_mode < 3 && ui.toolbar_pos.is_none() { panel_width(ui) + 6.0 } else { 0.0 } } -/// Register redocking targets even while Tools floats; a docked panel follows the canvas's -/// left edge as dock columns resize, collapse or hide. +/// With `UiState::tools_beside_canvas` on, the canvas-edge redocking target, registered even +/// while Tools floats, and the docked panel, which follows the canvas's left edge as dock columns +/// resize, collapse or hide. With it off, `app.rs` draws Tools before the docking layout. pub(crate) fn show_beside_canvas(app: &mut VectorcraftApp, ui: &mut Ui) { - if app.ui.toolbar && app.ui.screen_mode < 3 { + if app.ui.tools_beside_canvas && app.ui.toolbar && app.ui.screen_mode < 3 { crate::tools_float::set_dock_zones(ui.ctx(), ui.available_rect_before_wrap(), WIDTH); if app.ui.toolbar_pos.is_none() { show(app, ui); @@ -125,11 +132,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { match app.ui.toolbar_pos { None => { let margin = egui::Margin { left: 6, right: 0, top: 6, bottom: 6 }; - egui::Panel::left("toolbar").resizable(false).exact_size(w + 6.0).frame(egui::Frame::NONE).show(ui, |ui| { + let docked = egui::Panel::left("toolbar").resizable(false).exact_size(w + 6.0).frame(egui::Frame::NONE).show(ui, |ui| { crate::theme::card(ui, &t, margin, |ui| body(app, ui, cols, None)); }); + ui.ctx().data_mut(|d| d.insert_temp(docked_rect_id(), docked.response.rect)); } Some(pos) => { + ui.ctx().data_mut(|d| d.remove::(docked_rect_id())); // Floating, kept inside the window, its tools scrolling when they don't fit under it. let ctx = ui.ctx().clone(); let id = egui::Id::unique("toolbar-floating"); diff --git a/crates/ui-egui/src/tools_float.rs b/crates/ui-egui/src/tools_float.rs index c6b4422ba..13108368f 100644 --- a/crates/ui-egui/src/tools_float.rs +++ b/crates/ui-egui/src/tools_float.rs @@ -1,9 +1,10 @@ -//! The Tools panel floats by its title bar and docks beside the canvas's left edge +//! The Tools panel floats by its title bar and docks again on the window's left edge //! ([`crate::toolbar`]). It stays outside the docking layout: dragging its title bar out of the //! docked panel floats it under the pointer, a floating panel's title bar moves it, and a release -//! over the canvas's left edge or the window's left edge (lit up) docks it beside the canvas. -//! Escape or losing the -//! window's focus puts it back where the drag began. +//! over the strip along the window's left edge (lit up) docks it there. With Keep Tools Beside the +//! Canvas on (`UiState::tools_beside_canvas`), a release over the canvas's left edge docks it too, +//! and either release docks it beside the canvas. Escape or losing the window's focus puts it back +//! where the drag began. //! //! `window.panel.float {panel: "tools"}` / `window.panel.dock {panel: "tools"}` do the same for //! agents. Its position is kept with the preferences and in user workspaces, and a position saved @@ -42,7 +43,8 @@ pub(crate) fn escape_cancelled_move(ctx: &egui::Context) -> bool { ctx.data(|d| d.get_temp::(escape_cancel_id())) == Some(ctx.cumulative_frame_nr_for(ctx.viewport_id())) } -/// The strip along the canvas's left edge where the Tools panel docks, as last laid out. +/// The strip along the canvas's left edge where the Tools panel docks while Keep Tools Beside the +/// Canvas is on, as last laid out. pub(crate) fn tools_zone_id() -> Id { Id::unique("tools-float-zone") } @@ -58,6 +60,12 @@ pub(crate) fn set_dock_zones(ctx: &egui::Context, canvas: Rect, width: f32) { }); } +/// Forget the canvas-edge target, so a strip laid out while Keep Tools Beside the Canvas was on +/// does not dock Tools after it is turned off. +pub(crate) fn clear_canvas_dock_zone(ctx: &egui::Context) { + ctx.data_mut(|d| d.remove::(tools_zone_id())); +} + /// The legacy 48-point window-edge target spans the full layout below the bars, including /// panels above and below the canvas. Its width does not follow the toolbar's column count. pub(crate) fn set_window_dock_zone(ctx: &egui::Context, window: Rect) { @@ -108,9 +116,13 @@ pub(crate) fn drag_tools(app: &mut VectorcraftApp, ctx: &egui::Context, title: & } } -/// The docking strip under `at`, if any. -fn drop_zone(ctx: &egui::Context, at: Pos2) -> Option { - ctx.data(|d| [d.get_temp::(tools_zone_id()), d.get_temp::(window_zone_id())].into_iter().flatten().find(|r| r.contains(at))) +/// The docking strip under `at`, if any: the window's left edge, and with `beside_canvas` the +/// canvas's left edge as well. +fn drop_zone(ctx: &egui::Context, at: Pos2, beside_canvas: bool) -> Option { + ctx.data(|d| { + let canvas = d.get_temp::(tools_zone_id()).filter(|_| beside_canvas); + [canvas, d.get_temp::(window_zone_id())].into_iter().flatten().find(|r| r.contains(at)) + }) } fn highlight(ctx: &egui::Context, zone: Rect) { @@ -134,7 +146,7 @@ pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) { return; } let at = ctx.input(|i| i.pointer.interact_pos()); - let zone = at.filter(|at| at.distance(m.from) > TRAVEL).and_then(|at| drop_zone(ctx, at)); + let zone = at.filter(|at| at.distance(m.from) > TRAVEL).and_then(|at| drop_zone(ctx, at, app.ui.tools_beside_canvas)); if ctx.input(|i| i.pointer.primary_down()) { if let Some(p) = at.map(|at| at - m.grab) && p.x.is_finite() diff --git a/crates/ui-egui/src/workspaces.rs b/crates/ui-egui/src/workspaces.rs index d7590ff3b..d13e6a688 100644 --- a/crates/ui-egui/src/workspaces.rs +++ b/crates/ui-egui/src/workspaces.rs @@ -57,6 +57,9 @@ pub struct Workspace { pub docking_collapsed: std::collections::BTreeSet, /// Where the Tools panel floats (built-in workspaces: docked). pub toolbar_pos: Option<[f32; 2]>, + /// The docked Tools panel sits beside the canvas, inside any left-side panels (built-in + /// workspaces and workspaces saved without it: at the window's left edge). + pub tools_beside_canvas: bool, /// Main dock column width in screen points while `docking` is none (#1753). #[serde(default, skip_serializing_if = "Option::is_none")] pub dock_width: Option, @@ -197,6 +200,7 @@ pub fn capture(ui: &UiState, name: &str) -> Workspace { docking_icons: Default::default(), docking_collapsed: Default::default(), toolbar_pos: ui.toolbar_pos, + tools_beside_canvas: ui.tools_beside_canvas, dock_width: ui.dock_width, toolbars: ui.custom_toolbars.clone(), } @@ -223,6 +227,7 @@ pub fn apply(ui: &mut UiState, w: &Workspace) { ui.docking_hidden = w.docking_hidden.clone(); ui.docking_hidden_groups = w.docking_hidden_groups.clone(); ui.toolbar_pos = w.toolbar_pos; + ui.tools_beside_canvas = w.tools_beside_canvas; ui.dock_width = w.dock_width; crate::state::sanitize_toolbar_pos(&mut ui.toolbar_pos); ui.custom_toolbars = w.toolbars.clone(); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 5f70424ce..a799f22ff 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -429,7 +429,9 @@ below). Dragging a tab, a group's drag bar or an icon out of the dock and droppi floats it; dropping it on a group's tabs, on a group's upper or lower edge or beside a column docks it there. On the desktop each floating window is an OS window of its own; on the web and on Wayland it floats inside the app window. `ui.toolbar_pos` in `ui.inspect` is the Tools panel's corner (null: docked); `window.panel.float` and -`window.panel.dock` with `panel: "tools"` float it at `x`, `y` or dock it on the window's left edge. +`window.panel.dock` with `panel: "tools"` float it at `x`, `y` or dock it on the window's left edge. With +`window.toolsBesideCanvas {on?}` (Window › Toolbars › Keep Tools Beside the Canvas, off by default; `ui.tools_beside_canvas`) +the docked Tools panel sits between left-side panels and the canvas, and `window.panel.dock` docks it there. Flatten Transparency: `ui.flattenTransparencyDialog` opens the `flattenTransparency` dialog for the selection (fields `preset`: a preset name, setting it loads that preset's options; the option keys of diff --git a/docs/mcp.md b/docs/mcp.md index 4908480fa..79159150e 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1693,6 +1693,21 @@ changes neither option. Behavior reference: [View artwork](https://helpx.adobe.c {"name":"invoke_menu","arguments":{"command":"ui.navigatorPanelOptions","params":{"color":"Light Blue","solidDashes":true}}} ``` +## Tools panel placement + +By default the docked Tools panel sits at the window's left edge, outside any panels docked on the left. +`window.toolsBesideCanvas {on?}` (Window › Toolbars › Keep Tools Beside the Canvas, remote mode, `invoke_menu`) +docks it between those panels and the canvas instead (`on: true`), back at the window's edge (`on: false`), or +toggles it (no `on`), and answers the new state. A value of `on` that is not true or false is an error and changes +nothing. With the setting on, a floating Tools panel docks when it is dropped on the canvas's left edge as well as +on the window's left edge, and `window.panel.dock {panel: "tools"}` docks it beside the canvas. A floating Tools +panel stays floating when the setting changes. The setting is VectorCraft's own layout option, off by default; it is +kept with the workspace, and built-in workspaces and workspaces saved without it keep Tools at the window's edge. + +```json +{"name":"invoke_menu","arguments":{"command":"window.toolsBesideCanvas","params":{"on":true}}} +``` + ## Ruler guides Ruler guides are numbered in the order they were made. `guide.add {vertical, pos, artboard?}` makes one (the x of a diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index b4dfd8d85..6a5cf1bb7 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -695,6 +695,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - The dock's double arrow collapses Properties, Layers and Libraries to icons at the top of the icon column, popping out like the other icon panels, and expands them again (`window.collapseDock`, kept with the preferences and in saved workspaces). The icon column's own arrow (expanding the icon panels into a full column) is not done. - Window › Toolbars › New Toolbar… and New Toolbar… in the All Tools drawer's menu ask for a name and make a blank toolbar that floats on its own, moved by its title bar and listed under Window › Toolbars, whose item shows or hides it. The title bar's × hides it, and its double arrow shows its tools in one column or two. Its ••• button opens the All Tools drawer beside it, and tools dragged from the drawer fill it. The toolbars are kept with the workspace they were made in: switching workspaces shows that workspace's toolbars, Reset Workspace keeps them, and New Workspace starts the new workspace with a copy of them (; `window.toolbar.new`, `window.toolbar.custom`, the `toolbar` param of `window.editToolbar` and `toolbar.addTool`/`removeTool`/`reset`; `tools_added_to_a_new_toolbar_survive_a_workspace_reload`). - Window › Toolbars › Manage Toolbars… and Manage Toolbars… in the All Tools drawer's menu open a dialog that lists the toolbars made with New Toolbar: a new name typed for the selected toolbar renames it, + copies it with its tools (with none selected, + makes a blank toolbar), and the trash can deletes it. Each change shows under Window › Toolbars and stays in the workspace (; `window.toolbar.manage`, `window.toolbar.rename`, `window.toolbar.duplicate`, `window.toolbar.delete`; `manage_toolbars_renames_duplicates_and_deletes`). + - Window › Toolbars › Keep Tools Beside the Canvas, a VectorCraft layout option that is off by default, docks the Tools panel between left-side panels and the canvas instead of at the window's left edge; the canvas's left edge then also docks a floating Tools panel, and workspaces keep the setting. Scores are unchanged (`window.toolsBesideCanvas`; `docked_tools_keep_the_window_edge_unless_kept_beside_the_canvas`, `tools_redock_at_the_targets_of_the_chosen_placement`). - Hindi (2026-11-06): Hindi (`hi`): 7,877-row machine-assisted catalog, zero/one singular plurals, desktop font fallback and optional Devanagari craft-fonts fallback; all 55 translation tests pass; native review and a web font remain; scores and estimates unchanged. [Terminology and limitations](localization-hi.md). - Czech render-site labels: desktop/web file filters, diagnostics, suggested preset names and contributor tooltips/plurals now use catalog lookups; gradients have Czech adjective agreement and printer marks have a separate context (PDF/Print and Summary). Search, Home, toolbar/panel tooltips and Preferences already look up their labels. Every complete catalog has rows for these labels and for the Roman and Japanese printer mark styles. Evidence: `i18n::tests::czech_render_site_follow_up`, `i18n::tests::complete_catalogs_translate_render_site_labels`, `dialogs::tests_modal::printer_mark_labels_are_distinct_from_language_names`; see [localization-parity.md](localization-parity.md). - The History panel, Edit › Undo and Edit › Redo translate the names of the steps that apply or redefine a character or paragraph style and of the step that redefines a graphic style: each such step keeps its template (`Apply {name}`, `Redefine {name}`, `Redefine Graphic Style “{name}”`) and the style's name, every complete catalog has a row for each template, and the panel and the menu show the style's name as the document holds it, except the Normal styles' names, which they show in the interface language (`steps_named_after_a_style_list_the_template_around_the_name`, `undo_and_redo_name_a_style_step_around_the_style_name`).