mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 19:57:05 -06:00
Docking: panels on craft-ui's shared docking, floating panels in OS windows (#4135)
* Build: craft-libs at 2869b8cc Pin craft-pdf, craft-print, craft-docpath, craft-ui, craft-keymap, craft-window and the patched hayro/krilla backends to craft-libs 2869b8cc1077530bd07a7e192fb2b0e1304a1354, with the pdf-deps gate, NOTICE, ASSETS.md and docs/pdf-backend.md at the same revision. PDF: no modification date beyond the creation date, DeviceN images are not JPEG-encoded, and clipping text draws nothing (its outlines arrive as a clip path). * Docking: compile against craft-ui at 2869b8cc Floating windows are trees of groups: window_panels and window_active read one as a flat group, and windows rebuilt from the legacy groups keep their ids. Unknown dock actions are rejected, the new ones are checked like the old, and every panel a close action removes keeps its return location. * Docking: panels and canvas on craft-ui's docking area * Docking: tests for the dock area, flyouts, floating panels and the Tools panel * Docking: catalogs without the old dock's strings * Build: craft-libs at 8a503cfe * Docking: floating panels in OS windows of their own on the desktop * Docking: modal dialogs block panel windows; their keys and dropped files reach the main window * Docking: art dragged onto a panel in its own window drops there * Docking: module doc names the OS windows * Build: craft-libs at a042cf26 * Docking: onto, above, below, collapsed, icon and column places on Float and Dock Panel; ui.docking * Docking: command descriptions of Float, Dock, Move and Collapse Panel for the craft-ui layout * Docking: control protocol describes the craft-ui layout, its commands and ui.docking * Docking: rustfmt * Docking: layouts saved before the canvas joined convert on load Saved ui.json files and workspaces from before the shared layout held the canvas (a dock-only tree plus floating_panels, docking_icons, docking_collapsed, dock_collapsed and dock_width) convert into one layout: the dock tree's groups in the main column, the icon column with the panels dropped on it, floating groups and sets, sets docked as columns, collapsed groups, the dock width and the places of closed panels. The legacy fields are read, never written. A ui.json read only in part is copied to ui.unreadable.json before anything saves over it. * Docking: catalogs without the old collapse message * Docking: converted floating sets start at most 720 points tall * Docking: closed icon panels convert into the icon column; old floating rects, set heights and a copy of the file before conversion * Build: craft-libs at 27e60765 * Docking: dock column places and icon count from the icon column; a docked column moves to a later place * Build: craft-libs at d24e4bcc * Build: craft-libs at 5bbe9f62 * Docs: floating panels in OS windows of their own close the other-monitor gap * Docking: keys forwarded from a floating panel's OS window run shortcuts while the main window is unfocused
This commit is contained in:
@@ -352,20 +352,20 @@ The shared PDF backend keeps the existing standard-font and CMap features. These
|
||||
|
||||
| Crate / file | Author | Source | Licence | SHA-256 | Use |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitDingbats.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/assets/FoxitDingbats.pfb | BSD-3-Clause | `845c752392b6c914fb989c75a08b7792b88f542d2499042ef2889f8c814a16ed` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitFixed.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/assets/FoxitFixed.pfb | BSD-3-Clause | `b6c8fe53f134b8b6d4578cd2d544df4cee9624c4efa8d51a560fb40ea296101b` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitFixedBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/assets/FoxitFixedBold.pfb | BSD-3-Clause | `f1b7159702973f54fc86254ea38bcf3712b2a736eb3a0995751e9f4bb45ad603` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitFixedBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/assets/FoxitFixedBoldItalic.pfb | BSD-3-Clause | `8a000945843bd31add06aee63bf9fd41b7578b4f3242a4b7cd46349ad9d24d4d` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitFixedItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/assets/FoxitFixedItalic.pfb | BSD-3-Clause | `5007faf8320fa1fcc08b8894b356ad976f60b992ba29ee5272578bbdebaf3876` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSans.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/assets/FoxitSans.pfb | BSD-3-Clause | `a02339e95d20db14cbdfc1c6baf359cd36a091d5434925c3a4040f60e1a1af9e` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSansBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/assets/FoxitSansBold.pfb | BSD-3-Clause | `7bdfa36de1d5757dcfaf8b8a4be95c6e41c40fa70dbed9d78bf979a7b9959054` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSansBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/assets/FoxitSansBoldItalic.pfb | BSD-3-Clause | `555ce45924338e2e4b32821910221ce85da691fd59a9723c4c971a7c5627fc8b` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSansItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/assets/FoxitSansItalic.pfb | BSD-3-Clause | `7751cbefeda351708dae8c4a88db043411bc39ab2ae2097685da845d4d16f307` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSerif.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/assets/FoxitSerif.pfb | BSD-3-Clause | `4f57d2b9d884af8f907bf22df6019b52d86cbf6214fdbd02b1ae05472a543f35` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSerifBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/assets/FoxitSerifBold.pfb | BSD-3-Clause | `0bdf4b04e964139818d51eda03d566ba999fc3ba2421b1c6c51f9dc969022e80` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSerifBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/assets/FoxitSerifBoldItalic.pfb | BSD-3-Clause | `a406cac82583bf98175cb62c87ed5e95c45fbb34eece63a10b0a13e793cb2e10` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSerifItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/assets/FoxitSerifItalic.pfb | BSD-3-Clause | `610ae0687198045c4db5d1a6650fd5e92536631706382eb8b72e38df578d0ae9` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSymbol.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/assets/FoxitSymbol.pfb | BSD-3-Clause | `47967d055530e7357088a08403115425643ec2cdfd6201ba8af0fbd7116c1539` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/CGATS001Compat-v2-micro.icc` | Clinton Ingram (saucecontrol/Compact-ICC-Profiles) | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc | CC0-1.0 | `73e1ba37d2bad5bab2a964f40a9eed96209666efc067c3322626214bbef234a0` | Default CMYK colour conversion (pinned hayro-interpret) |
|
||||
| `hayro-cmap 0.1.0 / assets/cmaps.brotli` | Adobe | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-cmap/assets/cmaps.brotli | BSD-3-Clause | `ddf37af521d904d5646c2fc37b2b7e912dc1b56f0d210190b61f598154758e46` | Character-code mapping tables required to read CJK PDFs. Non-visual dependency data; existing embedding unchanged |
|
||||
| `hayro-interpret 0.7.0 / src/font/generated/metrics.rs` | Adobe (Core 14 AFM metrics, PDF specification encodings); tables by The Hayro Authors | https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/hayro-interpret/src/font/generated/metrics.rs | MIT OR Apache-2.0 | `2ec9e608573eda459de1e1d9bda3bb494315e36bee885acc88912a11fe0caa41` | Glyph widths and encodings for non-embedded standard fonts. Non-visual dependency data; existing embedding unchanged |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitDingbats.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitDingbats.pfb | BSD-3-Clause | `845c752392b6c914fb989c75a08b7792b88f542d2499042ef2889f8c814a16ed` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitFixed.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitFixed.pfb | BSD-3-Clause | `b6c8fe53f134b8b6d4578cd2d544df4cee9624c4efa8d51a560fb40ea296101b` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitFixedBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitFixedBold.pfb | BSD-3-Clause | `f1b7159702973f54fc86254ea38bcf3712b2a736eb3a0995751e9f4bb45ad603` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitFixedBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitFixedBoldItalic.pfb | BSD-3-Clause | `8a000945843bd31add06aee63bf9fd41b7578b4f3242a4b7cd46349ad9d24d4d` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitFixedItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitFixedItalic.pfb | BSD-3-Clause | `5007faf8320fa1fcc08b8894b356ad976f60b992ba29ee5272578bbdebaf3876` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSans.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSans.pfb | BSD-3-Clause | `a02339e95d20db14cbdfc1c6baf359cd36a091d5434925c3a4040f60e1a1af9e` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSansBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSansBold.pfb | BSD-3-Clause | `7bdfa36de1d5757dcfaf8b8a4be95c6e41c40fa70dbed9d78bf979a7b9959054` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSansBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSansBoldItalic.pfb | BSD-3-Clause | `555ce45924338e2e4b32821910221ce85da691fd59a9723c4c971a7c5627fc8b` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSansItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSansItalic.pfb | BSD-3-Clause | `7751cbefeda351708dae8c4a88db043411bc39ab2ae2097685da845d4d16f307` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSerif.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSerif.pfb | BSD-3-Clause | `4f57d2b9d884af8f907bf22df6019b52d86cbf6214fdbd02b1ae05472a543f35` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSerifBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSerifBold.pfb | BSD-3-Clause | `0bdf4b04e964139818d51eda03d566ba999fc3ba2421b1c6c51f9dc969022e80` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSerifBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSerifBoldItalic.pfb | BSD-3-Clause | `a406cac82583bf98175cb62c87ed5e95c45fbb34eece63a10b0a13e793cb2e10` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSerifItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSerifItalic.pfb | BSD-3-Clause | `610ae0687198045c4db5d1a6650fd5e92536631706382eb8b72e38df578d0ae9` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/FoxitSymbol.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSymbol.pfb | BSD-3-Clause | `47967d055530e7357088a08403115425643ec2cdfd6201ba8af0fbd7116c1539` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) |
|
||||
| `hayro-interpret 0.7.0 / assets/CGATS001Compat-v2-micro.icc` | Clinton Ingram (saucecontrol/Compact-ICC-Profiles) | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc | CC0-1.0 | `73e1ba37d2bad5bab2a964f40a9eed96209666efc067c3322626214bbef234a0` | Default CMYK colour conversion (pinned hayro-interpret) |
|
||||
| `hayro-cmap 0.1.0 / assets/cmaps.brotli` | Adobe | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-cmap/assets/cmaps.brotli | BSD-3-Clause | `ddf37af521d904d5646c2fc37b2b7e912dc1b56f0d210190b61f598154758e46` | Character-code mapping tables required to read CJK PDFs. Non-visual dependency data; existing embedding unchanged |
|
||||
| `hayro-interpret 0.7.0 / src/font/generated/metrics.rs` | Adobe (Core 14 AFM metrics, PDF specification encodings); tables by The Hayro Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/src/font/generated/metrics.rs | MIT OR Apache-2.0 | `2ec9e608573eda459de1e1d9bda3bb494315e36bee885acc88912a11fe0caa41` | Glyph widths and encodings for non-embedded standard fonts. Non-visual dependency data; existing embedding unchanged |
|
||||
|
||||
Generated
+26
-20
@@ -217,7 +217,7 @@ dependencies = [
|
||||
"objc2-foundation 0.3.2",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
"wl-clipboard-rs",
|
||||
"x11rb",
|
||||
]
|
||||
@@ -950,12 +950,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-docpath"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=82a24b006eefa036b43c09a67fb9a3fbd17ffe63#82a24b006eefa036b43c09a67fb9a3fbd17ffe63"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
|
||||
|
||||
[[package]]
|
||||
name = "craft-keymap"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=25da1314d443171b4adeaad2cd9454b273f35c6f#25da1314d443171b4adeaad2cd9454b273f35c6f"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"serde",
|
||||
@@ -965,7 +965,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-pdf"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=2eca9b3a53a98ac52efed2a27353c6a6927ead10#2eca9b3a53a98ac52efed2a27353c6a6927ead10"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
|
||||
dependencies = [
|
||||
"hayro-interpret",
|
||||
"hayro-syntax",
|
||||
@@ -976,7 +976,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-print"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=25da1314d443171b4adeaad2cd9454b273f35c6f#25da1314d443171b4adeaad2cd9454b273f35c6f"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
|
||||
dependencies = [
|
||||
"craft-winprint-gdi",
|
||||
"miniz_oxide 0.8.9",
|
||||
@@ -989,18 +989,24 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-ui"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=25da1314d443171b4adeaad2cd9454b273f35c6f#25da1314d443171b4adeaad2cd9454b273f35c6f"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"log",
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-foundation 0.3.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-window"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=25da1314d443171b4adeaad2cd9454b273f35c6f#25da1314d443171b4adeaad2cd9454b273f35c6f"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
@@ -1481,7 +1487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2080,7 +2086,7 @@ checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a"
|
||||
[[package]]
|
||||
name = "hayro-cmap"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=2eca9b3a53a98ac52efed2a27353c6a6927ead10#2eca9b3a53a98ac52efed2a27353c6a6927ead10"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"hayro-postscript",
|
||||
@@ -2089,7 +2095,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "hayro-interpret"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=2eca9b3a53a98ac52efed2a27353c6a6927ead10#2eca9b3a53a98ac52efed2a27353c6a6927ead10"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"hayro-cmap",
|
||||
@@ -2107,7 +2113,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "hayro-jbig2"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=2eca9b3a53a98ac52efed2a27353c6a6927ead10#2eca9b3a53a98ac52efed2a27353c6a6927ead10"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
|
||||
dependencies = [
|
||||
"fearless_simd",
|
||||
"hayro-ccitt",
|
||||
@@ -2125,12 +2131,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "hayro-postscript"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=2eca9b3a53a98ac52efed2a27353c6a6927ead10#2eca9b3a53a98ac52efed2a27353c6a6927ead10"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
|
||||
|
||||
[[package]]
|
||||
name = "hayro-syntax"
|
||||
version = "0.7.2"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=2eca9b3a53a98ac52efed2a27353c6a6927ead10#2eca9b3a53a98ac52efed2a27353c6a6927ead10"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"hayro-ccitt",
|
||||
@@ -2667,7 +2673,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "krilla"
|
||||
version = "0.8.2"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=2eca9b3a53a98ac52efed2a27353c6a6927ead10#2eca9b3a53a98ac52efed2a27353c6a6927ead10"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bumpalo",
|
||||
@@ -3432,7 +3438,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3916,7 +3922,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4273,7 +4279,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4330,7 +4336,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5176,7 +5182,7 @@ dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix 1.1.5",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6840,7 +6846,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+12
-20
@@ -11,22 +11,14 @@ rust-version = "1.95"
|
||||
repository = "https://github.com/storytold/vectorcraft"
|
||||
|
||||
[workspace.dependencies]
|
||||
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "2eca9b3a53a98ac52efed2a27353c6a6927ead10", default-features = false, features = ["interpret", "write"] }
|
||||
# Kept at the egui-0.37 port revision on purpose: 25da contains 4f85efb's bounded print
|
||||
# parsing and 2bbef53's printer names; d991bf67 (VectorCraft main's pin) lacks both. This is a
|
||||
# targeted security/UI bump, not an all-libs flatten. Re-check if main backports them.
|
||||
craft-print = { git = "https://github.com/storytold/craft-libs", rev = "25da1314d443171b4adeaad2cd9454b273f35c6f" }
|
||||
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "82a24b006eefa036b43c09a67fb9a3fbd17ffe63" }
|
||||
# egui 0.37 port. 25da has craft_ui::capture and the old docking surface
|
||||
# (DockContent/show_customized; Floating {panels, active, rect}) that VectorCraft's
|
||||
# panel_docking.rs uses. Do not bump this to the new-docking L revision without porting that file.
|
||||
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "25da1314d443171b4adeaad2cd9454b273f35c6f", features = ["serde"] }
|
||||
# The egui 0.37 port of craft-keymap. 25da's core keymap API matches 2eca9b3a; only its
|
||||
# `ui` feature and tests differ, and VectorCraft enables only `egui`.
|
||||
craft-keymap = { git = "https://github.com/storytold/craft-libs", rev = "25da1314d443171b4adeaad2cd9454b273f35c6f" }
|
||||
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f", default-features = false, features = ["interpret", "write"] }
|
||||
craft-print = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" }
|
||||
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" }
|
||||
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f", features = ["serde"] }
|
||||
craft-keymap = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" }
|
||||
# Decides who draws the window frame (desktop vs. app) so the suite shares one native titlebar per
|
||||
# desktop. The `egui` feature adds `craft_window::decorate`. (craft-libs#217.)
|
||||
craft-window = { git = "https://github.com/storytold/craft-libs", rev = "25da1314d443171b4adeaad2cd9454b273f35c6f", features = ["egui"] }
|
||||
craft-window = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f", features = ["egui"] }
|
||||
# internal
|
||||
vectorcraft-geom = { path = "crates/geom" }
|
||||
vectorcraft-color = { path = "crates/color" }
|
||||
@@ -148,12 +140,12 @@ lto = "thin"
|
||||
codegen-units = 1
|
||||
|
||||
[patch.crates-io]
|
||||
hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "2eca9b3a53a98ac52efed2a27353c6a6927ead10" }
|
||||
hayro-interpret = { git = "https://github.com/storytold/craft-libs", rev = "2eca9b3a53a98ac52efed2a27353c6a6927ead10" }
|
||||
hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "2eca9b3a53a98ac52efed2a27353c6a6927ead10" }
|
||||
hayro-cmap = { git = "https://github.com/storytold/craft-libs", rev = "2eca9b3a53a98ac52efed2a27353c6a6927ead10" }
|
||||
hayro-postscript = { git = "https://github.com/storytold/craft-libs", rev = "2eca9b3a53a98ac52efed2a27353c6a6927ead10" }
|
||||
krilla = { git = "https://github.com/storytold/craft-libs", rev = "2eca9b3a53a98ac52efed2a27353c6a6927ead10" }
|
||||
hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" }
|
||||
hayro-interpret = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" }
|
||||
hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" }
|
||||
hayro-cmap = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" }
|
||||
hayro-postscript = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" }
|
||||
krilla = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" }
|
||||
ecolor = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
|
||||
emath = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
|
||||
epaint = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" }
|
||||
|
||||
@@ -77,10 +77,10 @@ Shared PDF backend
|
||||
|
||||
The patched hayro-interpret 0.7.0, hayro-syntax 0.7.2, hayro-jbig2 0.3.0, hayro-cmap 0.1.0
|
||||
and hayro-postscript 0.1.0 are fetched from https://github.com/storytold/craft-libs at revision
|
||||
2eca9b3a53a98ac52efed2a27353c6a6927ead10. Their upstream project is
|
||||
a0916808f7d78bed8c7b90cdf937176015454a7f. Their upstream project is
|
||||
https://github.com/LaurenzV/hayro, licensed MIT OR Apache-2.0. The snapshot retains each
|
||||
crate's upstream licence files. The patch ledger is at
|
||||
https://github.com/storytold/craft-libs/blob/2eca9b3a53a98ac52efed2a27353c6a6927ead10/vendor/README.craft-reader.md.
|
||||
https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/README.craft-reader.md.
|
||||
|
||||
Assets already bundled through Hayro's default features retain their licences:
|
||||
- Foxit standard-14 substitutes: Copyright 2014 PDFium Authors. All rights reserved.
|
||||
|
||||
@@ -83,6 +83,23 @@ struct App {
|
||||
maximize_tries: u8,
|
||||
}
|
||||
|
||||
/// The parts of [`App::raw_input_hook`] that every window needs, for the OS windows of floating
|
||||
/// panels: a click or Escape that closes the macOS menu bar's open menu, and the pen barrel
|
||||
/// button's secondary click on Windows. The control channel's input and the native menu's keys
|
||||
/// go to the main window only.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn native_input_hook(app: &VectorcraftApp) -> Option<vectorcraft_ui_egui::NativeInputHook> {
|
||||
app.services.native_menu.is_some().then(|| std::sync::Arc::new(mac_menu::raw_input_hook) as vectorcraft_ui_egui::NativeInputHook)
|
||||
}
|
||||
#[cfg(all(windows, not(target_vendor = "win7")))]
|
||||
fn native_input_hook(_: &VectorcraftApp) -> Option<vectorcraft_ui_egui::NativeInputHook> {
|
||||
Some(std::sync::Arc::new(windows_stylus::raw_input_hook))
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", all(windows, not(target_vendor = "win7")))))]
|
||||
fn native_input_hook(_: &VectorcraftApp) -> Option<vectorcraft_ui_egui::NativeInputHook> {
|
||||
None
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
// Each frame runs unconfined: it is the person at the keyboard's. The app confines what the
|
||||
// control channel asks for, and the frames carrying input it injected, to the automation roots
|
||||
@@ -249,50 +266,57 @@ fn prefs_enabled() -> bool {
|
||||
}
|
||||
|
||||
/// The saved UI preferences, read before the window opens (they hold its size and position).
|
||||
/// A file read only in part, or with layouts converted from an older version, is copied beside
|
||||
/// itself before anything saves over it.
|
||||
fn read_prefs() -> Option<vectorcraft_ui_egui::UiState> {
|
||||
if !prefs_enabled() {
|
||||
return None;
|
||||
}
|
||||
let bytes = prefs_path().and_then(|p| std::fs::read(p).ok()).or_else(|| legacy_prefs_path().and_then(|p| std::fs::read(p).ok()))?;
|
||||
if let Some(p) = prefs_path()
|
||||
&& let Ok(bytes) = std::fs::read(&p)
|
||||
{
|
||||
let read = vectorcraft_ui_egui::UiState::read_preferences(&bytes);
|
||||
if read.as_ref().is_none_or(|read| !read.intact) {
|
||||
keep_prefs_copy(&p, &bytes, "ui.unreadable");
|
||||
}
|
||||
if read.as_ref().is_some_and(|read| read.converted) {
|
||||
keep_prefs_copy(&p, &bytes, "ui.before-layout-migration");
|
||||
}
|
||||
return read.map(|read| read.ui);
|
||||
}
|
||||
let bytes = legacy_prefs_path().and_then(|p| std::fs::read(p).ok())?;
|
||||
decode_saved_prefs(&bytes)
|
||||
}
|
||||
|
||||
/// Recover only optional persisted docking metadata. Runtime UiState and command decoding
|
||||
/// remains strict, and an unreadable workspace layout cannot discard unrelated preferences.
|
||||
/// Saved preferences, with what this version cannot read of their docking data dropped and
|
||||
/// older layouts converted (`vectorcraft_ui_egui::layout_migration`).
|
||||
fn decode_saved_prefs(bytes: &[u8]) -> Option<vectorcraft_ui_egui::UiState> {
|
||||
use serde_json::{Map, Value, json};
|
||||
vectorcraft_ui_egui::UiState::read_preferences(bytes).map(|read| read.ui)
|
||||
}
|
||||
|
||||
fn recover(object: &mut Map<String, Value>, hidden_key: &str) {
|
||||
if let Some(layout) = object.get("docking")
|
||||
&& serde_json::from_value::<vectorcraft_ui_egui::UiState>(json!({"docking": layout})).is_err()
|
||||
{
|
||||
object.remove("docking");
|
||||
}
|
||||
if let Some(hidden) = object.get_mut(hidden_key) {
|
||||
if let Value::Object(entries) = hidden {
|
||||
entries.retain(|panel, location| {
|
||||
serde_json::from_value::<vectorcraft_ui_egui::UiState>(json!({"docking_hidden": {panel: location}})).is_ok()
|
||||
});
|
||||
} else {
|
||||
object.remove(hidden_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Set when `ui.json` needed a copy kept ([`keep_prefs_copy`]) and none could be: the file is
|
||||
/// then left as it is rather than overwritten.
|
||||
static KEEP_UI_PREFS: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
let mut value: Value = serde_json::from_slice(bytes).ok()?;
|
||||
let object = value.as_object_mut()?;
|
||||
recover(object, "docking_hidden");
|
||||
// Saved workspaces and the layout changes kept for each workspace carry the same layouts.
|
||||
for key in ["custom_workspaces", "workspace_changes"] {
|
||||
if let Some(Value::Array(workspaces)) = object.get_mut(key) {
|
||||
for workspace in workspaces {
|
||||
if let Some(object) = workspace.as_object_mut() {
|
||||
recover(object, "dockingHidden");
|
||||
/// Copy `ui.json` beside it as `<stem>.json` or the first free `<stem>-N.json`, before the next
|
||||
/// save replaces it: `ui.unreadable` when it could not be read in full (a newer VectorCraft may
|
||||
/// have written it), `ui.before-layout-migration` when its layouts were converted.
|
||||
fn keep_prefs_copy(path: &std::path::Path, bytes: &[u8], stem: &str) {
|
||||
use std::io::Write as _;
|
||||
for n in 1..100u32 {
|
||||
let name = if n == 1 { format!("{stem}.json") } else { format!("{stem}-{n}.json") };
|
||||
match std::fs::OpenOptions::new().write(true).create_new(true).open(path.with_file_name(name)) {
|
||||
Ok(mut file) => {
|
||||
if file.write_all(bytes).is_ok() {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
serde_json::from_value(value).ok()
|
||||
KEEP_UI_PREFS.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn load_prefs(app: &mut VectorcraftApp, saved: Option<vectorcraft_ui_egui::UiState>) {
|
||||
@@ -306,7 +330,7 @@ fn load_prefs(app: &mut VectorcraftApp, saved: Option<vectorcraft_ui_egui::UiSta
|
||||
}
|
||||
|
||||
fn save_prefs(app: &VectorcraftApp) {
|
||||
if !prefs_enabled() {
|
||||
if !prefs_enabled() || KEEP_UI_PREFS.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
if let Some(p) = prefs_path() {
|
||||
@@ -1111,6 +1135,7 @@ fn main() -> std::process::ExitCode {
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let _ = in_window_menus;
|
||||
app.services.native_input_hook = native_input_hook(&app);
|
||||
file_access::unconfined(|| open_files(&mut app, files));
|
||||
// The first frame is due from now (`gpu::watch_first_frame`).
|
||||
#[cfg(feature = "wgpu")]
|
||||
@@ -1244,6 +1269,25 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unreadable_ui_json_is_kept_beside_it() {
|
||||
let stamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
|
||||
let dir = std::env::temp_dir().join(format!("vectorcraft-ui-prefs-{}-{stamp}", std::process::id()));
|
||||
std::fs::create_dir(&dir).unwrap();
|
||||
let path = dir.join("ui.json");
|
||||
super::keep_prefs_copy(&path, b"{\"docking\": {\"root\": {\"FutureNode\": {}}}}", "ui.unreadable");
|
||||
super::keep_prefs_copy(&path, b"not json", "ui.unreadable");
|
||||
super::keep_prefs_copy(&path, b"{}", "ui.before-layout-migration");
|
||||
let first = std::fs::read(dir.join("ui.unreadable.json")).unwrap();
|
||||
let second = std::fs::read(dir.join("ui.unreadable-2.json")).unwrap();
|
||||
let before = std::fs::read(dir.join("ui.before-layout-migration.json")).unwrap();
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
assert!(first.starts_with(b"{\"docking\""), "the first copy is not replaced");
|
||||
assert_eq!(second, b"not json");
|
||||
assert_eq!(before, b"{}");
|
||||
assert!(!super::KEEP_UI_PREFS.load(std::sync::atomic::Ordering::Relaxed), "ui.json can be saved once a copy is kept");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_image_trace_preview_setting_is_kept_across_a_restart() {
|
||||
for preview in [false, true] {
|
||||
|
||||
@@ -320,6 +320,7 @@ impl Writer {
|
||||
description: given(&info.description),
|
||||
keywords: info.keywords.clone(),
|
||||
created: created.map(date_time),
|
||||
modified: None,
|
||||
};
|
||||
let pdf = write::Writer::new(settings, metadata);
|
||||
Ok(Self {
|
||||
|
||||
@@ -353,6 +353,7 @@ fn to_jpeg(samples: &[u8], space: ImageColorspace, width: u32, height: u32, qual
|
||||
let t = if space == ImageColorspace::Rgb { ExtendedColorType::Rgb8 } else { ExtendedColorType::L8 };
|
||||
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut out, quality).encode(samples, width, height, t).ok()?;
|
||||
}
|
||||
ImageColorspace::DeviceN => return None,
|
||||
}
|
||||
Image::from_jpeg(out.into(), interpolate).ok()
|
||||
}
|
||||
|
||||
@@ -2040,6 +2040,10 @@ impl<'a> Device<'a> for Builder<'_> {
|
||||
paint: &craft_pdf::interpret::Paint<'a>,
|
||||
draw_mode: &GlyphDrawMode,
|
||||
) {
|
||||
// Clipping text paints nothing; its outlines arrive separately as a clip path.
|
||||
if matches!(draw_mode, GlyphDrawMode::Clip) {
|
||||
return;
|
||||
}
|
||||
self.pending = false;
|
||||
let stroke = match draw_mode {
|
||||
// Invisible text (a scanned page's recognized text, over the scan) draws nothing but
|
||||
@@ -2054,7 +2058,7 @@ impl<'a> Device<'a> for Builder<'_> {
|
||||
}
|
||||
return;
|
||||
}
|
||||
GlyphDrawMode::Fill => None,
|
||||
GlyphDrawMode::Fill | GlyphDrawMode::Clip => None,
|
||||
GlyphDrawMode::Stroke(p) => Some(p.clone()),
|
||||
};
|
||||
match glyph {
|
||||
|
||||
@@ -50,6 +50,10 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
# Floating panels open in OS windows of their own on the desktop; on the web they stay in the page.
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
craft-ui = { workspace = true, features = ["native-windows"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# `now_ms` (src/lib.rs) reads the page's `performance.now()` with this crate in the web build. It is
|
||||
# MIT OR Apache-2.0 and already in the build through eframe.
|
||||
|
||||
@@ -1158,7 +1158,8 @@ fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect:
|
||||
}
|
||||
ui.data_mut(|dd| dd.insert_temp(drag_id(), Drag::ZoomScrub { start, held, moved }));
|
||||
}
|
||||
Drag::Art if on_canvas(ui, resp, p) => {
|
||||
// Without a pointer it is over a floating panel's own OS window (`drag_art_out`).
|
||||
Drag::Art if pointer.has_pointer() && on_canvas(ui, resp, p) => {
|
||||
// Back on the canvas: the move goes on from where it left, with the pointer.
|
||||
egui::DragAndDrop::clear_payload(ui.ctx());
|
||||
let ev =
|
||||
@@ -1699,12 +1700,17 @@ fn drop_press(app: &mut VectorcraftApp, ui: &Ui, view: vectorcraft_engine::ViewI
|
||||
/// canvas, and a panel it is released on takes it (the Graphic Styles panel makes a style of it).
|
||||
/// Back on the canvas, [`Drag::Art`] takes the move up again where the pointer is (#2826).
|
||||
fn drag_art_out(app: &VectorcraftApp, ui: &Ui, resp: &egui::Response, p: Pos2) -> bool {
|
||||
let off = !on_canvas(ui, resp, p);
|
||||
let Some(st) = app.session.active().filter(|_| off && app.session.tool_id() == "selection") else { return false };
|
||||
let Some(st) = app.session.active().filter(|_| app.session.tool_id() == "selection") else { return false };
|
||||
// The Selection tool's interaction while it moves (or Alt-copies) the selection.
|
||||
if !st.interaction.as_ref().is_some_and(|i| i.label == "Move" || i.label == "Copy") {
|
||||
return false;
|
||||
}
|
||||
// A floating panel in an OS window of its own takes the art too: craft-ui hands that window
|
||||
// the pointer while it is over it, and this window sees the pointer go.
|
||||
craft_ui::docking::route_app_drag(ui.ctx(), crate::panel_docking::area_id());
|
||||
if on_canvas(ui, resp, p) && ui.input(|i| i.pointer.has_pointer()) {
|
||||
return false;
|
||||
}
|
||||
let ids = st.selection.objects.clone();
|
||||
egui::DragAndDrop::set_payload(ui.ctx(), widgets::PanelDrag::Art(ids));
|
||||
true
|
||||
@@ -1715,24 +1721,50 @@ fn drag_art_out(app: &VectorcraftApp, ui: &Ui, resp: &egui::Response, p: Pos2) -
|
||||
/// left open (#2826). The art stays where it last moved while the pointer is over the panel.
|
||||
pub(crate) fn release_over_panel(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
crate::document_drag::cleanup(app, ctx);
|
||||
let art = matches!(ctx.data(|d| d.get_temp::<Drag>(drag_id())), Some(Drag::Art));
|
||||
// Over a floating panel's own OS window the pointer is gone from this window, and the release
|
||||
// comes here later, at a point of this window that may lie on the canvas: that panel takes it.
|
||||
let gone_id = drag_id().with("art-gone");
|
||||
if !ctx.input(|i| i.pointer.primary_released()) {
|
||||
if art {
|
||||
let gone = ctx.input(|i| !i.pointer.has_pointer());
|
||||
ctx.data_mut(|d| d.insert_temp(gone_id, gone));
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(p) = ctx.input(|i| i.pointer.interact_pos().or(i.pointer.latest_pos())) else { return };
|
||||
if !matches!(ctx.data(|d| d.get_temp::<Drag>(drag_id())), Some(Drag::Art)) {
|
||||
let gone = ctx.data_mut(|d| d.remove_temp::<bool>(gone_id)).unwrap_or(false);
|
||||
let p = ctx.input(|i| i.pointer.interact_pos().or(i.pointer.latest_pos()));
|
||||
if !art {
|
||||
return;
|
||||
}
|
||||
let layer = ctx.data(|d| d.get_temp::<egui::LayerId>(drag_layer_id()));
|
||||
let on = app.canvas_rect.is_some_and(|r| r.contains(p)) && ctx.layer_id_at(p).is_none_or(|l| Some(l) == layer);
|
||||
let on = !gone && p.is_some_and(|p| app.canvas_rect.is_some_and(|r| r.contains(p)) && ctx.layer_id_at(p).is_none_or(|l| Some(l) == layer));
|
||||
if on {
|
||||
// The canvas's release finishes the move.
|
||||
return;
|
||||
}
|
||||
end_art_move(app, ctx);
|
||||
}
|
||||
|
||||
/// A release in a floating panel's own OS window, during an art drag from the canvas: the move
|
||||
/// ends before that panel takes the art. The main window would only see it on its next pass.
|
||||
pub(crate) fn release_art_in_window(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let art = matches!(ctx.data(|d| d.get_temp::<Drag>(drag_id())), Some(Drag::Art));
|
||||
if art && ctx.input(|i| i.pointer.primary_released()) {
|
||||
end_art_move(app, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Roll back the art move dragged off the canvas; a panel takes the art instead.
|
||||
fn end_art_move(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
// With no document open there is nothing to roll back; the tool still gets its mouse-up.
|
||||
let _ = app.session.cancel_interaction();
|
||||
let view = app.view_info();
|
||||
dispatch(app, &PointerEvent { kind: PointerKind::Up, pos: Point::ZERO, mods: Mods::default(), pressure: 1.0 }, view);
|
||||
ctx.data_mut(|d| d.remove::<Drag>(drag_id()));
|
||||
ctx.data_mut(|d| {
|
||||
d.remove::<Drag>(drag_id());
|
||||
d.remove::<bool>(drag_id().with("art-gone"));
|
||||
});
|
||||
}
|
||||
|
||||
/// Send a pointer event to the active tool and act on UI requests (dialogs, tool switches).
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
//! - `engine.execute {command, params}` / `ui.menu.invoke {command|id, params}`: run any command
|
||||
//! - `engine.commands`: engine + UI commands with enablement
|
||||
//! - `document.inspect`: document summary; `ui.inspect`: UI state
|
||||
//! - `ui.docking`: the panel layout in use, the open flyout, the focused panel and the docking
|
||||
//! controls' rects
|
||||
//! - `ui.menu.list`: flattened menu tree
|
||||
//! - `ui.contextMenu.list`: the canvas context menu for the current selection, flattened
|
||||
//! - `ui.tool.select {tool}`, `ui.tool.list`
|
||||
@@ -140,10 +142,16 @@ pub fn all_commands(app: &VectorcraftApp) -> Value {
|
||||
|
||||
pub fn inspect(app: &VectorcraftApp, ctx: &egui::Context) -> Value {
|
||||
let r = ctx.content_rect();
|
||||
let mut ui = serde_json::to_value(&app.ui).unwrap_or_default();
|
||||
// The old floating panels' record, kept only to read older preferences: `ui.docking` has
|
||||
// the layout.
|
||||
if let Some(fields) = ui.as_object_mut() {
|
||||
fields.remove("floating_panels");
|
||||
}
|
||||
json!({
|
||||
"tool": app.session.tool_id(),
|
||||
"toolOptions": app.session.tool_options(),
|
||||
"ui": serde_json::to_value(&app.ui).unwrap_or_default(),
|
||||
"ui": ui,
|
||||
// Not saved with the UI state, so not in `ui`: 0 normal … 3 Presentation Mode.
|
||||
"screenMode": app.ui.screen_mode,
|
||||
// The Contextual Task Bar: pinned (not saved either) and where it shows.
|
||||
@@ -218,6 +226,7 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlReques
|
||||
"engine.commands" => ok(all_commands(app)),
|
||||
"document.inspect" => wrap(app.run("document.inspect", p.clone())),
|
||||
"ui.inspect" => ok(inspect(app, ctx)),
|
||||
"ui.docking" => ok(crate::panel_docking::inspect(app, ctx)),
|
||||
"ui.menu.list" => ok(serde_json::to_value(crate::menus::menu_entries(app)).unwrap_or_default()),
|
||||
"ui.contextMenu.list" => ok(serde_json::to_value(crate::menus::context_entries(app)).unwrap_or_default()),
|
||||
"ui.tool.select" => wrap(app.run("tool.select", json!({"tool": s("tool").unwrap_or("")}))),
|
||||
|
||||
@@ -381,7 +381,7 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut
|
||||
changed = true;
|
||||
}
|
||||
let (nr, _) = ui.allocate_exact_size(vec2(46.0, 20.0), Sense::hover());
|
||||
let nresp = ui.interact(nr, egui::Id::new((NEW_CHIP, i)), Sense::click());
|
||||
let nresp = ui.interact(nr, egui::Id::unique((NEW_CHIP, i)), Sense::click());
|
||||
let to = to_color(row).unwrap_or_default();
|
||||
chip(ui, nr, to, off);
|
||||
if nresp.clicked() {
|
||||
@@ -722,7 +722,7 @@ mod tests {
|
||||
let input = egui::RawInput { time: Some(time), events, ..Default::default() };
|
||||
let mut out = ctx.run_ui(input, |ui| super::super::show(app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
ctx.read_response(egui::Id::new((NEW_CHIP, i))).map(|r| r.rect)
|
||||
ctx.read_response(egui::Id::unique((NEW_CHIP, i))).map(|r| r.rect)
|
||||
}
|
||||
|
||||
/// Runs frames from `time` until the dialog stops moving (it centres itself as it measures its
|
||||
|
||||
@@ -141,7 +141,7 @@ fn the_menus_reach_asset_export() {
|
||||
let window = entries.iter().find(|e| e.label == "Asset Export").unwrap();
|
||||
assert_eq!(window.params, json!({"panel": crate::panels::asset_export::ID, "toggle": true}));
|
||||
app.run("window.panel", window.params.clone()).unwrap();
|
||||
assert_eq!(app.ui.open_panel.as_deref(), Some(crate::panels::asset_export::ID));
|
||||
assert!(crate::panel_docking::showing(&app.ui, crate::panels::asset_export::ID));
|
||||
app.run("assets.add", json!({"multiple": false})).unwrap();
|
||||
assert_eq!(assets(&mut app).len(), 1);
|
||||
}
|
||||
|
||||
+12
-875
@@ -1,514 +1,10 @@
|
||||
//! The right dock: Properties | Layers | Libraries tabs, plus the collapsed icon-panel column
|
||||
//! whose panels pop out to the left, and left of it the floating sets docked as columns
|
||||
//! ([`floating::docked_columns`]). The double arrow at the top of the dock collapses the tabbed
|
||||
//! group to icons at the top of the icon column (`window.collapseDock`); the arrow above the column
|
||||
//! then expands it again.
|
||||
//! Panel bodies: what the dock area ([`crate::panel_docking`]) draws inside a docked, floating or
|
||||
//! flyout panel.
|
||||
|
||||
use egui::{CornerRadius, Pos2, Rect, Sense, Stroke, Ui, vec2};
|
||||
use serde_json::json;
|
||||
use egui::Ui;
|
||||
|
||||
use crate::state::{DockTab, ICON_PANEL_GROUPS, ICON_PANELS};
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::{VectorcraftApp, floating, icons, panels, widgets};
|
||||
|
||||
const ICON_COL: f32 = 38.0;
|
||||
/// Height of the strip along the top of a dock column that carries its double arrow.
|
||||
const HEADER: f32 = 14.0;
|
||||
/// The tabbed group's width until the dock has been laid out (its default size).
|
||||
const DOCK_WIDTH: f32 = 300.0;
|
||||
pub(crate) const LEGACY_DOCK_PANEL: &str = "dock";
|
||||
pub(crate) const SHARED_DOCK_PANEL: &str = "shared-panel-dock";
|
||||
const LEGACY_DOCK_MIN: f32 = 230.0;
|
||||
const LEGACY_DOCK_MAX: f32 = 520.0;
|
||||
const SHARED_DOCK_MIN: f32 = 220.0;
|
||||
const SHARED_DOCK_MAX: f32 = 720.0;
|
||||
|
||||
pub(crate) fn clamp_legacy_dock_width(width: f32) -> f32 {
|
||||
width.clamp(LEGACY_DOCK_MIN, LEGACY_DOCK_MAX)
|
||||
}
|
||||
|
||||
pub(crate) fn clamp_shared_dock_width(width: f32) -> f32 {
|
||||
width.clamp(SHARED_DOCK_MIN, SHARED_DOCK_MAX)
|
||||
}
|
||||
|
||||
/// Seed the panel's public persisted state before it is laid out: `Panel` keys it by the parent
|
||||
/// `Ui`'s scope id and its own salt in 0.37, not by a global `Id` (the old `Id::unique(panel)` key
|
||||
/// silently stopped restoring the width). Keep this in one place so the key cannot drift.
|
||||
fn seed_dock_panel_width(ui: &egui::Ui, panel: &str, width: f32) {
|
||||
let screen = ui.ctx().content_rect();
|
||||
let w = width.max(1.0);
|
||||
let outer_rect = egui::Rect::from_min_size(egui::pos2(screen.right() - w, screen.top()), egui::vec2(w, screen.height()));
|
||||
let state = egui::containers::panel::PanelState { outer_rect };
|
||||
ui.ctx().data_mut(|data| data.insert_persisted(ui.scope_id().with(panel), state));
|
||||
}
|
||||
|
||||
pub(crate) fn apply_pending_dock_width(app: &mut VectorcraftApp, ui: &egui::Ui, panel: &str, clamp: fn(f32) -> f32) {
|
||||
if !app.ui.dock_width_pending {
|
||||
return;
|
||||
}
|
||||
if let Some(width) = app.ui.dock_width.filter(|w| w.is_finite() && *w > 0.0) {
|
||||
seed_dock_panel_width(ui, panel, clamp(width));
|
||||
}
|
||||
app.ui.dock_width_pending = false;
|
||||
}
|
||||
|
||||
/// Remember the width the panel was actually laid out at (its `response.rect`), so a workspace
|
||||
/// switch or the next session restores it without reaching into egui's private `PanelState`.
|
||||
pub(crate) fn note_dock_width(app: &mut VectorcraftApp, width: f32) {
|
||||
if width.is_finite() && width > 0.0 {
|
||||
app.ui.dock_width = Some(width);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn legacy_dock_default_width(ui: &crate::state::UiState) -> f32 {
|
||||
ui.dock_width.filter(|w| w.is_finite()).map(clamp_legacy_dock_width).unwrap_or(DOCK_WIDTH)
|
||||
}
|
||||
|
||||
pub(crate) fn shared_dock_default_width(ui: &crate::state::UiState) -> f32 {
|
||||
ui.dock_width.filter(|w| w.is_finite()).map(clamp_shared_dock_width).unwrap_or(310.0)
|
||||
}
|
||||
/// Top of a popped-out panel.
|
||||
const FLYOUT_TOP: f32 = 110.0;
|
||||
|
||||
/// Where the icon column's left edge was drawn last (egui temp memory, one frame old).
|
||||
fn column_left_id() -> egui::Id {
|
||||
egui::Id::unique("dock-icon-column-left")
|
||||
}
|
||||
|
||||
/// Where the dock's left edge was drawn last: the icon column's, or the leftmost dock column's
|
||||
/// (egui temp memory, one frame old). Popped-out panels open left of it.
|
||||
pub(crate) fn dock_left_id() -> egui::Id {
|
||||
egui::Id::unique("dock-left")
|
||||
}
|
||||
|
||||
/// Collapse the tabbed group to icons or expand it again (`window.collapseDock`). Expanding while
|
||||
/// one of its panels is popped out shows that panel's tab instead.
|
||||
pub fn set_collapsed(app: &mut VectorcraftApp, collapsed: bool) {
|
||||
app.ui.dock_collapsed = collapsed;
|
||||
app.ui.dock = true;
|
||||
if !collapsed && let Some(tab) = app.ui.open_panel.as_deref().and_then(DockTab::from_id) {
|
||||
app.ui.dock_tab = tab;
|
||||
app.ui.open_panel = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The strip along the top of a dock column with its double arrow: » collapses the dock to icons,
|
||||
/// « expands it. The whole strip is the click target (as on the toolbar). True when clicked.
|
||||
pub(crate) fn collapse_header(ui: &mut Ui, collapsed: bool) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (hdr, resp) = ui.allocate_exact_size(vec2(ui.available_width(), HEADER), Sense::click());
|
||||
let arrow = Rect::from_min_size(hdr.right_top() + vec2(-16.0, 1.0), vec2(12.0, 12.0));
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(arrow, CornerRadius::same(2), t.hover);
|
||||
}
|
||||
let icon = if collapsed { "chevrons-left" } else { "chevrons-right" };
|
||||
icons::paint(ui, icon, arrow.shrink(1.0), if resp.hovered() { t.text_strong } else { t.text });
|
||||
resp.on_hover_text(if collapsed { tl!("Expand Panels") } else { tl!("Collapse to Icons") }).clicked()
|
||||
}
|
||||
|
||||
/// Run `window.collapseDock` for a click on a double arrow.
|
||||
fn toggle(app: &mut VectorcraftApp, collapsed: bool) {
|
||||
// It only fails on bad params, which this never sends; show it all the same.
|
||||
if let Err(e) = app.run("window.collapseDock", json!({ "collapsed": collapsed })) {
|
||||
app.ui.status = e;
|
||||
}
|
||||
}
|
||||
|
||||
/// One icon of the column: a click pops its panel out (or puts it away); dragged out of the
|
||||
/// column (`column`), the panel floats.
|
||||
fn panel_icon(app: &mut VectorcraftApp, ui: &mut Ui, id: &'static str, label: &str, icon: &str, _column: Rect) {
|
||||
let open = app.ui.open_panel.as_deref() == Some(id);
|
||||
// Its own id: the icons below it move up when it floats, and mustn't take over its drag.
|
||||
let (_, rect) = ui.allocate_space(vec2(30.0, 30.0));
|
||||
let resp = widgets::paint_icon_button(ui, ui.interact(rect, ui.scope_id().with(("panel-icon", id)), Sense::click_and_drag()), icon, label, open);
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled(), open, tl!(label)));
|
||||
if resp.clicked() {
|
||||
app.ui.open_panel = if open { None } else { Some(id.to_string()) };
|
||||
}
|
||||
crate::panel_docking::legacy_tab(app, ui, id, &resp, false);
|
||||
}
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
crate::panel_docking::prune_collapsed(&mut app.ui);
|
||||
if crate::panel_docking::show(app, ui) {
|
||||
return;
|
||||
}
|
||||
apply_pending_dock_width(app, ui, LEGACY_DOCK_PANEL, clamp_legacy_dock_width);
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let shown = floating::shown_tab(&app.ui);
|
||||
ui.ctx().data_mut(|d| d.insert_temp(floating::root_groups_id(), Vec::<floating::RootGroup>::new()));
|
||||
// Main tabbed group, unless it is collapsed to icons or all its panels float.
|
||||
let group = shown.filter(|_| !app.ui.dock_collapsed).map(|active| {
|
||||
egui::Panel::right(LEGACY_DOCK_PANEL)
|
||||
.resizable(true)
|
||||
.default_size(legacy_dock_default_width(&app.ui))
|
||||
.size_range(LEGACY_DOCK_MIN..=LEGACY_DOCK_MAX)
|
||||
.frame(egui::Frame::NONE)
|
||||
.show(ui, |ui| {
|
||||
crate::theme::card(ui, &t, egui::Margin { left: 0, right: 6, top: 6, bottom: 6 }, |ui| {
|
||||
if collapse_header(ui, false) {
|
||||
toggle(app, true);
|
||||
}
|
||||
// What a group dropped on it joins ([`floating::Drop::Root`]).
|
||||
let rect = ui.available_rect_before_wrap();
|
||||
let group = floating::RootGroup {
|
||||
panels: floating::docked_tabs(&app.ui).map(|tab| tab.info().0).collect(),
|
||||
shown: active.info().0,
|
||||
rect,
|
||||
strip: Rect::from_min_size(rect.min, vec2(rect.width(), 33.0)),
|
||||
};
|
||||
ui.ctx().data_mut(|d| d.insert_temp(floating::root_groups_id(), vec![group]));
|
||||
if tab_strip(app, ui, active) {
|
||||
// It floats from this frame on.
|
||||
return;
|
||||
}
|
||||
// Collapsed to its tabs (a double-click on one).
|
||||
if app.ui.docking_collapsed.contains(app.ui.dock_tab.info().0) {
|
||||
return;
|
||||
}
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 12, right: 10, top: 10, bottom: 8 }).show(ui, |ui| match active {
|
||||
DockTab::Properties => {
|
||||
widgets::panel_scroll(ui, egui::ScrollArea::vertical().id_salt("props").auto_shrink([false, false]), |ui| {
|
||||
panels::properties::show(app, ui)
|
||||
});
|
||||
}
|
||||
DockTab::Layers => panels::layers::show(app, ui),
|
||||
DockTab::Libraries => panels::libraries(app, ui),
|
||||
});
|
||||
})
|
||||
})
|
||||
.response
|
||||
.rect
|
||||
});
|
||||
if let Some(rect) = group {
|
||||
note_dock_width(app, rect.width());
|
||||
}
|
||||
// Collapsed icon-panel strip, left of the expanded panel group (like Illustrator's dock). A
|
||||
// collapsed group's panels head it, under the « that expands them again. Floating panels leave
|
||||
// it.
|
||||
let collapsed = app.ui.dock_collapsed && shown.is_some();
|
||||
let column = egui::Panel::right("icon_column").resizable(false).exact_size(ICON_COL).frame(egui::Frame::NONE).show(ui, |ui| {
|
||||
crate::theme::card(ui, &t, egui::Margin { left: 3, right: 3, top: 6, bottom: 6 }, |ui| {
|
||||
let bounds = ui.max_rect();
|
||||
if collapsed && collapse_header(ui, true) {
|
||||
toggle(app, false);
|
||||
}
|
||||
egui::Frame::NONE.inner_margin(egui::Margin::symmetric(1, 4)).show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 2.0;
|
||||
// The icons scroll in a window too short for them.
|
||||
widgets::strip_scroll(ui, "icon_column", |ui| {
|
||||
if collapsed {
|
||||
for tab in DockTab::ALL {
|
||||
let (id, label, icon) = tab.info();
|
||||
if floating::group_of(&app.ui, id).is_none() {
|
||||
panel_icon(app, ui, id, label, icon, bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
// A divider above each group with icons left, but the first.
|
||||
let mut divider = collapsed;
|
||||
for group in ICON_PANEL_GROUPS {
|
||||
let mut first = true;
|
||||
for id in group.iter() {
|
||||
let Some((_, label, icon)) = ICON_PANELS.iter().find(|p| p.0 == *id) else { continue };
|
||||
if floating::group_of(&app.ui, id).is_some() {
|
||||
continue;
|
||||
}
|
||||
if std::mem::take(&mut first) && std::mem::replace(&mut divider, true) {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 7.0), Sense::hover());
|
||||
ui.painter()
|
||||
.line_segment([r.left_center() + vec2(4.0, 0.0), r.right_center() - vec2(4.0, 0.0)], Stroke::new(1.0, t.divider));
|
||||
}
|
||||
panel_icon(app, ui, id, label, icon, bounds);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
let column = column.response.rect;
|
||||
let dock = group.map_or(column, |g| g.union(column));
|
||||
// Floating sets docked as columns, left of the icon column.
|
||||
let left = floating::docked_columns(app, ui).iter().map(|r| r.left()).fold(column.left(), f32::min);
|
||||
ui.ctx().data_mut(|d| {
|
||||
d.insert_temp(dock_left_id(), left);
|
||||
d.insert_temp(column_left_id(), column.left());
|
||||
d.insert_temp(floating::dock_rect_id(), dock);
|
||||
d.insert_temp(floating::icons_rect_id(), column);
|
||||
});
|
||||
}
|
||||
|
||||
/// The tabbed group's tab strip, showing `active`: a click on a tab shows it, a double-click on a
|
||||
/// tab or on the strip right of the tabs collapses the group to its tabs or expands it, a tab
|
||||
/// dragged out of the strip floats on its own, and the strip right of the tabs floats the whole
|
||||
/// group. True when the shown panel floats now.
|
||||
fn tab_strip(app: &mut VectorcraftApp, ui: &mut Ui, active: DockTab) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (strip, _) = ui.allocate_exact_size(vec2(ui.available_width(), 33.0), Sense::hover());
|
||||
ui.painter().line_segment([strip.left_bottom(), strip.right_bottom()], Stroke::new(1.0, t.divider));
|
||||
let outside = ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| !strip.contains(p));
|
||||
// What a drag out of the strip floats: the panels, the one shown, where its tab began.
|
||||
let mut tear: Option<(Vec<&'static str>, &'static str, f32)> = None;
|
||||
let tabs: Vec<_> = floating::docked_tabs(&app.ui).collect();
|
||||
let names: Vec<_> = tabs
|
||||
.iter()
|
||||
.map(|tab| {
|
||||
let (id, label, _) = tab.info();
|
||||
(id, label)
|
||||
})
|
||||
.collect();
|
||||
let selected = tabs.iter().position(|&tab| tab == active).unwrap_or(0);
|
||||
let menu = Rect::from_center_size(strip.right_center() - vec2(14.0, 0.0), vec2(16.0, 16.0));
|
||||
let area = Rect::from_min_max(strip.min, egui::pos2((menu.left() - 4.0).max(strip.left()), strip.bottom() - 1.0));
|
||||
let out = panel_tabs(ui, "docktab", area, &names, selected, theme::semibold(12.5), true);
|
||||
let pointer = ui.input(|i| i.pointer.interact_pos());
|
||||
for (i, resp) in &out.responses {
|
||||
let Some(&tab) = tabs.get(*i) else { continue };
|
||||
let id = tab.info().0;
|
||||
if let Some((from, to)) = tab_reorder_on_release(strip, pointer, resp, &out.responses) {
|
||||
apply_dock_tab_reorder(app, &tabs, from, to);
|
||||
} else if resp.double_clicked() {
|
||||
if let Err(error) = app.run("window.panel.collapse", json!({ "panel": id })) {
|
||||
app.ui.status = error;
|
||||
}
|
||||
} else if resp.clicked() {
|
||||
app.ui.dock_tab = tab;
|
||||
}
|
||||
crate::panel_docking::legacy_tab_in_strip(app, ui, id, resp, true, Some(strip));
|
||||
}
|
||||
if let Some(tab) = out.picked.and_then(|i| tabs.get(i)).copied() {
|
||||
app.ui.dock_tab = tab;
|
||||
}
|
||||
let rest = Rect::from_min_max(egui::pos2(out.right, strip.top()), egui::pos2(menu.left() - 4.0, strip.bottom()));
|
||||
if rest.width() > 0.0 {
|
||||
let resp = ui.interact(rest, ui.scope_id().with("dock-group-bar"), Sense::click_and_drag());
|
||||
if resp.double_clicked() {
|
||||
if let Err(error) = app.run("window.panel.collapse", json!({ "panel": active.info().0 })) {
|
||||
app.ui.status = error;
|
||||
}
|
||||
} else if resp.dragged() && outside {
|
||||
tear = Some((floating::docked_tabs(&app.ui).map(|t| t.info().0).collect(), active.info().0, strip.left()));
|
||||
}
|
||||
resp.on_hover_cursor(egui::CursorIcon::Grab).on_hover_text(tl!("Drag to float the panel group"));
|
||||
}
|
||||
panels::panel_menu(app, ui, active.info().0, menu);
|
||||
if let Some((ids, id, left)) = tear {
|
||||
floating::tear(app, ui.ctx(), &ids, id, floating::strip_grab(ui.ctx(), left));
|
||||
}
|
||||
// Torn off, or floated from its menu.
|
||||
floating::group_of(&app.ui, active.info().0).is_some()
|
||||
}
|
||||
|
||||
/// App styling and overflow menus around the shared tab fitting/interaction components. Docking
|
||||
/// and floating groups consume the responses themselves, preserving their different drag rules.
|
||||
/// Drag a tab onto another tab in the same strip to reorder (pointer still on the strip at release),
|
||||
/// as in Illustrator's workspace basics
|
||||
/// (<https://helpx.adobe.com/illustrator/using/workspace-basics.html>).
|
||||
pub(crate) fn tab_reorder_on_release(
|
||||
strip: Rect,
|
||||
pointer: Option<Pos2>,
|
||||
source: &egui::Response,
|
||||
tabs: &[(usize, egui::Response)],
|
||||
) -> Option<(usize, usize)> {
|
||||
if !source.drag_stopped() {
|
||||
return None;
|
||||
}
|
||||
let pointer = pointer.filter(|p| p.is_finite())?;
|
||||
if !strip.expand(4.0).contains(pointer) {
|
||||
return None;
|
||||
}
|
||||
if source.drag_delta().length() < 2.0 {
|
||||
return None;
|
||||
}
|
||||
let from = tabs.iter().find(|(_, r)| r.id == source.id).map(|(i, _)| *i)?;
|
||||
let to = tabs.iter().find(|(_, r)| r.rect.contains(pointer)).map(|(i, _)| *i)?;
|
||||
(from != to).then_some((from, to))
|
||||
}
|
||||
|
||||
fn apply_dock_tab_reorder(app: &mut VectorcraftApp, visible: &[DockTab], from: usize, to: usize) {
|
||||
let Some(from_tab) = visible.get(from) else { return };
|
||||
let Some(to_tab) = visible.get(to) else { return };
|
||||
if let Err(error) = crate::panel_docking::strip_tab_reorder(app, from_tab.info().0, to_tab.info().0) {
|
||||
app.ui.status = error;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PanelTabs {
|
||||
pub responses: Vec<(usize, egui::Response)>,
|
||||
pub picked: Option<usize>,
|
||||
pub right: f32,
|
||||
}
|
||||
|
||||
pub(crate) fn panel_tabs(
|
||||
ui: &mut Ui,
|
||||
id: &str,
|
||||
area: Rect,
|
||||
tabs: &[(&str, &str)],
|
||||
selected: usize,
|
||||
font: egui::FontId,
|
||||
separators: bool,
|
||||
) -> PanelTabs {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let labels: Vec<_> = tabs.iter().map(|&(_, label)| tl!(label)).collect();
|
||||
let galleys: Vec<_> = labels
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, label)| ui.painter().layout_no_wrap((*label).to_string(), font.clone(), if i == selected { t.text_strong } else { t.text_dim }))
|
||||
.collect();
|
||||
let natural: Vec<_> = galleys.iter().map(|g| g.size().x + 24.0).collect();
|
||||
let fit = craft_ui::tabs::fit(&natural, selected, area.width(), 40.0, 18.0);
|
||||
let mut out = PanelTabs { responses: Vec::with_capacity(fit.shown.len()), picked: None, right: area.left() };
|
||||
for (i, width) in fit.shown {
|
||||
let Some((key, _)) = tabs.get(i) else { continue };
|
||||
let Some(label) = labels.get(i) else { continue };
|
||||
let Some(galley) = galleys.get(i) else { continue };
|
||||
let rect = Rect::from_min_size(egui::pos2(out.right, area.top()), vec2(width, area.height()));
|
||||
let shrunk = natural.get(i).is_some_and(|natural| *natural > width + 0.5);
|
||||
let galley = if shrunk {
|
||||
let mut job =
|
||||
egui::text::LayoutJob::simple_singleline((*label).to_string(), font.clone(), if i == selected { t.text_strong } else { t.text_dim });
|
||||
job.wrap =
|
||||
egui::text::TextWrapping { max_width: (width - 8.0).max(1.0), max_rows: 1, break_anywhere: true, overflow_character: Some('…') };
|
||||
ui.painter().layout_job(job)
|
||||
} else {
|
||||
galley.clone()
|
||||
};
|
||||
let resp = craft_ui::tabs::Tab::new(ui.scope_id().with((id, key)), label, i == selected)
|
||||
.sense(Sense::click_and_drag())
|
||||
.focus_stroke(Stroke::new(1.0, t.accent))
|
||||
.show_at(ui, rect, |ui, _| {
|
||||
if i == selected {
|
||||
ui.painter().rect_filled(rect.shrink2(vec2(3.0, 5.0)), 6.0, t.button);
|
||||
}
|
||||
ui.painter().with_clip_rect(rect.intersect(ui.clip_rect())).galley(rect.center() - galley.size() / 2.0, galley, t.text);
|
||||
let _ = separators;
|
||||
});
|
||||
out.responses.push((i, if shrunk { resp.on_hover_text(*label) } else { resp }));
|
||||
out.right = rect.right();
|
||||
}
|
||||
if !fit.overflow.is_empty() && fit.overflow_width > 0.0 {
|
||||
let rect = Rect::from_min_size(egui::pos2(out.right, area.top()), vec2(fit.overflow_width, area.height()));
|
||||
let resp = ui.interact(rect, ui.scope_id().with((id, "overflow")), Sense::click());
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(rect, 0.0, t.hover);
|
||||
}
|
||||
if resp.has_focus() && rect.width() > 4.0 {
|
||||
ui.painter().rect_stroke(rect.shrink(2.0), 0.0, Stroke::new(1.0, t.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
icons::paint(ui, "chevrons-right", Rect::from_center_size(rect.center(), vec2(12.0, 12.0)).intersect(rect), t.text);
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), tl!("Panels")));
|
||||
let resp = resp.on_hover_text(tl!("Panels"));
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
widgets::menu_scroll(ui, |ui| {
|
||||
for i in fit.overflow {
|
||||
if let Some(label) = labels.get(i)
|
||||
&& ui.button(*label).clicked()
|
||||
{
|
||||
out.picked = Some(i);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
out.right = rect.right();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A panel popped out next to the icon column: an icon panel, or one of the tabbed group's panels
|
||||
/// while the dock is collapsed. Dragging its title floats it.
|
||||
pub fn floating_panel(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let open = app.ui.open_panel.as_deref();
|
||||
let Some(id) =
|
||||
open.and_then(|p| crate::state::all_panels().map(|(q, _)| q).find(|q| *q == p).or_else(|| crate::panels::library_panel::intern(p)))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let tab = DockTab::from_id(id);
|
||||
// Expanded, the group shows its panels in the dock itself, unless one was dropped on the icon
|
||||
// column; a floating panel shows in its group.
|
||||
let in_rail = app.ui.docking_icons.contains(id) && app.ui.docking.as_ref().is_some_and(|layout| !layout.contains(&id.to_string()));
|
||||
if tab.is_some() && !app.ui.dock_collapsed && !in_rail || floating::group_of(&app.ui, id).is_some() {
|
||||
return;
|
||||
}
|
||||
// A library panel (in a collapsed dock) is titled by its library's name.
|
||||
let label = crate::panels::library_panel::tab_label(app, id)
|
||||
.unwrap_or_else(|| crate::state::all_panels().find(|(q, _)| *q == id).map_or(id, |(_, label)| tl!(label)).to_string());
|
||||
let t = Tokens::get(ctx);
|
||||
let screen = ctx.content_rect();
|
||||
// Pinned by its right edge, 6 points left of the icon column wherever the dock's width (or its
|
||||
// collapsed state) put it: content wider than the flyout grows it towards the canvas rather
|
||||
// than over the panel icons. Without a dock on screen it keeps to the window's right edge.
|
||||
let column = if app.ui.dock && app.ui.screen_mode < 3 {
|
||||
ctx.data(|d| d.get_temp::<f32>(column_left_id()))
|
||||
.filter(|x| x.is_finite() && *x > screen.left() && *x <= screen.right())
|
||||
.unwrap_or_else(|| screen.right() - ICON_COL - if app.ui.dock_collapsed { 0.0 } else { DOCK_WIDTH })
|
||||
} else {
|
||||
screen.right()
|
||||
};
|
||||
let left = ctx.data(|d| d.get_temp::<f32>(dock_left_id())).filter(|x| x.is_finite() && *x > screen.left() && *x <= column).unwrap_or(column);
|
||||
let right = if app.ui.dock && app.ui.screen_mode < 3 { left } else { column };
|
||||
let right = right - 6.0;
|
||||
// The tabbed group's panels fill their height (Layers) or scroll (Properties): give them one
|
||||
// that keeps the flyout on screen.
|
||||
let width = panel_width(id);
|
||||
let tall = (screen.height() - FLYOUT_TOP - 26.0 - 20.0 - 40.0).clamp(160.0, 560.0);
|
||||
let mut open = true;
|
||||
let mut torn = None;
|
||||
let area = egui::Area::new(egui::Id::unique("icon-panel")).order(egui::Order::Foreground).pivot(egui::Align2::RIGHT_TOP);
|
||||
area.fixed_pos(egui::pos2(right, FLYOUT_TOP)).show(ctx, |ui| {
|
||||
egui::Frame::popup(ui.style()).fill(t.panel).corner_radius(CornerRadius::same(4)).inner_margin(egui::Margin::ZERO).show(ui, |ui| {
|
||||
ui.set_width(width);
|
||||
let (strip, _) = ui.allocate_exact_size(vec2(width, 26.0), Sense::hover());
|
||||
ui.painter().rect_filled(strip, CornerRadius { nw: 4, ne: 4, sw: 0, se: 0 }, t.panel_darker);
|
||||
let title =
|
||||
Rect::from_min_size(strip.min, vec2(ui.painter().layout_no_wrap(label.clone(), theme::semibold(12.0), t.text).size().x + 24.0, 26.0));
|
||||
ui.painter().rect_filled(title, CornerRadius { nw: 4, ne: 0, sw: 0, se: 0 }, t.panel);
|
||||
ui.painter().text(title.left_center() + vec2(12.0, 0.0), egui::Align2::LEFT_CENTER, &label, theme::semibold(12.0), t.text);
|
||||
let close = Rect::from_center_size(strip.right_center() - vec2(13.0, 0.0), vec2(14.0, 14.0));
|
||||
let cr = ui.interact(close, ui.scope_id().with("close-panel"), Sense::click());
|
||||
icons::paint(ui, "chevrons-right", close, if cr.hovered() { t.text } else { t.text_dim });
|
||||
if cr.clicked() {
|
||||
open = false;
|
||||
}
|
||||
let menu_r = Rect::from_center_size(strip.right_center() - vec2(34.0, 0.0), vec2(16.0, 16.0));
|
||||
panels::panel_menu(app, ui, id, menu_r);
|
||||
let drag = ui.interact(title, ui.scope_id().with("title"), Sense::drag()).on_hover_cursor(egui::CursorIcon::Grab);
|
||||
if drag.dragged() && ui.input(|i| i.pointer.is_decidedly_dragging()) {
|
||||
torn = Some(title.left());
|
||||
}
|
||||
// Torn off, or floated from its menu: it floats from this frame on.
|
||||
if torn.is_some() || floating::group_of(&app.ui, id).is_some() {
|
||||
return;
|
||||
}
|
||||
panel_body(app, ui, id, width, tall);
|
||||
});
|
||||
});
|
||||
// User Interface › Auto-Collapse Iconic Panels (#394): a press away from the flyout puts it
|
||||
// away, on the canvas or a docked panel. Not one on the icon column (its icons swap or close the
|
||||
// flyout themselves, on release), nor one on a foreground layer (the flyout itself, a dialog, a palette) or a modal dialog's
|
||||
// backdrop, nor one that closes a popup (a panel menu, a dropdown the flyout opened).
|
||||
if open && app.session.prefs.auto_collapse_icon_panels && !egui::Popup::is_any_open(ctx) {
|
||||
let away = |p: egui::Pos2| {
|
||||
!(column..=column + ICON_COL).contains(&p.x)
|
||||
&& ctx.layer_id_at(p).is_none_or(|l| l.order != egui::Order::Foreground && l != crate::dialogs::modal::backdrop())
|
||||
};
|
||||
if ctx.input(|i| i.pointer.any_pressed().then(|| i.pointer.interact_pos()).flatten()).is_some_and(away) {
|
||||
open = false;
|
||||
}
|
||||
}
|
||||
if let Some(left) = torn {
|
||||
floating::tear(app, ctx, &[id], id, floating::strip_grab(ctx, left));
|
||||
} else if !open {
|
||||
app.ui.open_panel = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The width of a popped-out or floating panel: the tabbed group's panels keep the width the dock
|
||||
/// gives them; icon panels are 256 points.
|
||||
pub(crate) fn panel_width(id: &str) -> f32 {
|
||||
if DockTab::from_id(id).is_some() { DOCK_WIDTH } else { 256.0 }
|
||||
}
|
||||
use crate::state::DockTab;
|
||||
use crate::{VectorcraftApp, panels, widgets};
|
||||
|
||||
/// Panel `id`'s contents in a popped-out or floating panel `width` wide; the tabbed group's panels
|
||||
/// fill (Layers) or scroll (Properties) `tall` points.
|
||||
@@ -542,156 +38,20 @@ pub(crate) fn panel_body(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, width:
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use egui::vec2;
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::*;
|
||||
use crate::toolbar::tests::{wheel, widget_rects};
|
||||
use crate::theme;
|
||||
|
||||
#[test]
|
||||
fn tab_headers_fit_narrow_widths_keep_the_selected_tab_and_clip_labels() {
|
||||
let ctx = egui::Context::default();
|
||||
egui_extras::install_image_loaders(&ctx);
|
||||
theme::install_fonts(&ctx);
|
||||
theme::apply(&ctx, theme::Brightness::MediumDark);
|
||||
let names = [("one", "Long panel name"), ("two", "Another long panel name"), ("three", "Layers")];
|
||||
for width in [0.0, 1.0, 5.0, 18.0, 19.0, 80.0, 180.0, 300.0] {
|
||||
for selected in 0..names.len() {
|
||||
let area = Rect::from_min_size(egui::pos2(20.0, 20.0), vec2(width, 32.0));
|
||||
let mut output = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
let out = panel_tabs(ui, "fit-test", area, &names, selected, theme::semibold(12.5), true);
|
||||
assert!(out.responses.iter().any(|(i, _)| *i == selected), "selected index {selected} stays on the strip");
|
||||
assert!(out.right <= area.right() + 0.001, "{width}: strip ends at {}", out.right);
|
||||
for (_, response) in &out.responses {
|
||||
if response.rect.is_positive() {
|
||||
assert!(area.contains_rect(response.rect), "{width}: {:?}", response.rect);
|
||||
}
|
||||
}
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
for shape in output.shapes.iter().filter(|s| matches!(s.shape, egui::Shape::Text(_))) {
|
||||
assert!(shape.clip_rect.right() <= area.right() + 0.001, "{width}: a label is clipped to its tab");
|
||||
}
|
||||
if width > 0.0 && width < 18.0 {
|
||||
assert!(
|
||||
output.shapes.iter().any(|s| matches!(&s.shape, egui::Shape::Rect(rect) if rect.brush.is_some())),
|
||||
"the actual overflow SVG was painted"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// The main column's width in the default layout.
|
||||
const DOCK_WIDTH: f32 = 310.0;
|
||||
|
||||
fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
crate::panel_docking::show(app, ui);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_menu_can_activate_a_hidden_panel() {
|
||||
crate::i18n::set_current(crate::i18n::Lang::EN);
|
||||
let ctx = egui::Context::default();
|
||||
ctx.enable_accesskit();
|
||||
egui_extras::install_image_loaders(&ctx);
|
||||
theme::install_fonts(&ctx);
|
||||
theme::apply(&ctx, theme::Brightness::MediumDark);
|
||||
let names = [("properties", "Properties"), ("layers", "Layers"), ("libraries", "Libraries")];
|
||||
let mut selected = 0;
|
||||
let mut time = 0.0;
|
||||
let mut frame = |events| {
|
||||
time += 0.1;
|
||||
let input = egui::RawInput {
|
||||
events,
|
||||
time: Some(time),
|
||||
screen_rect: Some(Rect::from_min_size(egui::Pos2::ZERO, vec2(400.0, 300.0))),
|
||||
..Default::default()
|
||||
};
|
||||
let mut output = ctx.run_ui(input, |ui| {
|
||||
let area = Rect::from_min_size(egui::pos2(20.0, 20.0), vec2(90.0, 32.0));
|
||||
let out = panel_tabs(ui, "overflow-test", area, &names, selected, theme::semibold(12.5), true);
|
||||
if let Some(picked) = out.picked {
|
||||
selected = picked;
|
||||
}
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
output
|
||||
};
|
||||
let button = |output: &egui::FullOutput, label: &str| {
|
||||
let bounds = output
|
||||
.platform_output
|
||||
.accesskit_update
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|(_, n)| n.label() == Some(label) && n.role() == egui::accesskit::Role::Button)
|
||||
.unwrap()
|
||||
.1
|
||||
.bounds()
|
||||
.unwrap();
|
||||
egui::pos2((bounds.x0 + bounds.x1) as f32 * 0.5, (bounds.y0 + bounds.y1) as f32 * 0.5)
|
||||
};
|
||||
let press =
|
||||
|pos, pressed| egui::Event::PointerButton { pos, pressed, button: egui::PointerButton::Primary, modifiers: egui::Modifiers::NONE };
|
||||
frame(vec![]);
|
||||
let output = frame(vec![]);
|
||||
let overflow = button(&output, "Panels");
|
||||
frame(vec![egui::Event::PointerMoved(overflow), press(overflow, true)]);
|
||||
frame(vec![press(overflow, false)]);
|
||||
let output = frame(vec![]);
|
||||
let libraries = button(&output, "Libraries");
|
||||
frame(vec![egui::Event::PointerMoved(libraries), press(libraries, true)]);
|
||||
frame(vec![press(libraries, false)]);
|
||||
assert_eq!(selected, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_flyout_stays_left_of_the_icon_column() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
|
||||
let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap()["id"].clone();
|
||||
app.run("select.set", json!({ "ids": [id] })).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
theme::install_fonts(&ctx);
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(1600.0, 1200.0));
|
||||
// Where the icon column begins: the flyout must stay left of it.
|
||||
let column = screen.right() - ICON_COL - 300.0;
|
||||
let mut too_wide = vec![];
|
||||
for (panel, _, _) in crate::state::ICON_PANELS {
|
||||
app.ui.open_panel = Some(panel.to_string());
|
||||
// Two frames: the first lays the panel out, the second places the settled area.
|
||||
for _ in 0..2 {
|
||||
let input = egui::RawInput { screen_rect: Some(screen), ..Default::default() };
|
||||
ctx.run_ui(input, |ui| floating_panel(&mut app, ui.ctx())).textures_delta.clear();
|
||||
}
|
||||
let rect = ctx.memory(|m| m.area_rect(egui::Id::unique("icon-panel"))).unwrap();
|
||||
if rect.right() > column {
|
||||
too_wide.push(format!("{panel}: {:.0} past the column", rect.right() - column));
|
||||
}
|
||||
}
|
||||
assert!(too_wide.is_empty(), "flyouts reaching over the icon column: {too_wide:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_icon_column_scrolls_in_a_short_window() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
theme::install_fonts(&ctx);
|
||||
let mut frame = |time: f64, events| {
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(900.0, 400.0));
|
||||
let input = egui::RawInput { time: Some(time), events, screen_rect: Some(screen), ..Default::default() };
|
||||
ctx.run_ui(input, |ui| show(&mut app, ui)).textures_delta.clear();
|
||||
widget_rects(&ctx, vec2(30.0, 30.0))
|
||||
};
|
||||
let icons = frame(0.0, vec![]);
|
||||
assert!(icons.last().unwrap().bottom() > 400.0, "the last panel icon starts below the window");
|
||||
frame(0.1, wheel(icons[0].center(), 2000.0));
|
||||
let mut icons = vec![];
|
||||
for k in 2..40 {
|
||||
icons = frame(f64::from(k) * 0.1, vec![]);
|
||||
}
|
||||
assert!(icons.last().unwrap().bottom() <= 400.0, "scrolled into view: {:?}", icons.last());
|
||||
}
|
||||
|
||||
/// #3093: in a window too short for the Properties panel, its scroll bar takes a column of its
|
||||
/// own instead of floating over the panel's right edge, so it covers none of the controls
|
||||
/// there, such as the arrow of an image's Image Trace button.
|
||||
#[test]
|
||||
fn the_properties_scroll_bar_covers_no_control() {
|
||||
use egui::accesskit::{Action, Role};
|
||||
@@ -774,227 +134,4 @@ mod tests {
|
||||
let covered: Vec<_> = rows.iter().filter(|r| r.intersect(bar).is_positive()).collect();
|
||||
assert!(covered.is_empty(), "under the scroll bar at {bar:?}: {covered:#?}");
|
||||
}
|
||||
|
||||
const SCREEN: egui::Vec2 = vec2(1600.0, 1000.0);
|
||||
|
||||
/// A frame of the dock and its flyout on a 1600 × 1000 window.
|
||||
struct Harness {
|
||||
app: VectorcraftApp,
|
||||
ctx: egui::Context,
|
||||
time: f64,
|
||||
}
|
||||
|
||||
impl Harness {
|
||||
fn new() -> Self {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
theme::install_fonts(&ctx);
|
||||
let mut h = Self { app, ctx, time: 0.0 };
|
||||
h.settle();
|
||||
h
|
||||
}
|
||||
|
||||
fn frame(&mut self, events: Vec<egui::Event>) {
|
||||
self.time += 0.05;
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, SCREEN);
|
||||
let input = egui::RawInput { time: Some(self.time), events, screen_rect: Some(screen), ..Default::default() };
|
||||
let app = &mut self.app;
|
||||
self.ctx
|
||||
.run_ui(input, |ui| {
|
||||
show(app, ui);
|
||||
floating_panel(app, ui.ctx());
|
||||
crate::dialogs::show(app, ui.ctx());
|
||||
})
|
||||
.textures_delta
|
||||
.clear();
|
||||
}
|
||||
|
||||
fn settle(&mut self) {
|
||||
for _ in 0..3 {
|
||||
self.frame(vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
fn click(&mut self, at: egui::Pos2) {
|
||||
let button =
|
||||
|pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
self.frame(vec![egui::Event::PointerMoved(at), button(true)]);
|
||||
self.frame(vec![button(false)]);
|
||||
self.settle();
|
||||
}
|
||||
|
||||
/// The icon column's left edge as last drawn.
|
||||
fn column_left(&self) -> f32 {
|
||||
self.ctx.data(|d| d.get_temp::<f32>(column_left_id())).unwrap()
|
||||
}
|
||||
|
||||
/// The panel icons of the column, top to bottom.
|
||||
fn icons(&self) -> Vec<Rect> {
|
||||
widget_rects(&self.ctx, vec2(30.0, 30.0))
|
||||
}
|
||||
|
||||
/// The double arrow at the top right of the window (the dock's, or the collapsed column's).
|
||||
fn arrow(&self) -> egui::Pos2 {
|
||||
egui::pos2(SCREEN.x - 10.0, HEADER / 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_panel_count() -> usize {
|
||||
ICON_PANEL_GROUPS.iter().map(|g| g.len()).sum()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_double_arrow_collapses_the_dock_to_icons_and_back() {
|
||||
let mut h = Harness::new();
|
||||
assert!(!h.app.ui.dock_collapsed);
|
||||
// Expanded, the group (at least its 230-point minimum) is right of the column.
|
||||
let expanded = |left: f32| left <= SCREEN.x - ICON_COL - 230.0;
|
||||
assert!(expanded(h.column_left()), "expanded: the group is right of the column: {}", h.column_left());
|
||||
assert_eq!(h.icons().len(), icon_panel_count());
|
||||
h.click(h.arrow());
|
||||
assert!(h.app.ui.dock_collapsed, "the » collapses the dock");
|
||||
assert!(h.column_left() > SCREEN.x - ICON_COL - 2.0, "collapsed: only the icon column is left: {}", h.column_left());
|
||||
let icons = h.icons();
|
||||
assert_eq!(icons.len(), icon_panel_count() + 3, "Properties, Layers and Libraries became icons");
|
||||
assert!(icons[0].top() > HEADER, "the icons sit under the « strip");
|
||||
h.click(h.arrow());
|
||||
assert!(!h.app.ui.dock_collapsed, "the « expands the dock again");
|
||||
assert!(expanded(h.column_left()), "the group is back: {}", h.column_left());
|
||||
assert_eq!(h.icons().len(), icon_panel_count());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_collapsed_panel_pops_out_left_of_the_column() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("window.collapseDock", json!({"collapsed": true})).unwrap();
|
||||
h.settle();
|
||||
// Properties, Layers, Libraries head the column.
|
||||
let layers = h.icons()[1];
|
||||
h.click(layers.center());
|
||||
assert_eq!(h.app.ui.open_panel.as_deref(), Some("layers"));
|
||||
h.settle();
|
||||
let rect = h.ctx.memory(|m| m.area_rect(egui::Id::unique("icon-panel"))).unwrap();
|
||||
let column = h.column_left();
|
||||
assert!(rect.right() <= column, "the flyout {rect:?} stays left of the column at {column}");
|
||||
assert!(rect.left() >= 0.0 && rect.bottom() <= SCREEN.y, "the flyout {rect:?} is on screen");
|
||||
assert!(rect.height() > 200.0, "Layers gets room for its list: {rect:?}");
|
||||
// Its » puts it away.
|
||||
h.click(egui::pos2(rect.right() - 13.0, rect.top() + 13.0));
|
||||
assert_eq!(h.app.ui.open_panel, None, "the flyout's » closes it");
|
||||
// Expanding with a panel popped out shows its tab.
|
||||
h.click(layers.center());
|
||||
assert_eq!(h.app.ui.open_panel.as_deref(), Some("layers"));
|
||||
h.click(h.arrow());
|
||||
assert!(!h.app.ui.dock_collapsed);
|
||||
assert_eq!((h.app.ui.open_panel.as_deref(), h.app.ui.dock_tab), (None, DockTab::Layers));
|
||||
// Properties pops out as wide as the dock and shows its sections, not a sliver, also as
|
||||
// the first flyout of a session (a fresh harness: the flyouts share one area, which
|
||||
// would otherwise keep the size the Layers flyout gave it).
|
||||
let mut h = Harness::new();
|
||||
h.app.run("window.collapseDock", json!({"collapsed": true})).unwrap();
|
||||
h.app.run("window.panel", json!({"panel": "properties"})).unwrap();
|
||||
h.settle();
|
||||
let rect = h.ctx.memory(|m| m.area_rect(egui::Id::unique("icon-panel"))).unwrap();
|
||||
assert!(rect.width() >= DOCK_WIDTH && rect.right() <= h.column_left(), "Properties flyout {rect:?}");
|
||||
// Taller than the 400 points egui sizes a new area at: its sections (Align, Quick
|
||||
// Actions) aren't cut off below Appearance.
|
||||
assert!(rect.height() > 450.0 && rect.bottom() <= SCREEN.y, "Properties flyout {rect:?}");
|
||||
}
|
||||
|
||||
/// User Interface › Auto-Collapse Iconic Panels (#394): on, a click away from a popped-out
|
||||
/// panel puts it away; off (the default), the panel stays until its » or its icon.
|
||||
#[test]
|
||||
fn auto_collapse_puts_a_flyout_away_on_a_click_elsewhere() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("window.collapseDock", json!({"collapsed": true})).unwrap();
|
||||
h.settle();
|
||||
let (properties, layers) = (h.icons()[0], h.icons()[1]);
|
||||
let away = egui::pos2(300.0, 500.0);
|
||||
h.click(layers.center());
|
||||
assert_eq!(h.app.ui.open_panel.as_deref(), Some("layers"));
|
||||
h.click(away);
|
||||
assert_eq!(h.app.ui.open_panel.as_deref(), Some("layers"), "off: a click elsewhere leaves it");
|
||||
h.app.session.execute("prefs.set", &json!({"key": "autoCollapseIconPanels", "value": true})).unwrap();
|
||||
h.click(away);
|
||||
assert_eq!(h.app.ui.open_panel, None, "on: a click elsewhere puts it away");
|
||||
h.click(layers.center());
|
||||
assert_eq!(h.app.ui.open_panel.as_deref(), Some("layers"), "its icon still pops it out");
|
||||
let rect = h.ctx.memory(|m| m.area_rect(egui::Id::unique("icon-panel"))).unwrap();
|
||||
h.click(egui::pos2(rect.center().x, rect.top() + 13.0 + 26.0 + 10.0));
|
||||
assert_eq!(h.app.ui.open_panel.as_deref(), Some("layers"), "a click inside keeps it");
|
||||
// The icons still swap the flyout and put it away.
|
||||
h.click(properties.center());
|
||||
assert_eq!(h.app.ui.open_panel.as_deref(), Some("properties"), "another icon swaps it");
|
||||
h.click(properties.center());
|
||||
assert_eq!(h.app.ui.open_panel, None, "the same icon puts it away");
|
||||
// A dialog (one a panel menu opens, say): a click in it or on its backdrop keeps the panel.
|
||||
h.click(layers.center());
|
||||
h.app.ui.dialog = Some(crate::state::Dialog::new("move", json!({"dx": "0 pt", "dy": "0 pt"})));
|
||||
h.settle();
|
||||
let dialog = h.ctx.memory(|m| m.area_rect(egui::Id::unique(("dialog", "move")))).expect("the Move dialog");
|
||||
h.click(dialog.left_top() + vec2(20.0, 30.0));
|
||||
assert_eq!(h.app.ui.open_panel.as_deref(), Some("layers"), "a click in the dialog keeps it");
|
||||
h.click(egui::pos2(dialog.left() - 40.0, dialog.center().y));
|
||||
assert_eq!(h.app.ui.open_panel.as_deref(), Some("layers"), "a click on the backdrop keeps it");
|
||||
h.app.ui.dialog = None;
|
||||
// The dock expanded: a click in its panel group puts a popped-out icon panel away too.
|
||||
h.app.run("window.collapseDock", json!({"collapsed": false})).unwrap();
|
||||
h.settle();
|
||||
let icon = h.icons()[0];
|
||||
h.click(icon.center());
|
||||
let open = h.app.ui.open_panel.clone();
|
||||
assert!(open.is_some(), "an icon panel pops out beside the expanded dock");
|
||||
h.click(egui::pos2(SCREEN.x - 100.0, 300.0));
|
||||
assert_eq!(h.app.ui.open_panel, None, "a click in the docked group puts {open:?} away");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_panel_move_reorders_legacy_dock_tabs_and_persists_order() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
|
||||
assert!(app.ui.docking.is_none());
|
||||
app.run("window.panel.move", json!({ "panel": "libraries", "anchor": "properties", "before": "properties" })).unwrap();
|
||||
let order: Vec<_> = floating::docked_tabs(&app.ui).map(|t| t.info().0).collect();
|
||||
assert_eq!(order, ["libraries", "properties", "layers"]);
|
||||
let saved: crate::state::UiState = serde_json::from_slice(&serde_json::to_vec(&app.ui).unwrap()).unwrap();
|
||||
assert_eq!(saved.dock_tab_order, app.ui.dock_tab_order);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_dock_tab_order_appends_tabs_missing_from_preferences() {
|
||||
let ui =
|
||||
crate::state::UiState { dock_tab: DockTab::Layers, dock_tab_order: vec![DockTab::Layers, DockTab::Properties], ..Default::default() };
|
||||
let order = floating::resolved_dock_tab_order(&ui);
|
||||
assert_eq!(order.len(), DockTab::ALL.len());
|
||||
assert_eq!(order[..2], [DockTab::Layers, DockTab::Properties]);
|
||||
assert_eq!(order[2], DockTab::Libraries);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_collapse_command_sets_or_toggles_the_dock() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
assert_eq!(app.run("window.collapseDock", json!({})).unwrap(), json!(true));
|
||||
assert!(app.ui.dock_collapsed && crate::menus::checked(&app, "window.collapseDock", &json!({})) == Some(true));
|
||||
assert_eq!(app.run("window.collapseDock", json!({"collapsed": true})).unwrap(), json!(true));
|
||||
assert!(app.run("window.collapseDock", json!({"collapsed": "yes"})).is_err());
|
||||
// Window › Layers pops Layers out of its icon while collapsed.
|
||||
app.run("window.panel", json!({"panel": "layers"})).unwrap();
|
||||
assert_eq!(app.ui.open_panel.as_deref(), Some("layers"));
|
||||
assert_eq!(crate::menus::checked(&app, "window.panel", &json!({"panel": "layers"})), Some(true));
|
||||
assert_eq!(app.run("window.collapseDock", json!({})).unwrap(), json!(false));
|
||||
assert_eq!((app.ui.open_panel.as_deref(), app.ui.dock_tab), (None, DockTab::Layers));
|
||||
app.run("window.panel", json!({"panel": "properties"})).unwrap();
|
||||
assert_eq!((app.ui.open_panel.as_deref(), app.ui.dock_tab), (None, DockTab::Properties));
|
||||
// Saved with the preferences and with a user workspace; the built-in ones expand it.
|
||||
app.run("window.collapseDock", json!({"collapsed": true})).unwrap();
|
||||
let saved: crate::UiState = serde_json::from_slice(&serde_json::to_vec(&app.ui).unwrap()).unwrap();
|
||||
assert!(saved.sanitized().dock_collapsed);
|
||||
app.run("window.workspace.new", json!({"name": "Icons"})).unwrap();
|
||||
app.run("window.workspace", json!({"name": "Essentials"})).unwrap();
|
||||
assert!(!app.ui.dock_collapsed, "Essentials shows the panels");
|
||||
app.run("window.workspace", json!({"name": "Icons"})).unwrap();
|
||||
assert!(app.ui.dock_collapsed, "the user workspace keeps them collapsed");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3688,13 +3688,10 @@ axis Horizontal Хоризонтално
|
||||
axis Vertical Вертикално
|
||||
axis Both И двете
|
||||
Drag to move Плъзнете за преместване
|
||||
Drag to move out of the set Плъзнете, за да извадите от групата
|
||||
Drag to float as a toolbar Плъзнете, за да го направите плаваща лента с инструменти
|
||||
Put back in the toolbar Връщане в лентата с инструменти
|
||||
Float Panel Плаващ панел
|
||||
Dock Panel Закотвен панел
|
||||
Drag to float the panel group Плъзнете, за да направите групата от панели плаваща
|
||||
Put back in the dock Връщане в дока
|
||||
Drag to float the toolbar Плъзнете, за да направите лентата с инструменти плаваща
|
||||
Drag to the window's left edge to dock Плъзнете до левия ръб на прозореца за закотвяне
|
||||
Float Tool Group Плаваща група инструменти
|
||||
@@ -4526,7 +4523,6 @@ effect Soft Меко
|
||||
@msg before must name a panel параметърът „преди“ трябва да указва панел
|
||||
@msg docking action contains an unknown panel or an excessive split path действието по закачане съдържа непознат панел или прекомерен път на разделяне
|
||||
@msg group must be true or false групата трябва да е истина или лъжа
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} панелът е плаващ; свийте групата му с window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group панелът не е в група на дока
|
||||
@msg only a dock group above or below others collapses, not one beside another свива се само група на дока над или под други, не и такава до друга
|
||||
@msg icon must be true or false icon трябва да е истина или лъжа
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -770,7 +770,6 @@
|
||||
% of all commits % všech commitů
|
||||
Lines +/− Řádky +/−
|
||||
Drag to move Přetažením přesunete
|
||||
Drag to move out of the set Přetažením přesunete mimo sadu
|
||||
Drag to float as a toolbar Přetažením vytvoříte plovoucí panel nástrojů
|
||||
Put back in the toolbar Vrátit do panelu nástrojů
|
||||
Float Panel Plovoucí panel
|
||||
@@ -3833,8 +3832,6 @@ effect Regular Pravidelný
|
||||
Replace With Font Nahradit písmem
|
||||
— substituted by {style} — nahrazeno řezem {style}
|
||||
— {n} characters from another font — znaky z jiného písma: {n}
|
||||
Drag to float the panel group Přetažením vytvoříte plovoucí skupinu panelů
|
||||
Put back in the dock Vrátit do doku
|
||||
All Scripts Všechny systémy písma
|
||||
Any Kind Libovolný druh
|
||||
Font Technology Technologie písma
|
||||
@@ -4807,7 +4804,6 @@ imageTrace Outline Obrys
|
||||
@msg before must name a panel before musí určovat panel
|
||||
@msg docking action contains an unknown panel or an excessive split path akce ukotvení obsahuje neznámý panel nebo příliš dlouhou cestu rozdělení
|
||||
@msg group must be true or false group musí být true nebo false
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} panel je plovoucí; jeho skupinu sbalíte pomocí window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group panel není ve skupině doku
|
||||
@msg only a dock group above or below others collapses, not one beside another sbalit lze jen skupinu doku nad nebo pod jinými, ne vedle jiné
|
||||
@msg icon must be true or false icon musí být true nebo false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -707,7 +707,6 @@
|
||||
% of all commits % af alle commits
|
||||
Lines +/− Linjer +/−
|
||||
Drag to move Træk for at flytte
|
||||
Drag to move out of the set Træk for at flytte ud af sættet
|
||||
Drag to float as a toolbar Træk for at gøre den flydende som en værktøjslinje
|
||||
Put back in the toolbar Sæt tilbage på værktøjslinjen
|
||||
Float Panel Flydende panel
|
||||
@@ -3578,8 +3577,6 @@ effect Regular Regular
|
||||
Replace With Font Erstat med skrifttype
|
||||
— substituted by {style} — erstattet af {style}
|
||||
— {n} characters from another font — {n} tegn fra en anden skrifttype
|
||||
Drag to float the panel group Træk for at gøre panelgruppen flydende
|
||||
Put back in the dock Sæt tilbage i dokken
|
||||
All Scripts Alle scripts
|
||||
Any Kind Enhver type
|
||||
Font Technology Skrifttypeteknologi
|
||||
@@ -4492,7 +4489,6 @@ imageTrace Outline Kontur
|
||||
@msg before must name a panel before skal angive et panel
|
||||
@msg docking action contains an unknown panel or an excessive split path dokningshandlingen indeholder et ukendt panel eller en overdreven opdelingssti
|
||||
@msg group must be true or false group skal være true eller false
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} panelet flyder; skjul dets gruppe med window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group panelet er ikke i en dockgruppe
|
||||
@msg only a dock group above or below others collapses, not one beside another kun en dockgruppe over eller under andre kan skjules, ikke en ved siden af en anden
|
||||
@msg icon must be true or false icon skal være true eller false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3685,13 +3685,10 @@ axis Vertical Vertikal
|
||||
axis Both Beide
|
||||
theme Appearance Erscheinungsbild
|
||||
Drag to move Zum Verschieben ziehen
|
||||
Drag to move out of the set Ziehen, um aus dem Satz zu lösen
|
||||
Drag to float as a toolbar Ziehen, um als eigene Werkzeugleiste abzulösen
|
||||
Put back in the toolbar Zurück in die Werkzeugleiste setzen
|
||||
Float Panel Bedienfeld ablösen
|
||||
Dock Panel Bedienfeld andocken
|
||||
Drag to float the panel group Ziehen, um die Bedienfeldgruppe abzulösen
|
||||
Put back in the dock Zurück ins Dock setzen
|
||||
Drag to float the toolbar Ziehen, um die Werkzeugleiste abzulösen
|
||||
Drag to the window's left edge to dock Zum Andocken an den linken Fensterrand ziehen
|
||||
Float Tool Group Werkzeuggruppe ablösen
|
||||
@@ -4619,7 +4616,6 @@ theme Appearance Erscheinungsbild
|
||||
@msg before must name a panel before muss ein Bedienfeld benennen
|
||||
@msg docking action contains an unknown panel or an excessive split path Die Docking-Aktion enthält ein unbekanntes Bedienfeld oder einen zu langen Teilungspfad
|
||||
@msg group must be true or false group muss true oder false sein
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} Bedienfeld ist abgelöst; seine Gruppe mit window.panel.float {panel, collapsed} minimieren
|
||||
@msg panel is not in a dock group Bedienfeld ist in keiner Dock-Gruppe
|
||||
@msg only a dock group above or below others collapses, not one beside another nur eine Dock-Gruppe über oder unter anderen lässt sich minimieren, nicht eine daneben
|
||||
@msg icon must be true or false icon muss true oder false sein
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3898,11 +3898,8 @@ theme Appearance Εμφάνιση
|
||||
Joins Συνδέσεις
|
||||
Axis Άξονας
|
||||
Drag to move Σύρετε για μετακίνηση
|
||||
Drag to move out of the set Σύρετε για αφαίρεση από το σύνολο
|
||||
Drag to float as a toolbar Σύρετε για αποδέσμευση ως γραμμή εργαλείων
|
||||
Put back in the toolbar Επαναφορά στη γραμμή εργαλείων
|
||||
Drag to float the panel group Σύρετε για αποδέσμευση της ομάδας πινάκων
|
||||
Put back in the dock Επαναφορά στην περιοχή σύνδεσης
|
||||
Drag to float the toolbar Σύρετε για αποδέσμευση της γραμμής εργαλείων
|
||||
Drag to the window's left edge to dock Σύρετε στο αριστερό άκρο του παραθύρου για σύνδεση
|
||||
Float Tool Group Αποδέσμευση ομάδας εργαλείων
|
||||
@@ -4522,7 +4519,6 @@ theme Appearance Εμφάνιση
|
||||
@msg before must name a panel το before πρέπει να ορίζει πίνακα
|
||||
@msg docking action contains an unknown panel or an excessive split path η ενέργεια σύνδεσης περιέχει άγνωστο πίνακα ή υπερβολικά βαθιά διαδρομή διαίρεσης
|
||||
@msg group must be true or false το group πρέπει να είναι true ή false
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} Το πάνελ αιωρείται· συμπτύξτε την ομάδα του με window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group Το πάνελ δεν βρίσκεται σε ομάδα αγκύρωσης
|
||||
@msg only a dock group above or below others collapses, not one beside another συμπτύσσεται μόνο ομάδα αγκύρωσης πάνω ή κάτω από άλλες, όχι δίπλα σε άλλη
|
||||
@msg icon must be true or false το icon πρέπει να είναι true ή false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2069 and column 19.
|
@@ -3761,13 +3761,10 @@ axis Horizontal Horizontal
|
||||
axis Vertical Vertical
|
||||
axis Both Ambos
|
||||
Drag to move Arrastrar para mover
|
||||
Drag to move out of the set Arrastrar para sacar del conjunto
|
||||
Drag to float as a toolbar Arrastrar para flotar como barra de herramientas
|
||||
Put back in the toolbar Devolver a la barra de herramientas
|
||||
Float Panel Hacer flotar el panel
|
||||
Dock Panel Anclar panel
|
||||
Drag to float the panel group Arrastrar para hacer flotar el grupo de paneles
|
||||
Put back in the dock Devolver al área de paneles
|
||||
Drag to float the toolbar Arrastrar para hacer flotar la barra de herramientas
|
||||
Drag to the window's left edge to dock Arrastrar al borde izquierdo de la ventana para anclar
|
||||
Float Tool Group Hacer flotar el grupo de herramientas
|
||||
@@ -4622,7 +4619,6 @@ axis Both Ambos
|
||||
@msg before must name a panel before debe indicar un panel
|
||||
@msg docking action contains an unknown panel or an excessive split path La acción de anclaje contiene un panel desconocido o una ruta de divisiones demasiado larga
|
||||
@msg group must be true or false group debe ser true o false
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} el panel flota; contraiga su grupo con window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group el panel no está en un grupo del acoplamiento
|
||||
@msg only a dock group above or below others collapses, not one beside another solo se contrae un grupo del acoplamiento situado encima o debajo de otros, no al lado
|
||||
@msg icon must be true or false icon debe ser true o false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -758,7 +758,6 @@
|
||||
% of all commits % kaikista commiteista
|
||||
Lines +/− Rivit +/−
|
||||
Drag to move Siirrä vetämällä
|
||||
Drag to move out of the set Siirrä joukosta vetämällä
|
||||
Drag to float as a toolbar Kelluta työkalurivinä vetämällä
|
||||
Put back in the toolbar Palauta työkaluriviin
|
||||
Float Panel Kelluta paneeli
|
||||
@@ -3757,8 +3756,6 @@ effect Regular Normaali
|
||||
Replace With Font Korvaa fontilla
|
||||
— substituted by {style} — korvattu tyylillä {style}
|
||||
— {n} characters from another font — {n} merkkiä toisesta fontista
|
||||
Drag to float the panel group Irrota paneeliryhmä vetämällä
|
||||
Put back in the dock Palauta telakkaan
|
||||
All Scripts Kaikki kirjoitusjärjestelmät
|
||||
Any Kind Mikä tahansa laji
|
||||
Font Technology Fonttitekniikka
|
||||
@@ -4714,7 +4711,6 @@ imageTrace Outline Ääriviiva
|
||||
@msg before must name a panel before-arvon on nimettävä paneeli
|
||||
@msg docking action contains an unknown panel or an excessive split path telakointitoiminto sisältää tuntemattoman paneelin tai liian pitkän jakopolun
|
||||
@msg group must be true or false group-arvon on oltava true tai false
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} paneeli kelluu; kutista sen ryhmä komennolla window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group paneeli ei ole telakkaryhmässä
|
||||
@msg only a dock group above or below others collapses, not one beside another vain muiden ylä- tai alapuolella oleva telakkaryhmä kutistuu, ei toisen vieressä oleva
|
||||
@msg icon must be true or false icon-arvon on oltava true tai false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3758,13 +3758,10 @@ axis Horizontal Horizontal
|
||||
axis Vertical Vertical
|
||||
axis Both Les deux
|
||||
Drag to move Faire glisser pour déplacer
|
||||
Drag to move out of the set Faire glisser pour sortir de l’ensemble
|
||||
Drag to float as a toolbar Faire glisser pour détacher en barre d’outils flottante
|
||||
Put back in the toolbar Remettre dans la barre d’outils
|
||||
Float Panel Rendre le panneau flottant
|
||||
Dock Panel Ancrer le panneau
|
||||
Drag to float the panel group Faire glisser pour rendre le groupe de panneaux flottant
|
||||
Put back in the dock Remettre dans la zone d’ancrage
|
||||
Drag to float the toolbar Faire glisser pour rendre la barre d’outils flottante
|
||||
Drag to the window's left edge to dock Faire glisser vers le bord gauche de la fenêtre pour ancrer
|
||||
Float Tool Group Rendre le groupe d’outils flottant
|
||||
@@ -4634,7 +4631,6 @@ effect Soft Doux
|
||||
@msg before must name a panel before doit désigner un panneau
|
||||
@msg docking action contains an unknown panel or an excessive split path L’action d’ancrage contient un panneau inconnu ou un chemin de divisions trop long
|
||||
@msg group must be true or false group doit être true ou false
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} le panneau est flottant ; réduisez son groupe avec window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group le panneau n’est dans aucun groupe du dock
|
||||
@msg only a dock group above or below others collapses, not one beside another seul un groupe du dock situé au-dessus ou au-dessous d’autres se réduit, pas un groupe à côté
|
||||
@msg icon must be true or false icon doit être true ou false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1100,10 +1100,8 @@ font-technology Variable Változó
|
||||
Drag the art to move it on the paper Húzd a grafikát a papíron való áthelyezéshez
|
||||
Drag the light, or its rings. Shift snaps to 15°. Húzd a fényt vagy a gyűrűit. Shift: illesztés 15°-onként.
|
||||
Drag to float as a toolbar Húzd el, hogy önálló eszköztárként lebegjen
|
||||
Drag to float the panel group Húzással a panelcsoport lebegtethető
|
||||
Drag to float the toolbar Húzd el az eszköztár lebegtetéséhez
|
||||
Drag to move Húzással áthelyezhető
|
||||
Drag to move out of the set Húzással kivehető a csoportból
|
||||
Drag to the window's left edge to dock Dokkoláshoz húzd az ablak bal szélére
|
||||
Step up or down Léptetés fel vagy le
|
||||
Drag values or labels. Double-click a value to type. Húzd az értékeket vagy a címkéket. Gépeléshez kattints duplán egy értékre.
|
||||
@@ -2972,7 +2970,6 @@ mark-style Japanese Japán
|
||||
Puppet Warp Pins Bábtorzítás tűi
|
||||
Puppet Warp Tool Bábtorzítás
|
||||
Purple Lila
|
||||
Put back in the dock Visszahelyezés a dokkba
|
||||
Put back in the toolbar Visszahelyezés az eszköztárba
|
||||
Quality Minőség
|
||||
Quality {quality} Minőség: {quality}
|
||||
@@ -4641,7 +4638,6 @@ imageTrace Outline Körvonal
|
||||
@msg give the path `id` and the width point `index` add meg az útvonal `id` és a szélességi pont `index` értékét
|
||||
@msg gradient “{name}” isn't defined in the file: a plain colour stands in for it a színátmenet nincs megadva a fájlban: „{name}”; helyette egyszínű kitöltés áll
|
||||
@msg group must be true or false a group értéke true vagy false lehet
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} a panel lebeg; a csoportját a window.panel.float {panel, collapsed} paranccsal csukhatja össze
|
||||
@msg panel is not in a dock group a panel nincs dokkolt csoportban
|
||||
@msg only a dock group above or below others collapses, not one beside another csak a többi fölötti vagy alatti dokkolt csoport csukható össze, a mellettük lévő nem
|
||||
@msg icon must be true or false az icon értéke true vagy false lehet
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -707,7 +707,6 @@
|
||||
% of all commits % dari semua komit
|
||||
Lines +/− Baris +/−
|
||||
Drag to move Seret untuk memindahkan
|
||||
Drag to move out of the set Seret untuk memindahkan keluar dari set
|
||||
Drag to float as a toolbar Seret untuk mengambang sebagai bilah alat
|
||||
Put back in the toolbar Kembalikan ke bilah alat
|
||||
Float Panel Mengambangkan panel
|
||||
@@ -3578,8 +3577,6 @@ effect Regular Reguler
|
||||
Replace With Font Ganti dengan font
|
||||
— substituted by {style} — digantikan oleh {style}
|
||||
— {n} characters from another font — {n} karakter dari font lain
|
||||
Drag to float the panel group Seret untuk mengapungkan grup panel
|
||||
Put back in the dock Kembalikan ke dok
|
||||
All Scripts Semua skrip
|
||||
Any Kind Jenis apa pun
|
||||
Font Technology Teknologi font
|
||||
@@ -4492,7 +4489,6 @@ imageTrace Outline Kontur
|
||||
@msg before must name a panel before harus menyebutkan panel
|
||||
@msg docking action contains an unknown panel or an excessive split path tindakan docking berisi panel tidak dikenal atau jalur pemisahan berlebihan
|
||||
@msg group must be true or false group harus true atau false
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} panel mengambang; ciutkan grupnya dengan window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group panel tidak berada dalam grup dok
|
||||
@msg only a dock group above or below others collapses, not one beside another hanya grup dok di atas atau di bawah grup lain yang bisa diciutkan, bukan yang berada di sampingnya
|
||||
@msg icon must be true or false icon harus true atau false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3837 and column 19.
|
@@ -3801,13 +3801,10 @@ axis Horizontal Orizzontale
|
||||
axis Vertical Verticale
|
||||
axis Both Entrambi
|
||||
Drag to move Trascina per spostare
|
||||
Drag to move out of the set Trascina per spostare fuori dall’insieme
|
||||
Drag to float as a toolbar Trascina per rendere mobile come barra degli strumenti
|
||||
Put back in the toolbar Riporta nella barra degli strumenti
|
||||
Float Panel Rendi mobile pannello
|
||||
Dock Panel Ancora pannello
|
||||
Drag to float the panel group Trascina per rendere mobile il gruppo di pannelli
|
||||
Put back in the dock Riporta nell'area dei pannelli
|
||||
Drag to float the toolbar Trascina per rendere mobile la barra degli strumenti
|
||||
Drag to the window's left edge to dock Trascina sul bordo sinistro della finestra per ancorare
|
||||
Float Tool Group Rendi mobile gruppo strumenti
|
||||
@@ -4664,7 +4661,6 @@ effect Regular Normale
|
||||
@msg before must name a panel before deve indicare un pannello
|
||||
@msg docking action contains an unknown panel or an excessive split path L’azione di ancoraggio contiene un pannello sconosciuto o un percorso di divisioni troppo lungo
|
||||
@msg group must be true or false group deve essere true o false
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} il pannello è mobile; comprimi il suo gruppo con window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group il pannello non è in un gruppo del dock
|
||||
@msg only a dock group above or below others collapses, not one beside another si comprime solo un gruppo del dock sopra o sotto altri, non uno accanto
|
||||
@msg icon must be true or false icon deve essere true o false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3738,13 +3738,10 @@ axis Horizontal 水平方向
|
||||
axis Vertical 垂直方向
|
||||
axis Both 両方
|
||||
Drag to move 移動するにはドラッグ
|
||||
Drag to move out of the set ドラッグしてセットから出す
|
||||
Drag to float as a toolbar ドラッグしてツールバーとして浮かせる
|
||||
Put back in the toolbar ツールバーに戻す
|
||||
Float Panel パネルをフローティング
|
||||
Dock Panel パネルをドッキング
|
||||
Drag to float the panel group ドラッグしてパネルグループをフローティング
|
||||
Put back in the dock ドックに戻す
|
||||
Drag to float the toolbar ドラッグしてツールバーをフローティング
|
||||
Drag to the window's left edge to dock ウィンドウの左端までドラッグしてドッキング
|
||||
Float Tool Group ツールグループをフローティング
|
||||
@@ -4704,7 +4701,6 @@ effect Vertical 垂直
|
||||
@msg before must name a panel before にパネルを指定してください
|
||||
@msg docking action contains an unknown panel or an excessive split path ドッキング操作に不明なパネルまたは長すぎる分割パスが含まれています
|
||||
@msg group must be true or false group は true または false にしてください
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} パネルはフローティング中です。グループは window.panel.float {panel, collapsed} で折りたたんでください
|
||||
@msg panel is not in a dock group パネルはドックのグループにありません
|
||||
@msg only a dock group above or below others collapses, not one beside another 折りたためるのは他のグループの上下にあるドックのグループだけで、横に並んだグループは折りたためません
|
||||
@msg icon must be true or false icon は true または false である必要があります
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -649,7 +649,6 @@
|
||||
% of all commits ყველა კომიტის %
|
||||
Lines +/− სტრიქონები +/−
|
||||
Drag to move გადაათრიეთ გადასაადგილებლად
|
||||
Drag to move out of the set გადაათრიეთ ნაკრებიდან გამოსატანად
|
||||
Drag to float as a toolbar გადაათრიეთ მცურავ ხელსაწყოთა ზოლად გადასაქცევად
|
||||
Put back in the toolbar ხელსაწყოთა ზოლში დაბრუნება
|
||||
Float Panel მცურავი პანელი
|
||||
@@ -3361,8 +3360,6 @@ effect Regular ჩვეულებრივი
|
||||
Replace With Font ჩანაცვლება შრიფტით
|
||||
— substituted by {style} — ჩანაცვლებულია: {style}
|
||||
— {n} characters from another font — სხვა შრიფტის სიმბოლოები: {n}
|
||||
Drag to float the panel group გადაათრიეთ პანელების ჯგუფის მცურავად გადასაქცევად
|
||||
Put back in the dock დოკში დაბრუნება
|
||||
All Scripts ყველა დამწერლობა
|
||||
Any Kind ნებისმიერი ტიპი
|
||||
Font Technology შრიფტის ტექნოლოგია
|
||||
@@ -4255,7 +4252,6 @@ font-technology Variable ცვლადი
|
||||
@msg before must name a panel before უნდა ასახელებდეს პანელს
|
||||
@msg docking action contains an unknown panel or an excessive split path მიმაგრების მოქმედება შეიცავს უცნობ პანელს ან ზედმეტად გრძელ გაყოფის გზას
|
||||
@msg group must be true or false group უნდა იყოს true ან false
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} პანელი მცურავია; მისი ჯგუფი აკეცეთ window.panel.float {panel, collapsed}-ით
|
||||
@msg panel is not in a dock group პანელი არ არის დოკის ჯგუფში
|
||||
@msg only a dock group above or below others collapses, not one beside another იკეცება მხოლოდ სხვების ზემოთ ან ქვემოთ მდებარე დოკის ჯგუფი, არა სხვის გვერდით მდებარე
|
||||
@msg icon must be true or false icon უნდა იყოს true ან false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3727,13 +3727,10 @@ axis Horizontal 가로
|
||||
axis Vertical 세로
|
||||
axis Both 양쪽
|
||||
Drag to move 드래그하여 이동
|
||||
Drag to move out of the set 드래그하여 세트에서 꺼내기
|
||||
Drag to float as a toolbar 드래그하여 도구 모음으로 분리
|
||||
Put back in the toolbar 도구 모음으로 되돌리기
|
||||
Float Panel 패널 분리
|
||||
Dock Panel 패널 고정
|
||||
Drag to float the panel group 드래그하여 패널 그룹 분리
|
||||
Put back in the dock 독으로 되돌리기
|
||||
Drag to float the toolbar 드래그하여 도구 모음 분리
|
||||
Drag to the window's left edge to dock 창 왼쪽 가장자리로 드래그하여 고정
|
||||
Float Tool Group 도구 그룹 분리
|
||||
@@ -4634,7 +4631,6 @@ effect Vertical 세로
|
||||
@msg before must name a panel before는 패널을 지정해야 합니다
|
||||
@msg docking action contains an unknown panel or an excessive split path 도킹 동작에 알 수 없는 패널이나 지나치게 많은 분할 경로가 있습니다
|
||||
@msg group must be true or false group은 true 또는 false여야 합니다
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} 패널이 분리되어 있습니다. 그룹은 window.panel.float {panel, collapsed}로 접으세요
|
||||
@msg panel is not in a dock group 패널이 도크 그룹에 없습니다
|
||||
@msg only a dock group above or below others collapses, not one beside another 다른 그룹의 위나 아래에 있는 도크 그룹만 접을 수 있으며, 옆에 있는 그룹은 접을 수 없습니다
|
||||
@msg icon must be true or false icon은 true 또는 false여야 합니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -721,7 +721,6 @@
|
||||
% of all commits % van alle commits
|
||||
Lines +/− Regels +/−
|
||||
Drag to move Slepen om te verplaatsen
|
||||
Drag to move out of the set Slepen om uit de set te halen
|
||||
Drag to float as a toolbar Slepen om als losse werkbalk te laten zweven
|
||||
Put back in the toolbar Terug in de werkbalk zetten
|
||||
Float Panel Paneel laten zweven
|
||||
@@ -3592,8 +3591,6 @@ effect Regular Regelmatig
|
||||
Replace With Font Vervangen door lettertype
|
||||
— substituted by {style} — vervangen door {style}
|
||||
— {n} characters from another font — {n} tekens van een ander lettertype
|
||||
Drag to float the panel group Sleep om de paneelgroep te laten zweven
|
||||
Put back in the dock Terugzetten in het dock
|
||||
All Scripts Alle scripts
|
||||
Any Kind Elke soort
|
||||
Font Technology Lettertypetechnologie
|
||||
@@ -4504,7 +4501,6 @@ imageTrace Outline Contour
|
||||
@msg before must name a panel before moet een paneel benoemen
|
||||
@msg docking action contains an unknown panel or an excessive split path dokactie bevat een onbekend paneel of een overmatig splitspad
|
||||
@msg group must be true or false group moet true of false zijn
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} paneel zweeft; vouw de groep samen met window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group paneel staat niet in een dockgroep
|
||||
@msg only a dock group above or below others collapses, not one beside another alleen een dockgroep boven of onder andere vouwt samen, niet een ernaast
|
||||
@msg icon must be true or false icon moet true of false zijn
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -716,7 +716,6 @@
|
||||
% of all commits % wszystkich commitów
|
||||
Lines +/− Wiersze +/−
|
||||
Drag to move Przeciągnij, aby przesunąć
|
||||
Drag to move out of the set Przeciągnij, aby wyjąć z zestawu
|
||||
Drag to float as a toolbar Przeciągnij, aby oderwać jako pasek narzędzi
|
||||
Put back in the toolbar Odłóż z powrotem na pasek narzędzi
|
||||
Float Panel Odepnij panel
|
||||
@@ -3672,8 +3671,6 @@ effect Regular Normalny
|
||||
Replace With Font Zastąp czcionką
|
||||
— substituted by {style} — zastąpiona przez {style}
|
||||
— {n} characters from another font — znaki z innej czcionki: {n}
|
||||
Drag to float the panel group Przeciągnij, aby odpiąć grupę paneli
|
||||
Put back in the dock Odłóż z powrotem do doku
|
||||
All Scripts Wszystkie skrypty
|
||||
Any Kind Dowolny rodzaj
|
||||
Font Technology Technologia czcionek
|
||||
@@ -4635,7 +4632,6 @@ imageTrace Outline Kontur
|
||||
@msg icon must be true or false icon musi mieć wartość true lub false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon tylko dokuje panel w kolumnie ikon, bez anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column dokuje zestaw jako kolumnę doku, bez anchor ani icon
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} panel jest pływający; zwiń jego grupę za pomocą window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group panel nie należy do żadnej grupy doku
|
||||
@msg only a dock group above or below others collapses, not one beside another zwinąć można tylko grupę doku nad lub pod innymi, a nie obok innej
|
||||
@msg move requires a destination panel przeniesienie wymaga panelu docelowego
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3026,7 +3026,6 @@ imageTrace Outline Contorno
|
||||
% of all commits % de todos os commits
|
||||
Lines +/− Linhas +/−
|
||||
Drag to move Arraste para mover
|
||||
Drag to move out of the set Arraste para tirar do conjunto
|
||||
Drag to float as a toolbar Arraste para flutuar como barra de ferramentas
|
||||
Put back in the toolbar Devolver à barra de ferramentas
|
||||
Float Panel Flutuar Painel
|
||||
@@ -3356,8 +3355,6 @@ font-technology Variable Variáveis
|
||||
axis Horizontal Horizontal
|
||||
axis Vertical Vertical
|
||||
axis Both Ambos
|
||||
Drag to float the panel group Arraste para flutuar o grupo de painéis
|
||||
Put back in the dock Devolver ao dock
|
||||
Drag to float the toolbar Arraste para flutuar a barra de ferramentas
|
||||
Drag to the window's left edge to dock Arraste até a borda esquerda da janela para encaixar
|
||||
@msg no library `{id}` nenhuma biblioteca `{id}`
|
||||
@@ -4076,7 +4073,6 @@ axis Both Ambos
|
||||
@msg before must name a panel before deve nomear um painel
|
||||
@msg docking action contains an unknown panel or an excessive split path a ação de encaixe contém um painel desconhecido ou um caminho de divisão excessivo
|
||||
@msg group must be true or false group deve ser true ou false
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} o painel está flutuando; recolha o grupo com window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group o painel não está em um grupo do encaixe
|
||||
@msg only a dock group above or below others collapses, not one beside another só um grupo do encaixe acima ou abaixo de outros é recolhido, não um ao lado
|
||||
@msg icon must be true or false icon deve ser true ou false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3797,13 +3797,10 @@ axis Horizontal Горизонтально
|
||||
axis Vertical Вертикально
|
||||
axis Both Оба
|
||||
Drag to move Перетащите для перемещения
|
||||
Drag to move out of the set Перетащите, чтобы вынести из набора
|
||||
Drag to float as a toolbar Перетащите, чтобы сделать плавающей панелью инструментов
|
||||
Put back in the toolbar Вернуть на панель инструментов
|
||||
Float Panel Сделать панель плавающей
|
||||
Dock Panel Прикрепить панель
|
||||
Drag to float the panel group Перетащите, чтобы сделать группу панелей плавающей
|
||||
Put back in the dock Вернуть в док
|
||||
Drag to float the toolbar Перетащите, чтобы сделать панель инструментов плавающей
|
||||
Drag to the window's left edge to dock Перетащите к левому краю окна для прикрепления
|
||||
Float Tool Group Сделать группу инструментов плавающей
|
||||
@@ -4660,7 +4657,6 @@ axis Both Оба
|
||||
@msg before must name a panel before должен указывать панель
|
||||
@msg docking action contains an unknown panel or an excessive split path Действие прикрепления содержит неизвестную панель или слишком длинный путь разделений
|
||||
@msg group must be true or false group должен иметь значение true или false
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} панель плавающая; сверните её группу через window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group панель не находится в группе дока
|
||||
@msg only a dock group above or below others collapses, not one beside another свернуть можно только группу дока над или под другими, а не рядом с ними
|
||||
@msg icon must be true or false icon должен быть true или false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3688,13 +3688,10 @@ axis Horizontal Horisontell
|
||||
axis Vertical Vertikal
|
||||
axis Both Båda
|
||||
Drag to move Dra för att flytta
|
||||
Drag to move out of the set Dra för att flytta ut ur uppsättningen
|
||||
Drag to float as a toolbar Dra för att göra flytande som verktygsfält
|
||||
Put back in the toolbar Lägg tillbaka i verktygsfältet
|
||||
Float Panel Gör panelen flytande
|
||||
Dock Panel Docka panel
|
||||
Drag to float the panel group Dra för att göra panelgruppen flytande
|
||||
Put back in the dock Lägg tillbaka i dockan
|
||||
Drag to float the toolbar Dra för att göra verktygsfältet flytande
|
||||
Drag to the window's left edge to dock Dra till fönstrets vänsterkant för att docka
|
||||
Float Tool Group Gör verktygsgruppen flytande
|
||||
@@ -4543,7 +4540,6 @@ effect Soft Mjuk
|
||||
@msg before must name a panel before måste ange en panel
|
||||
@msg docking action contains an unknown panel or an excessive split path dockningsåtgärden innehåller en okänd panel eller en alltför djup delningssökväg
|
||||
@msg group must be true or false group måste vara true eller false
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} panelen flyter; komprimera dess grupp med window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group panelen ligger inte i en dockningsgrupp
|
||||
@msg only a dock group above or below others collapses, not one beside another bara en dockningsgrupp ovanför eller under andra komprimeras, inte en bredvid en annan
|
||||
@msg icon must be true or false icon måste vara true eller false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -3580,13 +3580,10 @@ axis Horizontal Yatay
|
||||
axis Vertical Dikey
|
||||
axis Both Her İkisi
|
||||
Drag to move Taşımak için sürükleyin
|
||||
Drag to move out of the set Setin dışına çıkmak için sürükleyin
|
||||
Drag to float as a toolbar Bağımsız araç çubuğu yapmak için sürükleyin
|
||||
Put back in the toolbar Araç çubuğuna geri yerleştir
|
||||
Float Panel Paneli Yüzer Yap
|
||||
Dock Panel Bağlantı İstasyonu Paneli
|
||||
Drag to float the panel group Panel grubunu ayırmak için sürükleyin
|
||||
Put back in the dock Dock'a geri koy
|
||||
Drag to float the toolbar Araç çubuğunu ayırmak için sürükleyin
|
||||
Drag to the window's left edge to dock Sabitlemek için pencerenin sol kenarına sürükleyin
|
||||
Float Tool Group Araç Grubunu Yüzer Yap
|
||||
@@ -5370,7 +5367,6 @@ theme Appearance Görünüm
|
||||
@msg thumbnail: no page {n} (the file has 1) thumbnail: {n}. sayfa yok (dosyada 1 sayfa var)
|
||||
@msg variables that aren't text were left out: {kinds} metin olmayan değişkenler dışarıda bırakıldı: {kinds}
|
||||
Collapse Panel Group Panel Grubunu Daralt
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} panel kayan durumda; grubunu window.panel.float {panel, collapsed} ile daraltın
|
||||
@msg panel is not in a dock group panel bir yuva grubunda değil
|
||||
@msg only a dock group above or below others collapses, not one beside another yalnızca diğerlerinin üstünde veya altındaki bir yuva grubu daraltılabilir, yanındaki değil
|
||||
@msg icon must be true or false icon true veya false olmalı
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -3790,13 +3790,10 @@ axis Horizontal Горизонтальна
|
||||
axis Vertical Вертикальна
|
||||
axis Both Обидві
|
||||
Drag to move Перетягніть для переміщення
|
||||
Drag to move out of the set Перетягніть, щоб винести з набору
|
||||
Drag to float as a toolbar Перетягніть, щоб від’єднати панель інструментів
|
||||
Put back in the toolbar Повернути на панель інструментів
|
||||
Float Panel Від’єднати панель
|
||||
Dock Panel Прикріпити панель
|
||||
Drag to float the panel group Перетягніть, щоб від’єднати групу панелей
|
||||
Put back in the dock Повернути у док
|
||||
Drag to float the toolbar Перетягніть, щоб від’єднати панель інструментів
|
||||
Drag to the window's left edge to dock Перетягніть до лівого краю вікна для прикріплення
|
||||
Float Tool Group Від’єднати групу інструментів
|
||||
@@ -4646,7 +4643,6 @@ axis Both Обидві
|
||||
@msg before must name a panel before має вказувати панель
|
||||
@msg docking action contains an unknown panel or an excessive split path Дія прикріплення містить невідому панель або надто довгий шлях поділів
|
||||
@msg group must be true or false group має мати значення true або false
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} панель від’єднана; згорніть її групу через window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group панель не входить до групи дока
|
||||
@msg only a dock group above or below others collapses, not one beside another згорнути можна лише групу дока над або під іншими, а не поруч з ними
|
||||
@msg icon must be true or false icon має бути true або false
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 749 and column 40.
|
@@ -984,10 +984,8 @@ font-technology Variable Biến thiên
|
||||
Drag the art to move it on the paper Kéo tác phẩm để di chuyển trên trang
|
||||
Drag the light, or its rings. Shift snaps to 15°. Kéo đèn hoặc vòng của nó. Shift khớp ở 15°.
|
||||
Drag to float as a toolbar Kéo để thả nổi thanh công cụ
|
||||
Drag to float the panel group Kéo để thả nổi nhóm bảng
|
||||
Drag to float the toolbar Kéo để thả nổi thanh công cụ
|
||||
Drag to move Kéo để di chuyển
|
||||
Drag to move out of the set Kéo để tách khỏi bộ
|
||||
Drag to the window's left edge to dock Kéo đến cạnh trái cửa sổ để neo
|
||||
Step up or down Tăng hoặc giảm một bước
|
||||
Drag values or labels. Double-click a value to type. Kéo giá trị hoặc nhãn. Bấm đúp vào giá trị để nhập.
|
||||
@@ -2750,7 +2748,6 @@ mark-style Japanese Kiểu Nhật
|
||||
Puppet Warp Biến dạng con rối
|
||||
Puppet Warp Pins Ghim biến dạng con rối
|
||||
Puppet Warp Tool Công cụ Biến dạng con rối
|
||||
Put back in the dock Đặt trở lại vào vùng neo
|
||||
Put back in the toolbar Đặt trở lại vào thanh công cụ
|
||||
Quality Chất lượng
|
||||
Quality {quality} Chất lượng {quality}
|
||||
@@ -4289,7 +4286,6 @@ theme Appearance Giao diện
|
||||
@msg icon must be true or false icon phải là true hoặc false
|
||||
@msg icon only docks a panel in the icon column, with no anchor icon chỉ neo bảng vào cột biểu tượng, không có anchor
|
||||
@msg column docks a set as a dock column, with no anchor or icon column neo một bộ thành cột neo, không có anchor hay icon
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} bảng đang nổi; hãy thu gọn nhóm của nó bằng window.panel.float {panel, collapsed}
|
||||
@msg panel is not in a dock group bảng không thuộc nhóm nào của dock
|
||||
@msg only a dock group above or below others collapses, not one beside another chỉ thu gọn được nhóm dock nằm trên hoặc dưới các nhóm khác, không phải nhóm nằm cạnh
|
||||
@msg guides are locked đường căn bị khóa
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4051 and column 10.
|
@@ -4326,7 +4326,6 @@ imageTrace Outline 轮廓
|
||||
@msg before must name a panel before 必须指定面板
|
||||
@msg docking action contains an unknown panel or an excessive split path 停靠操作包含未知面板或过长的拆分路径
|
||||
@msg group must be true or false group 必须是 true 或 false
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} 面板处于浮动状态;请使用 window.panel.float {panel, collapsed} 折叠其面板组
|
||||
@msg panel is not in a dock group 面板不在停放组中
|
||||
@msg only a dock group above or below others collapses, not one beside another 只有位于其他组上方或下方的停放组可以折叠,并排的不行
|
||||
@msg icon must be true or false icon 必须为 true 或 false
|
||||
@@ -4771,13 +4770,10 @@ axis Horizontal 水平
|
||||
axis Vertical 垂直
|
||||
axis Both 两者
|
||||
Drag to move 拖动以移动
|
||||
Drag to move out of the set 拖动以移出该组合
|
||||
Drag to float as a toolbar 拖动使其浮动为工具栏
|
||||
Put back in the toolbar 放回工具栏
|
||||
Float Panel 浮动面板
|
||||
Dock Panel 停放面板
|
||||
Drag to float the panel group 拖动以浮动面板组
|
||||
Put back in the dock 放回停放区
|
||||
Drag to float the toolbar 拖动以浮动工具栏
|
||||
Drag to the window's left edge to dock 拖到窗口左边缘以停放
|
||||
Float Tool Group 浮动工具组
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2301,7 +2301,6 @@ file-filter Templates 範本
|
||||
@msg before must name a panel before 必須指定面板
|
||||
@msg docking action contains an unknown panel or an excessive split path 停駐動作包含未知的面板或過長的分割路徑
|
||||
@msg group must be true or false group 必須是 true 或 false
|
||||
@msg panel floats; collapse its group with window.panel.float {panel, collapsed} 面板處於浮動狀態;請使用 window.panel.float {panel, collapsed} 摺疊其面板群組
|
||||
@msg panel is not in a dock group 面板不在停駐群組中
|
||||
@msg only a dock group above or below others collapses, not one beside another 只有位於其他群組上方或下方的停駐群組可以摺疊,並排的不行
|
||||
@msg icon must be true or false icon 必須為 true 或 false
|
||||
@@ -4575,13 +4574,10 @@ axis Horizontal 水平
|
||||
axis Vertical 垂直
|
||||
axis Both 兩者
|
||||
Drag to move 拖曳以移動
|
||||
Drag to move out of the set 拖曳以移出該組合
|
||||
Drag to float as a toolbar 拖曳使其浮動為工具列
|
||||
Put back in the toolbar 放回工具列
|
||||
Float Panel 浮動面板
|
||||
Dock Panel 停駐面板
|
||||
Drag to float the panel group 拖曳以浮動面板群組
|
||||
Put back in the dock 放回停駐區
|
||||
Drag to float the toolbar 拖曳以浮動工具列
|
||||
Drag to the window's left edge to dock 拖曳到視窗左側邊緣以停駐
|
||||
Float Tool Group 浮動工具組
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -0,0 +1,460 @@
|
||||
//! Saved preferences at the file boundary: docking data this version cannot read is dropped
|
||||
//! without resetting anything else, and layouts saved before the canvas joined the panel layout
|
||||
//! are converted.
|
||||
//!
|
||||
//! Those older files kept the dock as a tree of the right dock's panels only (`docking`, or the
|
||||
//! tabbed Properties · Layers · Libraries group when there was none) and described the rest in
|
||||
//! fields of their own: `floating_panels` (floating groups and sets, collapsed groups, sets
|
||||
//! docked as columns), `docking_icons` (panels dropped on the icon column), `docking_collapsed`
|
||||
//! (dock groups collapsed to their tabs), `dock_collapsed` and `dock_width`. [`from_legacy`]
|
||||
//! turns them into one layout; the legacy fields are read, never written.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use craft_ui::docking::{Column, Floating, Group, Layout, Location, Node};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::panel_docking::{self as docking, CANVAS, COLUMN_MAX, COLUMN_WIDTH, ICON_WIDTH, Placed};
|
||||
use crate::state::{DockTab, FloatingPanels, ICON_PANEL_GROUPS, ICON_PANELS, UiState};
|
||||
use crate::workspaces::Workspace;
|
||||
|
||||
/// Saved preferences, and whether reading them discarded anything.
|
||||
pub struct ReadPreferences {
|
||||
pub ui: UiState,
|
||||
/// False when unreadable docking data, or a layout that could not be converted, was dropped:
|
||||
/// the caller keeps a copy of the file before saving over it.
|
||||
pub intact: bool,
|
||||
/// True when a layout saved before the canvas joined the panel layout was converted: the
|
||||
/// caller keeps a copy of the file as it was.
|
||||
pub converted: bool,
|
||||
}
|
||||
|
||||
/// Floating groups read from one legacy list; more are dropped.
|
||||
const MAX_LEGACY_GROUPS: usize = 256;
|
||||
/// Size of a floating window that saved none (older files saved only its position).
|
||||
const FLOAT_WIDTH: f32 = 300.0;
|
||||
const FLOAT_HEIGHT: f32 = 400.0;
|
||||
const FLOAT_MIN: f32 = 200.0;
|
||||
/// The tallest a converted floating set starts.
|
||||
const STACK_MAX: f32 = 720.0;
|
||||
|
||||
impl UiState {
|
||||
/// Read saved preferences. None when the file is not a preferences file at all; unreadable
|
||||
/// docking data alone does not reset the other settings or the saved workspaces.
|
||||
pub fn read_preferences(bytes: &[u8]) -> Option<ReadPreferences> {
|
||||
let mut saved: Value = serde_json::from_slice(bytes).ok()?;
|
||||
let fields = saved.as_object_mut()?;
|
||||
let mut intact = recover(fields, Keys::SNAKE);
|
||||
for key in ["custom_workspaces", "workspace_changes"] {
|
||||
if let Some(Value::Array(workspaces)) = fields.get_mut(key) {
|
||||
for workspace in workspaces.iter_mut().filter_map(Value::as_object_mut) {
|
||||
intact &= recover(workspace, Keys::CAMEL);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut ui: UiState = serde_json::from_value(saved).ok()?;
|
||||
let outcome = migrate_ui(&mut ui);
|
||||
Some(ReadPreferences { ui, intact: intact && outcome.intact, converted: outcome.converted })
|
||||
}
|
||||
}
|
||||
|
||||
/// The JSON keys of the docking fields: `UiState` is snake_case, [`Workspace`] camelCase.
|
||||
struct Keys {
|
||||
hidden: &'static str,
|
||||
floating: &'static str,
|
||||
icons: &'static str,
|
||||
collapsed: &'static str,
|
||||
}
|
||||
|
||||
impl Keys {
|
||||
const SNAKE: Keys = Keys { hidden: "docking_hidden", floating: "floating_panels", icons: "docking_icons", collapsed: "docking_collapsed" };
|
||||
const CAMEL: Keys = Keys { hidden: "dockingHidden", floating: "floatingPanels", icons: "dockingIcons", collapsed: "dockingCollapsed" };
|
||||
}
|
||||
|
||||
/// Drop the docking data of one saved state that this version cannot read: the layout, each
|
||||
/// hidden place and floating group that fails, and legacy panel sets of another type. True
|
||||
/// when nothing was dropped.
|
||||
fn recover(fields: &mut Map<String, Value>, keys: Keys) -> bool {
|
||||
fn readable<T: serde::de::DeserializeOwned>(value: &Value) -> bool {
|
||||
serde_json::from_value::<T>(value.clone()).is_ok()
|
||||
}
|
||||
let mut intact = true;
|
||||
if fields.get("docking").is_some_and(|v| !readable::<Option<Layout<String>>>(v)) {
|
||||
fields.remove("docking");
|
||||
intact = false;
|
||||
}
|
||||
match fields.get_mut(keys.hidden) {
|
||||
Some(Value::Object(entries)) => {
|
||||
let before = entries.len();
|
||||
entries.retain(|_, location| readable::<Location<String>>(location));
|
||||
intact &= entries.len() == before;
|
||||
}
|
||||
Some(Value::Null) => {
|
||||
fields.remove(keys.hidden);
|
||||
}
|
||||
None => {}
|
||||
Some(_) => {
|
||||
fields.remove(keys.hidden);
|
||||
intact = false;
|
||||
}
|
||||
}
|
||||
match fields.get_mut(keys.floating) {
|
||||
Some(Value::Array(groups)) => {
|
||||
let before = groups.len();
|
||||
groups.retain(readable::<FloatingPanels>);
|
||||
intact &= groups.len() == before;
|
||||
}
|
||||
Some(Value::Null) => {
|
||||
fields.remove(keys.floating);
|
||||
}
|
||||
None => {}
|
||||
Some(_) => {
|
||||
fields.remove(keys.floating);
|
||||
intact = false;
|
||||
}
|
||||
}
|
||||
for key in [keys.icons, keys.collapsed] {
|
||||
if fields.get(key).is_some_and(|v| !readable::<BTreeSet<String>>(v)) {
|
||||
fields.remove(key);
|
||||
intact = false;
|
||||
}
|
||||
}
|
||||
intact
|
||||
}
|
||||
|
||||
/// Closed panels with the place each goes back to.
|
||||
type Hidden = BTreeMap<String, Location<String>>;
|
||||
|
||||
/// The docking fields of a `UiState` or a [`Workspace`].
|
||||
pub(crate) struct Fields<'a> {
|
||||
pub docking: &'a mut Option<Layout<String>>,
|
||||
pub hidden: &'a mut BTreeMap<String, Location<String>>,
|
||||
pub floating: &'a mut Vec<FloatingPanels>,
|
||||
pub icons: &'a mut BTreeSet<String>,
|
||||
pub collapsed: &'a mut BTreeSet<String>,
|
||||
pub dock_tab: DockTab,
|
||||
pub dock_tab_order: &'a [DockTab],
|
||||
pub dock_collapsed: bool,
|
||||
pub dock_width: Option<f32>,
|
||||
}
|
||||
|
||||
impl<'a> Fields<'a> {
|
||||
pub(crate) fn of_ui(ui: &'a mut UiState) -> Self {
|
||||
Fields {
|
||||
docking: &mut ui.docking,
|
||||
hidden: &mut ui.docking_hidden,
|
||||
floating: &mut ui.floating_panels,
|
||||
icons: &mut ui.docking_icons,
|
||||
collapsed: &mut ui.docking_collapsed,
|
||||
dock_tab: ui.dock_tab,
|
||||
dock_tab_order: &ui.dock_tab_order,
|
||||
dock_collapsed: ui.dock_collapsed,
|
||||
dock_width: ui.dock_width,
|
||||
}
|
||||
}
|
||||
|
||||
/// A workspace's fields; workspaces keep no tab order of their own.
|
||||
pub(crate) fn of_workspace(w: &'a mut Workspace, dock_tab_order: &'a [DockTab]) -> Self {
|
||||
Fields {
|
||||
docking: &mut w.docking,
|
||||
hidden: &mut w.docking_hidden,
|
||||
floating: &mut w.floating_panels,
|
||||
icons: &mut w.docking_icons,
|
||||
collapsed: &mut w.docking_collapsed,
|
||||
dock_tab: w.dock_tab,
|
||||
dock_tab_order,
|
||||
dock_collapsed: w.dock_collapsed,
|
||||
dock_width: w.dock_width,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What converting saved layouts did.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) struct Outcome {
|
||||
/// False when a layout was dropped.
|
||||
pub intact: bool,
|
||||
/// True when a legacy layout was converted (or dropped trying).
|
||||
pub converted: bool,
|
||||
}
|
||||
|
||||
impl Outcome {
|
||||
fn and(self, other: Outcome) -> Outcome {
|
||||
Outcome { intact: self.intact && other.intact, converted: self.converted || other.converted }
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the layouts of `ui`, its saved workspaces and the changes kept for each workspace.
|
||||
pub(crate) fn migrate_ui(ui: &mut UiState) -> Outcome {
|
||||
let order = ui.dock_tab_order.clone();
|
||||
let mut outcome = migrate(Fields::of_ui(ui));
|
||||
for w in ui.custom_workspaces.iter_mut().chain(ui.workspace_changes.iter_mut()) {
|
||||
outcome = outcome.and(migrate(Fields::of_workspace(w, &order)));
|
||||
}
|
||||
outcome
|
||||
}
|
||||
|
||||
/// Convert one saved state's legacy layout in place and clear its legacy fields. A state with
|
||||
/// no legacy data is left alone: without a layout, its flags still describe its default one.
|
||||
/// Not intact when a layout was dropped: one this version cannot use, or a conversion that
|
||||
/// failed (the state then falls back to its default layout).
|
||||
pub(crate) fn migrate(f: Fields<'_>) -> Outcome {
|
||||
let current = f.docking.as_ref().is_some_and(has_canvas);
|
||||
let legacy = !current && (f.docking.is_some() || !f.floating.is_empty() || !f.icons.is_empty() || !f.collapsed.is_empty());
|
||||
let mut intact = true;
|
||||
if current {
|
||||
intact = f.docking.as_ref().is_some_and(docking::valid);
|
||||
} else if legacy {
|
||||
match from_legacy(&f) {
|
||||
Some((layout, hidden)) => {
|
||||
*f.docking = Some(layout);
|
||||
*f.hidden = hidden;
|
||||
}
|
||||
None => {
|
||||
*f.docking = None;
|
||||
f.hidden.clear();
|
||||
intact = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
f.floating.clear();
|
||||
f.icons.clear();
|
||||
f.collapsed.clear();
|
||||
Outcome { intact, converted: legacy }
|
||||
}
|
||||
|
||||
/// The layout older versions described with `f`, and the hidden places that still apply. The
|
||||
/// dock, from the window edge inwards: the old dock tree's columns (its groups stacked top to
|
||||
/// bottom; a horizontal split puts its parts side by side), the icon column, then the floating
|
||||
/// sets docked as columns. Old split ratios are not kept. None when the result is not a valid
|
||||
/// layout.
|
||||
pub(crate) fn from_legacy(f: &Fields<'_>) -> Option<(Layout<String>, Hidden)> {
|
||||
let old = f.docking.as_ref();
|
||||
let mut placed = Placed::default();
|
||||
let groups: Vec<&FloatingPanels> = f.floating.iter().take(MAX_LEGACY_GROUPS).collect();
|
||||
|
||||
// Floating groups and sets first: the old dock tree never held a floating panel.
|
||||
let mut floating = Vec::new();
|
||||
let mut docked: Vec<(u32, Column<String>)> = Vec::new();
|
||||
let mut sets: Vec<u32> = Vec::new();
|
||||
for (k, g) in groups.iter().enumerate() {
|
||||
if let Some(set) = g.column {
|
||||
if sets.contains(&set) {
|
||||
continue;
|
||||
}
|
||||
sets.push(set);
|
||||
}
|
||||
let members: Vec<&FloatingPanels> = match g.column {
|
||||
Some(set) => groups.iter().skip(k).filter(|o| o.column == Some(set)).copied().collect(),
|
||||
None => vec![*g],
|
||||
};
|
||||
let kept: Vec<(Group<String>, f32)> = members
|
||||
.iter()
|
||||
.filter_map(|m| {
|
||||
let mut group = take_group(&mut placed, &m.panels, m.active)?;
|
||||
group.minimized = m.collapsed;
|
||||
let height = old_height(old, &group.panels);
|
||||
Some((group, height))
|
||||
})
|
||||
.collect();
|
||||
if kept.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let width = g.width.filter(|w| w.is_finite()).map_or(FLOAT_WIDTH, |w| w.clamp(FLOAT_MIN, COLUMN_MAX));
|
||||
match g.docked {
|
||||
Some(n) => {
|
||||
let groups = kept.into_iter().map(|(group, _)| group);
|
||||
match docked.iter_mut().find(|(m, _)| *m == n) {
|
||||
Some((_, column)) => column.groups.extend(groups),
|
||||
None => docked.push((n, Column::new(groups.collect(), width))),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let at = finite_pos(g.pos);
|
||||
let all_minimized = kept.iter().all(|(group, _)| group.minimized);
|
||||
let height = kept.iter().map(|(_, h)| *h).sum::<f32>().min(STACK_MAX);
|
||||
let rect = [at[0], at[1], width, height];
|
||||
// A floating window's groups have no minimized state of their own: a set is
|
||||
// minimized when all its groups were collapsed, else every group shows. Their
|
||||
// heights become the set's split ratios.
|
||||
let window = match kept.as_slice() {
|
||||
[(group, _)] => Some(Floating::single(group.panels.clone(), group.active, rect)),
|
||||
_ => Floating::stack(kept.into_iter().map(|(group, height)| Group { height: Some(height), ..group }).collect(), rect),
|
||||
};
|
||||
if let Some(mut window) = window {
|
||||
window.minimized = all_minimized;
|
||||
floating.push(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Windows of the old layout that the floating groups did not list.
|
||||
for window in old.map(|l| l.floating.as_slice()).unwrap_or_default() {
|
||||
if let Some(root) = unplaced_tree(&window.root, 0, &mut placed) {
|
||||
floating.push(Floating { minimized: window.minimized, ..Floating::new(root, usable_rect(window.rect)) });
|
||||
}
|
||||
}
|
||||
|
||||
// The dock tree, or the tabbed group of a file from before there was one.
|
||||
let tree_columns: Vec<Vec<Group<String>>> = match old {
|
||||
Some(layout) => {
|
||||
let mut reading = layout.root.as_ref().map(|root| tree_columns(root, 0, &mut placed)).unwrap_or_default();
|
||||
reading.reverse();
|
||||
reading
|
||||
}
|
||||
None => {
|
||||
let tabs: Vec<&str> = docking::main_tabs(f.dock_tab_order).into_iter().map(|t| t.info().0).filter(|id| !f.icons.contains(*id)).collect();
|
||||
let active = tabs.iter().position(|id| *id == f.dock_tab.info().0).unwrap_or(0);
|
||||
let tabs: Vec<String> = tabs.into_iter().map(String::from).collect();
|
||||
take_group(&mut placed, &tabs, active).map(|g| vec![vec![g]]).unwrap_or_default()
|
||||
}
|
||||
};
|
||||
let n = tree_columns.len().max(1) as f32;
|
||||
let tree_width = (docking::main_width(f.dock_width) / n).max(FLOAT_MIN.min(docking::main_width(f.dock_width)));
|
||||
let mut columns: Vec<Column<String>> = tree_columns
|
||||
.into_iter()
|
||||
.map(|mut groups| {
|
||||
for g in &mut groups {
|
||||
g.minimized |= g.panels.get(g.active).is_some_and(|p| f.collapsed.contains(p));
|
||||
}
|
||||
Column { groups, width: tree_width, iconic: f.dock_collapsed, iconic_width: Some(ICON_WIDTH) }
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Panels closed with a remembered place stay closed, except those the icon column showed
|
||||
// anyway (its rows, and the panels dropped on it): they take their place there.
|
||||
let in_rows = |id: &str| ICON_PANEL_GROUPS.iter().any(|row| row.contains(&id));
|
||||
let hidden: BTreeMap<String, Location<String>> =
|
||||
f.hidden.iter().take(craft_ui::docking::MAX_PANELS).map(|(id, location)| (id.clone(), location.clone())).collect();
|
||||
for id in hidden.keys().filter(|id| !in_rows(id) && !f.icons.contains(*id)) {
|
||||
let _ = placed.take(id);
|
||||
}
|
||||
|
||||
// The icon column: the tabbed group's panels dropped on it, its rows of icon panels, then
|
||||
// the other panels dropped on it.
|
||||
let mut icon_groups = Vec::new();
|
||||
let tabs: Vec<String> = DockTab::ALL.iter().map(|t| t.info().0.to_string()).filter(|id| f.icons.contains(id)).collect();
|
||||
icon_groups.extend(take_group(&mut placed, &tabs, 0));
|
||||
for row in ICON_PANEL_GROUPS {
|
||||
let row: Vec<String> = row.iter().map(|id| id.to_string()).collect();
|
||||
icon_groups.extend(take_group(&mut placed, &row, 0));
|
||||
}
|
||||
let dropped: Vec<String> = ICON_PANELS.iter().map(|e| e.0).filter(|id| !in_rows(id) && f.icons.contains(*id)).map(String::from).collect();
|
||||
icon_groups.extend(take_group(&mut placed, &dropped, 0));
|
||||
if !icon_groups.is_empty() {
|
||||
columns.push(Column { groups: icon_groups, width: COLUMN_WIDTH, iconic: true, iconic_width: Some(ICON_WIDTH) });
|
||||
}
|
||||
docked.sort_by_key(|(n, _)| *n);
|
||||
columns.extend(docked.into_iter().map(|(_, column)| column));
|
||||
|
||||
let layout = docking::with_canvas(columns, floating);
|
||||
if !docking::valid(&layout) {
|
||||
return None;
|
||||
}
|
||||
// A hidden place applies to a closed panel, and to a floating one Dock Panel sends back
|
||||
// there; a floating panel whose way back was the icon column finds its icon group again.
|
||||
let hidden = hidden
|
||||
.into_iter()
|
||||
.filter(|(id, _)| match layout.floating.iter().any(|w| w.contains(id)) {
|
||||
true => !f.icons.contains(id),
|
||||
false => !layout.contains(id),
|
||||
})
|
||||
.collect();
|
||||
Some((layout, hidden))
|
||||
}
|
||||
|
||||
/// A group of the panels of `panels` that have no place yet, showing the panel at `active` when
|
||||
/// it is kept.
|
||||
fn take_group(placed: &mut Placed, panels: &[String], active: usize) -> Option<Group<String>> {
|
||||
let front = panels.get(active).cloned();
|
||||
let kept: Vec<String> = panels.iter().take(craft_ui::docking::MAX_PANELS).filter_map(|p| placed.take(p)).collect();
|
||||
let active = front.and_then(|front| kept.iter().position(|p| *p == front)).unwrap_or(0);
|
||||
(!kept.is_empty()).then(|| Group::new(kept, active))
|
||||
}
|
||||
|
||||
/// The height the old layout gave the floating window of `panels`, else the default.
|
||||
fn old_height(old: Option<&Layout<String>>, panels: &[String]) -> f32 {
|
||||
old.and_then(|l| l.floating.iter().find(|w| panels.iter().any(|p| w.contains(p))))
|
||||
.map(|w| w.rect[3])
|
||||
.filter(|h| h.is_finite() && *h >= 40.0)
|
||||
.map_or(FLOAT_HEIGHT, |h| h.min(STACK_MAX))
|
||||
}
|
||||
|
||||
/// A saved floating window rect with a usable position and size.
|
||||
fn usable_rect(rect: [f32; 4]) -> [f32; 4] {
|
||||
let [x, y, width, height] = rect;
|
||||
let at = finite_pos([x, y]);
|
||||
let width = Some(width).filter(|w| w.is_finite() && *w > 0.0).map_or(FLOAT_WIDTH, |w| w.clamp(FLOAT_MIN, COLUMN_MAX));
|
||||
let height = Some(height).filter(|h| h.is_finite() && *h >= 40.0).map_or(FLOAT_HEIGHT, |h| h.min(STACK_MAX));
|
||||
[at[0], at[1], width, height]
|
||||
}
|
||||
|
||||
/// A saved floating position, or a place near the window's corner when it is not a number.
|
||||
fn finite_pos(pos: [f32; 2]) -> [f32; 2] {
|
||||
if pos.iter().all(|v| v.is_finite()) { pos.map(|v| v.clamp(-100_000.0, 100_000.0)) } else { [100.0, 100.0] }
|
||||
}
|
||||
|
||||
/// The groups of an old dock tree's node, column by column in reading order. A vertical split
|
||||
/// stacks its parts in one column; a horizontal split puts them side by side.
|
||||
fn tree_columns(node: &Node<String>, depth: usize, placed: &mut Placed) -> Vec<Vec<Group<String>>> {
|
||||
if depth > craft_ui::docking::MAX_DEPTH {
|
||||
return Vec::new();
|
||||
}
|
||||
let column = |groups: Vec<Group<String>>| if groups.is_empty() { Vec::new() } else { vec![groups] };
|
||||
match node {
|
||||
Node::Tabs { panels, active } => column(take_group(placed, panels, *active).into_iter().collect()),
|
||||
Node::Stack { entries } => column(
|
||||
entries
|
||||
.iter()
|
||||
.take(craft_ui::docking::MAX_PANELS)
|
||||
.filter_map(|e| placed.take(&e.panel).map(|id| Group { minimized: !e.open, ..Group::new(vec![id], 0) }))
|
||||
.collect(),
|
||||
),
|
||||
Node::Split { axis, first, second, .. } => {
|
||||
let mut parts = tree_columns(first, depth.saturating_add(1), placed);
|
||||
parts.extend(tree_columns(second, depth.saturating_add(1), placed));
|
||||
match axis {
|
||||
craft_ui::layout::SplitAxis::Horizontal => parts,
|
||||
_ => column(parts.into_iter().flatten().collect()),
|
||||
}
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A floating window's tree with only the panels not placed yet; a split that loses one side
|
||||
/// becomes its other side. None when no panel is left.
|
||||
fn unplaced_tree(node: &Node<String>, depth: usize, placed: &mut Placed) -> Option<Node<String>> {
|
||||
if depth > craft_ui::docking::MAX_DEPTH {
|
||||
return None;
|
||||
}
|
||||
match node {
|
||||
Node::Tabs { panels, active } => take_group(placed, panels, *active).map(|g| Node::Tabs { panels: g.panels, active: g.active }),
|
||||
Node::Stack { entries } => {
|
||||
let entries: Vec<_> = entries
|
||||
.iter()
|
||||
.take(craft_ui::docking::MAX_PANELS)
|
||||
.filter_map(|e| placed.take(&e.panel).map(|panel| craft_ui::docking::StackEntry { panel, ..e.clone() }))
|
||||
.collect();
|
||||
(!entries.is_empty()).then_some(Node::Stack { entries })
|
||||
}
|
||||
Node::Split { axis, size, first, second } => {
|
||||
let first = unplaced_tree(first, depth.saturating_add(1), placed);
|
||||
let second = unplaced_tree(second, depth.saturating_add(1), placed);
|
||||
match (first, second) {
|
||||
(Some(first), Some(second)) => Some(Node::Split { axis: *axis, size: *size, first: Box::new(first), second: Box::new(second) }),
|
||||
(one, other) => one.or(other),
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Does a saved layout hold the canvas (one saved by this version)?
|
||||
pub(crate) fn has_canvas(layout: &Layout<String>) -> bool {
|
||||
layout.panels().iter().any(|p| p.as_str() == CANVAS)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "layout_migration_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,229 @@
|
||||
use super::*;
|
||||
use craft_ui::docking::Side;
|
||||
use serde_json::json;
|
||||
|
||||
/// `ui.json` as VectorCraft 0.16 (8bf090b25) saved it after: Swatches moved below Layers;
|
||||
/// Color, then Stroke onto it, floated; Gradient floated with Transparency below it (a set);
|
||||
/// Align floated and collapsed; Pathfinder floated with Transform below it, the set docked as
|
||||
/// column 1; Libraries docked on the icon column; Symbols moved beside Swatches, then closed
|
||||
/// (0.16 then showed it in the icon column again);
|
||||
/// Swatches collapsed to its tab; the dock widened to 380; saved as workspace "Migration Test".
|
||||
const SAVED_BY_0_16: &str = include_str!("../tests/fixtures/ui-before-canvas-layout.json");
|
||||
|
||||
fn read(saved: &Value) -> ReadPreferences {
|
||||
UiState::read_preferences(&serde_json::to_vec(saved).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// The dock of a layout in the current shape: the canvas beside a dock at the end edge.
|
||||
fn dock(layout: &Layout<String>) -> &[Column<String>] {
|
||||
assert!(docking::valid(layout), "{layout:?}");
|
||||
let Some(Node::Split { first, second, .. }) = &layout.root else { panic!("expected the canvas split: {layout:?}") };
|
||||
assert!(matches!(&**first, Node::Tabs { panels, .. } if panels == &[CANVAS]));
|
||||
let Node::Dock { side: Side::End, columns } = &**second else { panic!("expected an end dock: {second:?}") };
|
||||
columns
|
||||
}
|
||||
|
||||
fn groups(column: &Column<String>) -> Vec<Vec<&str>> {
|
||||
column.groups.iter().map(|g| g.panels.iter().map(String::as_str).collect()).collect()
|
||||
}
|
||||
|
||||
fn window_groups(window: &Floating<String>) -> Vec<Vec<&str>> {
|
||||
let mut out = Vec::new();
|
||||
let mut visit = vec![&window.root];
|
||||
while let Some(node) = visit.pop() {
|
||||
match node {
|
||||
Node::Split { first, second, .. } => {
|
||||
visit.push(second);
|
||||
visit.push(first);
|
||||
}
|
||||
Node::Tabs { panels, .. } => out.push(panels.iter().map(String::as_str).collect()),
|
||||
_ => panic!("unexpected node {node:?}"),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_layout_saved_before_the_canvas_joined_keeps_its_arrangement() {
|
||||
let saved: Value = serde_json::from_str(SAVED_BY_0_16).unwrap();
|
||||
let read = read(&saved);
|
||||
assert!(read.intact && read.converted);
|
||||
let ui = read.ui;
|
||||
assert!(ui.floating_panels.is_empty() && ui.docking_icons.is_empty() && ui.docking_collapsed.is_empty());
|
||||
let layout = ui.docking.as_ref().expect("converted layout");
|
||||
let columns = dock(layout);
|
||||
assert_eq!(columns.len(), 3, "{columns:#?}");
|
||||
|
||||
let main = &columns[0];
|
||||
assert_eq!(groups(main), [vec!["properties", "layers"], vec!["swatches"]]);
|
||||
assert_eq!(main.groups[0].active, 1, "Layers in front");
|
||||
assert!(main.groups[1].minimized, "Swatches collapsed to its tab");
|
||||
assert!(!main.groups[0].minimized && !main.iconic);
|
||||
assert_eq!(main.width, 380.0, "dock width");
|
||||
|
||||
let icons = &columns[1];
|
||||
assert!(icons.iconic);
|
||||
assert_eq!(groups(icons)[0], ["libraries"], "dropped on the icon column, at its head");
|
||||
let iconic: Vec<&str> = groups(icons).concat();
|
||||
assert!(iconic.contains(&"colorGuide") && iconic.contains(&"brushes") && iconic.contains(&"variables"));
|
||||
assert!(groups(icons).contains(&vec!["brushes", "symbols", "patternOptions"]), "closed Symbols in its icon row");
|
||||
for elsewhere in ["swatches", "color", "stroke", "gradient", "transparency", "align", "pathfinder", "transform"] {
|
||||
assert!(!iconic.contains(&elsewhere), "{elsewhere} has a place of its own");
|
||||
}
|
||||
|
||||
let docked = &columns[2];
|
||||
assert_eq!(groups(docked), [vec!["pathfinder"], vec!["transform"]], "set docked as a column");
|
||||
assert!(!docked.iconic);
|
||||
|
||||
assert_eq!(layout.floating.len(), 3);
|
||||
let color = &layout.floating[0];
|
||||
assert_eq!(window_groups(color), [vec!["color", "stroke"]]);
|
||||
assert!(matches!(color.root, Node::Tabs { active: 1, .. }), "Stroke in front");
|
||||
assert_eq!(color.rect, [300.0, 120.0, 300.0, 400.0]);
|
||||
let set = &layout.floating[1];
|
||||
assert_eq!(window_groups(set), [vec!["gradient"], vec!["transparency"]], "floating set");
|
||||
assert_eq!(&set.rect[..3], &[640.0, 100.0, 300.0]);
|
||||
assert!(!set.minimized);
|
||||
let align = &layout.floating[2];
|
||||
assert_eq!(window_groups(align), [vec!["align"]]);
|
||||
assert!(align.minimized, "collapsed floating group");
|
||||
assert_eq!(&align.rect[..2], &[300.0, 560.0]);
|
||||
|
||||
// Symbols is in the icon column, and the floating panels came from it: none keeps a
|
||||
// place in the dock to go back to.
|
||||
assert!(ui.docking_hidden.is_empty(), "{:?}", ui.docking_hidden);
|
||||
|
||||
let workspace = ui.custom_workspaces.iter().find(|w| w.name == "Migration Test").expect("custom workspace");
|
||||
assert_eq!(workspace.docking, ui.docking);
|
||||
assert_eq!(workspace.docking_hidden, ui.docking_hidden);
|
||||
assert!(workspace.floating_panels.is_empty() && workspace.docking_icons.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_converted_file_saved_again_has_no_legacy_fields_and_loads_the_same() {
|
||||
let saved: Value = serde_json::from_str(SAVED_BY_0_16).unwrap();
|
||||
let ui = read(&saved).ui;
|
||||
let again = serde_json::to_value(&ui).unwrap();
|
||||
for key in ["floating_panels", "docking_icons", "docking_collapsed"] {
|
||||
assert!(again.get(key).is_none(), "{key} written");
|
||||
}
|
||||
for key in ["floatingPanels", "dockingIcons", "dockingCollapsed"] {
|
||||
assert!(again["custom_workspaces"][0].get(key).is_none(), "{key} written");
|
||||
}
|
||||
let reread = read(&again);
|
||||
assert!(reread.intact && !reread.converted);
|
||||
assert_eq!(reread.ui.docking, ui.docking);
|
||||
assert_eq!(reread.ui.docking_hidden, ui.docking_hidden);
|
||||
assert_eq!(reread.ui.custom_workspaces, ui.custom_workspaces);
|
||||
let ids = |ui: &UiState| ui.docking.iter().flat_map(|l| l.floating.iter().map(|w| w.id)).collect::<Vec<_>>();
|
||||
assert_eq!(ids(&reread.ui), ids(&ui), "floating windows keep their ids");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_from_before_the_shared_dock_converts_and_remembered_changes_too() {
|
||||
let ui = read(&json!({
|
||||
"dock_tab": "Libraries", "dock_collapsed": true, "dock_width": 400.0, "workspace": "Essentials",
|
||||
"floating_panels": [{"panels": ["layers"], "active": 0, "pos": [10.0, 20.0]}],
|
||||
"docking_icons": ["properties"],
|
||||
"workspace_changes": [{
|
||||
"name": "Essentials", "dockTab": "Layers",
|
||||
"floatingPanels": [{"panels": ["swatches", "brushes"], "active": 1, "pos": [50.0, 60.0], "width": 260.0, "collapsed": true}],
|
||||
}],
|
||||
}))
|
||||
.ui;
|
||||
let layout = ui.docking.as_ref().unwrap();
|
||||
let columns = dock(layout);
|
||||
assert_eq!(groups(&columns[0]), [vec!["libraries"]]);
|
||||
assert!(columns[0].iconic, "collapsed dock");
|
||||
assert_eq!(columns[0].width, 400.0);
|
||||
assert_eq!(groups(&columns[1])[0], ["properties"]);
|
||||
assert_eq!(window_groups(&layout.floating[0]), [vec!["layers"]]);
|
||||
assert_eq!(layout.floating[0].rect, [10.0, 20.0, 300.0, 400.0]);
|
||||
|
||||
let changed = ui.workspace_changes[0].docking.as_ref().unwrap();
|
||||
let columns = dock(changed);
|
||||
assert_eq!(groups(&columns[0]), [vec!["properties", "layers", "libraries"]]);
|
||||
assert_eq!(columns[0].groups[0].active, 1);
|
||||
assert!(!columns[0].iconic);
|
||||
assert!(groups(&columns[1]).contains(&vec!["symbols", "patternOptions"]));
|
||||
assert_eq!(window_groups(&changed.floating[0]), [vec!["swatches", "brushes"]]);
|
||||
assert!(matches!(changed.floating[0].root, Node::Tabs { active: 1, .. }));
|
||||
assert!(changed.floating[0].minimized);
|
||||
assert_eq!(changed.floating[0].rect, [50.0, 60.0, 260.0, 400.0]);
|
||||
|
||||
// A floating set keeps the heights of its groups.
|
||||
let ui = read(&json!({
|
||||
"floating_panels": [
|
||||
{"panels": ["stroke"], "active": 0, "pos": [0.0, 0.0], "column": 1},
|
||||
{"panels": ["gradient"], "active": 0, "pos": [0.0, 0.0], "column": 1},
|
||||
],
|
||||
"docking": {"root": {"Tabs": {"panels": ["properties"], "active": 0}}, "floating": [
|
||||
{"panels": ["stroke"], "active": 0, "rect": [0.0, 0.0, 300.0, 150.0]},
|
||||
{"panels": ["gradient"], "active": 0, "rect": [0.0, 0.0, 300.0, 450.0]},
|
||||
]},
|
||||
}))
|
||||
.ui;
|
||||
let set = &ui.docking.as_ref().unwrap().floating[0];
|
||||
let Node::Split { size: craft_ui::layout::SplitSize::Ratio(ratio), .. } = set.root else { panic!("{set:?}") };
|
||||
assert!((ratio - 0.25).abs() < 1e-4, "{ratio}");
|
||||
assert_eq!(set.rect[3], 600.0);
|
||||
|
||||
// Nothing to convert: the flags still describe the workspace's default layout.
|
||||
let read = read(&json!({"dock_tab": "Layers", "dock_collapsed": true, "dock_width": 400.0}));
|
||||
assert!(!read.converted);
|
||||
assert!(read.ui.docking.is_none());
|
||||
assert_eq!(read.ui.dock_width, Some(400.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unreadable_docking_data_is_dropped_and_reported() {
|
||||
let saved: Value = serde_json::from_str(SAVED_BY_0_16).unwrap();
|
||||
for (key, value) in [
|
||||
("docking", json!({"root": {"FutureNode": {}}, "floating": []})),
|
||||
("docking_hidden", json!(["malformed"])),
|
||||
("floating_panels", json!([{"panels": "color"}])),
|
||||
("docking_icons", json!(7)),
|
||||
] {
|
||||
let mut broken = saved.clone();
|
||||
broken[key] = value.clone();
|
||||
broken["custom_workspaces"][0]["docking"] = json!(false);
|
||||
let read = read(&broken);
|
||||
assert!(!read.intact, "{key}");
|
||||
assert_eq!(read.ui.workspace, "Migration Test", "the rest is kept");
|
||||
assert_eq!(read.ui.custom_workspaces.len(), 1);
|
||||
assert!(docking::valid(read.ui.docking.as_ref().unwrap()), "{key}: what is left still converts");
|
||||
}
|
||||
assert!(UiState::read_preferences(b"[1, 2]").is_none());
|
||||
assert!(UiState::read_preferences(br#"{"toolbar": "invalid"}"#).is_none(), "other fields stay strict");
|
||||
}
|
||||
|
||||
/// A hand-edited file: unknown and repeated panels, sizes out of range, more groups than any
|
||||
/// layout holds.
|
||||
#[test]
|
||||
fn untrusted_legacy_fields_convert_without_failing() {
|
||||
let mut floating: Vec<Value> =
|
||||
(0..2000).map(|k| json!({"panels": ["nosuch", "color"], "active": k, "pos": [1e39, -1e39], "column": k % 3})).collect();
|
||||
floating.insert(0, json!({"panels": ["stroke"], "active": 99, "pos": [0.0, 0.0], "width": 1e30, "docked": 4000000000u32}));
|
||||
let ui = read(&json!({
|
||||
"dock_width": -5.0,
|
||||
"floating_panels": floating,
|
||||
"docking_icons": ["nosuch", "glyphs", "tabs"],
|
||||
"docking_collapsed": ["properties", "nosuch"],
|
||||
"docking": {"root": {"Tabs": {"panels": ["properties", "properties", "nosuch"], "active": 9}}, "floating": [
|
||||
{"panels": ["brushes"], "active": 0, "rect": [0.0, 0.0, 0.0, 0.0]},
|
||||
{"panels": ["info"], "active": 0, "rect": [1e39, 1e39, 1e39, 1e39]},
|
||||
{"panels": ["navigator"], "active": 0, "rect": [-5.0, 2e6, -1.0, 1e7]},
|
||||
]},
|
||||
}))
|
||||
.ui;
|
||||
let layout = ui.docking.as_ref().unwrap();
|
||||
let columns = dock(layout);
|
||||
assert_eq!(groups(&columns[0]), [vec!["properties"]]);
|
||||
assert!(columns[0].groups[0].minimized);
|
||||
assert!(columns[0].width.is_finite());
|
||||
assert!(groups(&columns[1]).last().is_some_and(|g| g == &["tabs"]), "a panel outside the rows dropped on the icon column");
|
||||
assert_eq!(groups(columns.last().unwrap()), [vec!["stroke"]]);
|
||||
assert!(columns.last().unwrap().width <= COLUMN_MAX);
|
||||
assert_eq!(layout.floating.len(), 4, "every old window kept");
|
||||
assert!(layout.floating.iter().all(|w| w.rect.iter().all(|v| v.is_finite()) && w.rect[2] > 0.0 && w.rect[3] > 0.0));
|
||||
}
|
||||
+65
-21
@@ -27,7 +27,6 @@ pub mod dock;
|
||||
mod edit_toolbar;
|
||||
pub mod export_progress;
|
||||
pub mod find_font;
|
||||
pub mod floating;
|
||||
pub mod font_menu;
|
||||
mod frame_rate;
|
||||
mod free_transform;
|
||||
@@ -38,6 +37,7 @@ pub mod icons;
|
||||
pub(crate) mod ime;
|
||||
pub mod io;
|
||||
pub mod keymap;
|
||||
pub mod layout_migration;
|
||||
pub mod menus;
|
||||
pub mod native_menu;
|
||||
pub mod opening;
|
||||
@@ -63,6 +63,7 @@ pub mod theme;
|
||||
pub mod thumb_worker;
|
||||
pub mod titlebar;
|
||||
pub mod toolbar;
|
||||
mod tools_float;
|
||||
pub mod touch;
|
||||
mod ui_fonts;
|
||||
pub mod unsaved;
|
||||
@@ -334,8 +335,15 @@ pub struct Services {
|
||||
/// and the end to switch to the Eraser tool and back. Without it, pen pressure comes from
|
||||
/// touch events only ([`stylus`]).
|
||||
pub stylus: Option<stylus::StylusFeed>,
|
||||
/// The host's rewrites of a window's raw input (pen barrel button, closing the macOS menu
|
||||
/// bar's open menu) for floating panels in OS windows of their own: eframe runs the host's
|
||||
/// `raw_input_hook` for the main window only.
|
||||
pub native_input_hook: Option<NativeInputHook>,
|
||||
}
|
||||
|
||||
/// Rewrites one window's raw input before egui reads it ([`Services::native_input_hook`]).
|
||||
pub type NativeInputHook = std::sync::Arc<dyn Fn(&mut egui::RawInput) + Send + Sync>;
|
||||
|
||||
/// Cached canvas raster.
|
||||
pub struct CanvasCache {
|
||||
pub renderer: vectorcraft_render::Renderer,
|
||||
@@ -456,6 +464,8 @@ pub struct VectorcraftApp {
|
||||
pub services: Services,
|
||||
pub canvas: CanvasCache,
|
||||
pub perf: Perf,
|
||||
/// Docking state that lives for the session only.
|
||||
pub(crate) dock_rt: panel_docking::Runtime,
|
||||
/// macOS: the room the app bar keeps at its left for the window buttons over it, in points
|
||||
/// (0 where the OS draws no buttons over the app bar).
|
||||
pub titlebar_inset: f32,
|
||||
@@ -553,6 +563,15 @@ pub struct VectorcraftApp {
|
||||
pub recovery: recovery::Timer,
|
||||
/// Modifiers the keyboard holds (from the host's input), given back after synthetic input.
|
||||
host_modifiers: egui::Modifiers,
|
||||
/// Key, text and clipboard events a floating panel's own OS window forwarded, for the main
|
||||
/// window's next frame ([`Self::raw_input_hook`]), and the modifiers held in that window.
|
||||
pub(crate) forwarded: Vec<egui::Event>,
|
||||
pub(crate) forwarded_modifiers: Option<egui::Modifiers>,
|
||||
/// The last frame held the forwarded modifiers: this one goes back to the keyboard's.
|
||||
forwarded_modifiers_held: bool,
|
||||
/// This host iteration's input carries forwarded keys: they are the main window's although
|
||||
/// its OS window doesn't have the keyboard (see `shortcuts::handle`).
|
||||
pub(crate) forwarded_frame: bool,
|
||||
/// Synthetic input set the modifiers egui holds (see [`Self::raw_input_hook`]).
|
||||
synthetic_modifiers: bool,
|
||||
/// The marked text the system IME last sent (`None` once it commits or clears). When the Type
|
||||
@@ -762,6 +781,7 @@ impl VectorcraftApp {
|
||||
hover_cursor: None,
|
||||
},
|
||||
perf: Perf::default(),
|
||||
dock_rt: Default::default(),
|
||||
titlebar_inset: 0.0,
|
||||
last_effect: None,
|
||||
last_warp: None,
|
||||
@@ -807,6 +827,10 @@ impl VectorcraftApp {
|
||||
placing: Default::default(),
|
||||
recovery: Default::default(),
|
||||
host_modifiers: Default::default(),
|
||||
forwarded: Vec::new(),
|
||||
forwarded_modifiers: None,
|
||||
forwarded_modifiers_held: false,
|
||||
forwarded_frame: false,
|
||||
synthetic_modifiers: false,
|
||||
ime_marked: None,
|
||||
ime_context: Default::default(),
|
||||
@@ -1946,10 +1970,16 @@ impl VectorcraftApp {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn take_dropped_files(&mut self, ctx: &egui::Context) {
|
||||
let (dropped, shift) = ctx.input(|i| (i.raw.dropped_files.clone(), i.modifiers.shift));
|
||||
self.open_dropped_files(dropped, place::drag_pos(ctx), shift);
|
||||
}
|
||||
|
||||
/// Files dropped on the main window or on a floating panel's own OS window, opened or placed
|
||||
/// by kind; `pos` is the drop point in the main window, where the backend reports one.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn open_dropped_files(&mut self, dropped: Vec<egui::DroppedFileHandle>, pos: Option<egui::Pos2>, shift: bool) {
|
||||
if dropped.is_empty() {
|
||||
return;
|
||||
}
|
||||
let pos = place::drag_pos(ctx);
|
||||
let mut files = vec![];
|
||||
for f in dropped {
|
||||
let path = Some(f.path().to_string_lossy().to_string()).filter(|s| !s.is_empty());
|
||||
@@ -2126,6 +2156,19 @@ impl VectorcraftApp {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Keys a floating panel's own OS window left over run here, as if typed in this window,
|
||||
// under the modifiers held there for this frame only.
|
||||
if std::mem::take(&mut self.forwarded_modifiers_held) {
|
||||
raw.events.insert(0, egui::Event::ModifiersChanged(self.host_modifiers));
|
||||
}
|
||||
self.forwarded_frame = !self.forwarded.is_empty();
|
||||
if !self.forwarded.is_empty() {
|
||||
if let Some(m) = self.forwarded_modifiers.take() {
|
||||
raw.events.push(egui::Event::ModifiersChanged(m));
|
||||
self.forwarded_modifiers_held = true;
|
||||
}
|
||||
raw.events.append(&mut self.forwarded);
|
||||
}
|
||||
// The previous pass settled: the batch it took is done, so the next step may go.
|
||||
if self.injected_settled.swap(false, Ordering::Relaxed) {
|
||||
self.acknowledge_injected_batch();
|
||||
@@ -2185,6 +2228,23 @@ impl VectorcraftApp {
|
||||
raw.events.extend(batch);
|
||||
}
|
||||
|
||||
/// The canvas panel's body: the document tabs, the canvas and the isolation bar over it.
|
||||
pub(crate) fn canvas_area(&mut self, ui: &mut egui::Ui) {
|
||||
let t = theme::Tokens::get(ui.ctx());
|
||||
ui.painter().rect_filled(ui.max_rect(), 0.0, t.pasteboard);
|
||||
if self.ui.screen_mode < 3 {
|
||||
chrome::doc_tabs(self, ui);
|
||||
}
|
||||
canvas::show(self, ui);
|
||||
// Over the canvas inside the rulers, so the rulers and scroll bars stay uncovered.
|
||||
if self.ui.screen_mode < 3
|
||||
&& !menus::home_showing(self)
|
||||
&& let Some(area) = self.canvas_rect
|
||||
{
|
||||
chrome::isolation_bar(self, ui, area);
|
||||
}
|
||||
}
|
||||
|
||||
/// Lay out the whole window: the first UI pass of a host iteration sets the UI up (fonts,
|
||||
/// theme, image loaders) and takes the input; later passes (a `request_discard` rerun) draw
|
||||
/// only.
|
||||
@@ -2222,7 +2282,7 @@ impl VectorcraftApp {
|
||||
self.ime_caret_rect = None;
|
||||
scrub::begin_frame(self, &ctx);
|
||||
font_menu::end_stale_preview(self, &ctx);
|
||||
floating::track(self, &ctx);
|
||||
tools_float::track(self, &ctx);
|
||||
// A selection move released over a panel ends before any panel sees the release (#2826).
|
||||
canvas::release_over_panel(self, &ctx);
|
||||
let t = theme::Tokens::get(&ctx);
|
||||
@@ -2243,24 +2303,8 @@ impl VectorcraftApp {
|
||||
toolbar::show(self, ui);
|
||||
}
|
||||
panels::library_panel::tidy(self);
|
||||
if self.ui.dock && self.ui.screen_mode < 3 {
|
||||
dock::show(self, ui);
|
||||
}
|
||||
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.pasteboard)).show(ui, |ui| {
|
||||
if self.ui.screen_mode < 3 {
|
||||
chrome::doc_tabs(self, ui);
|
||||
}
|
||||
canvas::show(self, ui);
|
||||
// Over the canvas inside the rulers, so the rulers and scroll bars stay uncovered.
|
||||
if self.ui.screen_mode < 3
|
||||
&& !menus::home_showing(self)
|
||||
&& let Some(area) = self.canvas_rect
|
||||
{
|
||||
chrome::isolation_bar(self, ui, area);
|
||||
}
|
||||
});
|
||||
dock::floating_panel(self, &ctx);
|
||||
floating::show(self, &ctx);
|
||||
// The canvas and the panels: one docking layout. Tab hides the panels but not the canvas.
|
||||
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.pasteboard)).show(ui, |ui| panel_docking::show(self, ui));
|
||||
dialogs::show(self, &ctx);
|
||||
export_progress::show(self, &ctx);
|
||||
plugin_runs::show(self, &ctx);
|
||||
|
||||
@@ -12,7 +12,7 @@ use vectorcraft_engine::cmd::fileio::SaveMode;
|
||||
use crate::VectorcraftApp;
|
||||
use crate::io;
|
||||
use crate::panels::character::Face;
|
||||
use crate::state::{DockTab, next_zoom};
|
||||
use crate::state::next_zoom;
|
||||
use crate::theme::{Brightness, Tokens};
|
||||
use crate::widgets;
|
||||
|
||||
@@ -473,7 +473,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"window.panel.move",
|
||||
"Move Panel",
|
||||
"",
|
||||
"{panel, anchor, zone?: tab|center|left|right|top|bottom, before?: panel} group, split or reorder a panel",
|
||||
"{panel, anchor, zone?: tab|center|left|right|top|bottom|above|below, before?: panel, group?: bool, placement?} move a panel (with `group`, its whole group) into the group holding `anchor`: as a tab (before the `before` panel; null appends), a split beside it, or (`above` / `below`) a group of its own above or below it in its dock column or floating window; `placement` (a serialized Placement) replaces zone and before",
|
||||
),
|
||||
("window.panel.activate", "Activate Panel", "", "{panel} show and select a panel in its current group"),
|
||||
("window.panel.close", "Close Panel", "", "{panel} hide a panel"),
|
||||
@@ -481,25 +481,25 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"window.panel.collapse",
|
||||
"Collapse Panel Group",
|
||||
"",
|
||||
"{panel, collapsed?: bool} show the dock group holding a panel as its tabs only (or its panel again), as a double-click on its tab does; the panel becomes the group's shown one. Without `collapsed` it toggles. Floating groups collapse through window.panel.float {panel, collapsed}. Returns {panel, collapsed}",
|
||||
"{panel, collapsed?: bool} minimize the group holding a panel to its tabs (in a floating window, the window to its title bar), or restore it, as a double-click on a tab or the drag bar does; the same as window.panel.minimize {panel, minimized}. Without `collapsed` it toggles. Returns {panel, collapsed, layout}",
|
||||
),
|
||||
(
|
||||
"window.panel.float",
|
||||
"Float Panel",
|
||||
"",
|
||||
"{panel: id or \"tools\", x?, y?, onto?: id, below?: id, above?: id, group?: bool, collapsed?: bool} float a panel out of the dock (or out of its floating group) as its own floating group with its top-left corner at x, y in window points (default: cascaded), as dragging its tab out of the dock does; `onto` stacks it as a tab with the floating group holding that panel instead; `below` / `above` puts its group in a set with that group, right below or above it, as dropping it on that group's lower or upper half does (the set's groups stack top to bottom and move together); `group` takes its whole group (its floating group, or the Properties | Layers | Libraries tabs left in the dock); a panel alone in its group is moved (out of its set, given x, y); `collapsed` shows its group's tabs only, as a double-click on a tab does; a docked group floats its dock column. \"tools\" floats the Tools panel. Returns {panel, floating, group, set, pos, collapsed, column}",
|
||||
"{panel: id or \"tools\", x?, y?, width?, height?, group?: bool, column?: bool or n, onto?: id, above?: id, below?: id, collapsed?: bool} float a panel (with `group`, its whole group; with `column`, its whole dock column or floating window) as a floating window with its top-left corner at x, y in window points (default: inside the dock area, cascaded), as dragging its tab out does; a whole floating window keeps its rect unless given one. `onto` makes it a tab of the floating group holding that panel instead; `above` / `below` a group of its own above or below the group holding that panel, in its floating window or dock column. `collapsed` then minimizes (true) or restores (false) its window or group. A hidden panel shows first. \"tools\" floats the Tools panel at x, y. Returns {layout, closed, floating}",
|
||||
),
|
||||
(
|
||||
"window.panel.dock",
|
||||
"Dock Panel",
|
||||
"",
|
||||
"{panel: id or \"tools\", group?: bool, column?: true or n, anchor?: id, zone?: \"tab\" | \"top\" | \"bottom\" | \"left\" | \"right\" | \"center\", before?: id, icon?: bool} put a floating panel (with `group`, its whole group) back in the dock, as dropping it on the dock or its group's × does: Properties, Layers and Libraries in the tabbed group, the other panels in the icon column; with `anchor`, in the dock group holding that panel instead: `zone` \"tab\" (the default) as a tab of it (before the `before` panel's tab), as dropping it on that group's tabs does, \"top\" / \"bottom\" as a new group above / below it, as dropping it on that group's upper / lower half does (\"left\", \"right\" and \"center\" split or join it likewise); with `icon: true` (no `anchor`), as icons in the icon column, the tabbed group's panels too, as dropping it on the icon column does: it lives there from then on and pops out of its icon; with `column`, dock its group and the rest of its set as a dock column instead, as dropping it on the left edge of the icon column or of a dock column does: `true` at the dock's left, n at that place (1: next to the icon column, counting left); \"tools\" docks the Tools panel at the window's left edge. Returns {panel, floating, column}, or with `anchor` or `icon: true` (the shared layout) {panel, floating, layout} and no `column`",
|
||||
"{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. Returns {layout, closed, floating}",
|
||||
),
|
||||
(
|
||||
"window.collapseDock",
|
||||
"Collapse Panels to Icons",
|
||||
"",
|
||||
"{collapsed?: bool} collapse the dock's Properties | Layers | Libraries group to icons (true), expand it (false) or toggle (omitted), as the double arrow at the top of the dock does; returns the new state",
|
||||
"{collapsed?: bool} collapse every expanded dock column to icons (true), expand the outer dock column, Properties | Layers | Libraries (false), or toggle the outer column (omitted), as the double arrow at the top of the dock does; returns the new state",
|
||||
),
|
||||
("window.brightness", "UI Brightness", "", "{brightness: dark|mediumDark|mediumLight|light}"),
|
||||
(
|
||||
@@ -1287,7 +1287,6 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
}
|
||||
if let Some(r) = crate::shortcut_editor::run_command(app, id, p)
|
||||
.or_else(|| crate::workspaces::run_command(app, id, p))
|
||||
.or_else(|| crate::floating::command(app, id, p))
|
||||
.or_else(|| crate::edit_toolbar::command(app, id, p))
|
||||
{
|
||||
return Some(r);
|
||||
@@ -1636,49 +1635,6 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
}
|
||||
Ok(json!(on))
|
||||
}
|
||||
"window.collapseDock" => opt_bool(p, "collapsed").map(|collapsed| {
|
||||
let collapsed = collapsed.unwrap_or(!app.ui.dock_collapsed);
|
||||
crate::dock::set_collapsed(app, collapsed);
|
||||
json!(collapsed)
|
||||
}),
|
||||
"window.panel" => {
|
||||
let raw = s("panel").unwrap_or_default();
|
||||
// Canonical id (case-insensitive; display labels work too), then the
|
||||
// dock tab it names, if any.
|
||||
let canonical = normalize_panel(&raw);
|
||||
// A floating panel shows its tab in its group.
|
||||
if let Some(p) = canonical
|
||||
&& let Some(g) = crate::floating::group_of(&app.ui, p).and_then(|i| app.ui.floating_panels.get_mut(i))
|
||||
{
|
||||
g.active = g.panels.iter().position(|q| q == p).unwrap_or(g.active);
|
||||
g.collapsed = false;
|
||||
let out = json!({ "floating": g.panels });
|
||||
app.ui.dock = true;
|
||||
return Some(Ok(out));
|
||||
}
|
||||
let tab = canonical.and_then(DockTab::from_id);
|
||||
match (canonical, tab) {
|
||||
// A collapsed dock pops the panel out of its icon, like the icon panels.
|
||||
(Some(p), Some(tab)) if app.ui.dock_collapsed => {
|
||||
app.ui.dock_tab = tab;
|
||||
app.ui.open_panel = if app.ui.open_panel.as_deref() == Some(p) { None } else { Some(p.to_string()) };
|
||||
app.ui.dock = true;
|
||||
Ok(json!({"open": app.ui.open_panel}))
|
||||
}
|
||||
(Some(_), Some(tab)) => {
|
||||
app.ui.dock_tab = tab;
|
||||
Ok(Value::Null)
|
||||
}
|
||||
// A library panel isn't in the icon column: it floats.
|
||||
(Some(p), None) if crate::panels::library_panel::parse(p).is_some() => crate::panels::library_panel::float(app, p),
|
||||
(Some(p), None) => {
|
||||
app.ui.open_panel = if app.ui.open_panel.as_deref() == Some(p) { None } else { Some(p.to_string()) };
|
||||
app.ui.dock = true;
|
||||
Ok(json!({"open": app.ui.open_panel}))
|
||||
}
|
||||
(None, _) => Err(format!("unknown panel `{raw}`")),
|
||||
}
|
||||
}
|
||||
"window.brightness" => match s("brightness").as_deref().and_then(Brightness::parse) {
|
||||
Some(b) => {
|
||||
app.ui.brightness = b;
|
||||
@@ -2040,17 +1996,9 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> {
|
||||
"window.taskBar.pin" => app.ui.task_bar_place.pinned,
|
||||
"window.panel" => {
|
||||
let panel = p.get("panel").and_then(Value::as_str).unwrap_or("");
|
||||
let canonical = normalize_panel(panel);
|
||||
match canonical.and_then(DockTab::from_id) {
|
||||
_ if app.ui.docking.is_some() => canonical.is_some_and(|id| {
|
||||
app.ui.open_panel.as_deref() == Some(id) || app.ui.docking.as_ref().is_some_and(|layout| layout.contains(&id.to_string()))
|
||||
}),
|
||||
_ if canonical.is_some_and(|id| crate::floating::group_of(&app.ui, id).is_some()) => true,
|
||||
Some(tab) if !app.ui.dock_collapsed => crate::floating::shown_tab(&app.ui) == Some(tab),
|
||||
_ => canonical.is_some_and(|id| app.ui.open_panel.as_deref() == Some(id)),
|
||||
}
|
||||
normalize_panel(panel).is_some_and(|id| crate::panel_docking::showing(&app.ui, id))
|
||||
}
|
||||
"window.collapseDock" => app.ui.dock_collapsed,
|
||||
"window.collapseDock" => crate::panel_docking::dock_collapsed(&app.ui),
|
||||
"window.workspace" => p.get("name").and_then(Value::as_str) == Some(app.ui.workspace.as_str()),
|
||||
"window.brightness" => p.get("brightness").and_then(Value::as_str).and_then(Brightness::parse) == Some(app.ui.brightness),
|
||||
"view.slices.lock" => app.session.slices_locked(),
|
||||
|
||||
+1137
-1378
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,319 @@
|
||||
//! Floating panels in OS windows of their own: the whole app's frames, with a stand-in for the
|
||||
//! window system that runs each native window's frame with its own input.
|
||||
// Clippy's test exemption doesn't cover a `cfg(all(test, …))` module's helpers.
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use egui::{Event, Key, Pos2, Rect, ViewportCommand, ViewportId, pos2, vec2};
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use super::*;
|
||||
|
||||
const SCREEN: Rect = Rect::from_min_max(Pos2::ZERO, pos2(1600.0, 1000.0));
|
||||
/// Where the main window's content sits on the desktop.
|
||||
const ORIGIN: Pos2 = pos2(100.0, 50.0);
|
||||
|
||||
struct Window {
|
||||
/// Desktop rect of its content.
|
||||
rect: Rect,
|
||||
events: Vec<Event>,
|
||||
dropped: Vec<egui::DroppedFileHandle>,
|
||||
frames: usize,
|
||||
}
|
||||
|
||||
struct Host {
|
||||
windows: HashMap<ViewportId, Window>,
|
||||
/// The window with the keyboard.
|
||||
focus: ViewportId,
|
||||
time: f64,
|
||||
/// Every command sent to a window, in order.
|
||||
commands: Vec<(ViewportId, ViewportCommand)>,
|
||||
}
|
||||
|
||||
impl Host {
|
||||
fn infos(&self) -> egui::ViewportIdMap<egui::ViewportInfo> {
|
||||
let info = |id: ViewportId, rect: Rect| egui::ViewportInfo {
|
||||
inner_rect: Some(rect),
|
||||
outer_rect: Some(rect),
|
||||
focused: Some(self.focus == id),
|
||||
native_pixels_per_point: Some(1.0),
|
||||
..Default::default()
|
||||
};
|
||||
std::iter::once((ViewportId::ROOT, info(ViewportId::ROOT, SCREEN.translate(ORIGIN.to_vec2()))))
|
||||
.chain(self.windows.iter().map(|(id, w)| (*id, info(*id, w.rect))))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn apply(&mut self, full: &egui::FullOutput) {
|
||||
for (id, output) in &full.viewport_output {
|
||||
for command in &output.commands {
|
||||
self.commands.push((*id, command.clone()));
|
||||
let Some(window) = self.windows.get_mut(id) else { continue };
|
||||
match command {
|
||||
ViewportCommand::OuterPosition(pos) => window.rect = Rect::from_min_size(*pos, window.rect.size()),
|
||||
ViewportCommand::InnerSize(size) => window.rect = Rect::from_min_size(window.rect.min, *size),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One native window's frame.
|
||||
fn render(host: &RefCell<Self>, ctx: &egui::Context, mut viewport: egui::ImmediateViewport<'_>) {
|
||||
let id = viewport.ids.this;
|
||||
let input = {
|
||||
let mut host = host.borrow_mut();
|
||||
let builder = &viewport.builder;
|
||||
let rect = Rect::from_min_size(builder.position.unwrap_or_default(), builder.inner_size.unwrap_or(vec2(300.0, 200.0)));
|
||||
host.windows.entry(id).or_insert_with(|| Window { rect, events: Vec::new(), dropped: Vec::new(), frames: 0 });
|
||||
let viewports = host.infos();
|
||||
let (time, focused) = (host.time, host.focus == id);
|
||||
let Some(window) = host.windows.get_mut(&id) else { return };
|
||||
window.frames += 1;
|
||||
egui::RawInput {
|
||||
viewport_id: id,
|
||||
viewports,
|
||||
screen_rect: Some(Rect::from_min_size(Pos2::ZERO, window.rect.size())),
|
||||
events: std::mem::take(&mut window.events),
|
||||
dropped_files: std::mem::take(&mut window.dropped),
|
||||
focused,
|
||||
time: Some(time),
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
let mut full = ctx.run_ui(input, |ui| (viewport.viewport_ui_cb)(ui));
|
||||
full.textures_delta.clear();
|
||||
host.borrow_mut().apply(&full);
|
||||
}
|
||||
}
|
||||
|
||||
struct Harness {
|
||||
app: VectorcraftApp,
|
||||
ctx: egui::Context,
|
||||
host: Rc<RefCell<Host>>,
|
||||
}
|
||||
|
||||
impl Harness {
|
||||
fn new() -> Self {
|
||||
let host = Rc::new(RefCell::new(Host { windows: HashMap::new(), focus: ViewportId::ROOT, time: 0.0, commands: Vec::new() }));
|
||||
let shared = host.clone();
|
||||
egui::Context::set_immediate_viewport_renderer(move |ctx, viewport| Host::render(&shared, ctx, viewport));
|
||||
let ctx = egui::Context::default();
|
||||
ctx.set_embed_viewports(false);
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
let mut h = Self { app, ctx, host };
|
||||
h.frames(3);
|
||||
h
|
||||
}
|
||||
|
||||
fn frame(&mut self, events: Vec<Event>) {
|
||||
let (viewports, focused, time) = {
|
||||
let mut host = self.host.borrow_mut();
|
||||
host.time += 0.05;
|
||||
(host.infos(), host.focus == ViewportId::ROOT, host.time)
|
||||
};
|
||||
let mut input = egui::RawInput { time: Some(time), events, screen_rect: Some(SCREEN), viewports, focused, ..Default::default() };
|
||||
self.app.raw_input_hook(&self.ctx, &mut input);
|
||||
let app = &mut self.app;
|
||||
let mut full = self.ctx.run_ui(input, |ui| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
});
|
||||
full.textures_delta.clear();
|
||||
self.host.borrow_mut().apply(&full);
|
||||
}
|
||||
|
||||
fn frames(&mut self, n: usize) {
|
||||
for _ in 0..n {
|
||||
self.frame(vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
/// The viewport of the floating window holding `panel`.
|
||||
fn viewport(&self, panel: &str) -> ViewportId {
|
||||
let layout = layout_of(&self.app.ui);
|
||||
let window = floating_window(&layout, panel).expect("the panel floats");
|
||||
craft_ui::docking::DockArea::<String>::window_viewport(area_id(), window.id)
|
||||
}
|
||||
|
||||
/// Float `panel` into a native window and give that window the keyboard.
|
||||
fn float(&mut self, panel: &str) -> ViewportId {
|
||||
self.app.run("window.panel.float", json!({"panel": panel, "x": 200, "y": 200})).unwrap();
|
||||
self.frames(3);
|
||||
let viewport = self.viewport(panel);
|
||||
assert!(self.host.borrow().windows.get(&viewport).is_some_and(|w| w.frames > 0), "{panel} opened an OS window");
|
||||
self.host.borrow_mut().focus = viewport;
|
||||
viewport
|
||||
}
|
||||
|
||||
/// Press and release `key` in the window `viewport`, with the text a letter types, then let
|
||||
/// the main window run.
|
||||
fn key_in(&mut self, viewport: ViewportId, key: Key) {
|
||||
let letter = key.name().len() == 1 && key.name().chars().all(|c| c.is_ascii_alphabetic());
|
||||
for pressed in [true, false] {
|
||||
let mut events = vec![Event::Key { key, physical_key: None, pressed, repeat: false, modifiers: Default::default() }];
|
||||
if pressed && letter {
|
||||
events.push(Event::Text(key.name().to_lowercase()));
|
||||
}
|
||||
if let Some(window) = self.host.borrow_mut().windows.get_mut(&viewport) {
|
||||
window.events.extend(events);
|
||||
}
|
||||
self.frame(vec![]);
|
||||
}
|
||||
self.frames(2);
|
||||
}
|
||||
|
||||
fn objects(&self) -> usize {
|
||||
self.app.session.active().map_or(0, |d| d.doc.selectable_art().len())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MockDrop(Vec<u8>);
|
||||
|
||||
impl egui::DroppedFile for MockDrop {
|
||||
fn path(&self) -> &std::path::Path {
|
||||
std::path::Path::new("")
|
||||
}
|
||||
fn bytes(&self) -> Result<Vec<u8>, String> {
|
||||
Ok(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_floated_panel_opens_its_own_os_window_where_the_backend_has_viewports() {
|
||||
let mut h = Harness::new();
|
||||
let viewport = h.float("swatches");
|
||||
let rect = h.host.borrow().windows.get(&viewport).map(|w| w.rect).unwrap();
|
||||
assert!(rect.min.x >= ORIGIN.x && rect.min.y >= ORIGIN.y, "opens over its place in the app window: {rect:?}");
|
||||
// The window keeps its id, and with it its OS window.
|
||||
let before = layout_of(&h.app.ui).floating[0].id;
|
||||
h.frames(3);
|
||||
assert_eq!(layout_of(&h.app.ui).floating[0].id, before);
|
||||
assert_eq!(h.host.borrow().windows.len(), 1);
|
||||
|
||||
// Embedded viewports (the web) draw it in the app window.
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("window.panel.float", json!({"panel": "swatches", "x": 200, "y": 200})).unwrap();
|
||||
for _ in 0..3 {
|
||||
let input = egui::RawInput { screen_rect: Some(SCREEN), ..Default::default() };
|
||||
let mut full = ctx.run_ui(input, |ui| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
});
|
||||
full.textures_delta.clear();
|
||||
}
|
||||
let tab = craft_ui::docking::DockArea::<String>::hit_rects(&ctx, area_id())
|
||||
.into_iter()
|
||||
.any(|(hit, _)| hit == craft_ui::docking::Hit::Tab("swatches".into()));
|
||||
assert!(tab, "its tab is drawn in the app window");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keys_typed_in_a_native_window_run_the_main_windows_shortcuts() {
|
||||
let mut h = Harness::new();
|
||||
let viewport = h.float("swatches");
|
||||
h.key_in(viewport, Key::P);
|
||||
assert_eq!(h.app.session.tool_id(), "pen", "a tool key picks the tool");
|
||||
h.key_in(viewport, Key::V);
|
||||
assert_eq!(h.app.session.tool_id(), "selection");
|
||||
|
||||
h.app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap();
|
||||
h.app.run("shape.rectangle", json!({"x": 80, "y": 10, "width": 50, "height": 40})).unwrap();
|
||||
h.app.run("select.all", json!({})).unwrap();
|
||||
let group = h.app.run("object.group", json!({})).unwrap()["id"].as_u64().unwrap();
|
||||
h.app.run("object.isolate", json!({"id": group})).unwrap();
|
||||
h.frames(2);
|
||||
h.key_in(viewport, Key::Escape);
|
||||
assert!(h.app.session.active().unwrap().isolation.is_none(), "Escape leaves isolation mode");
|
||||
|
||||
h.app.run("select.all", json!({})).unwrap();
|
||||
let before = h.objects();
|
||||
h.key_in(viewport, Key::Delete);
|
||||
assert!(h.objects() < before, "Delete deletes the selection");
|
||||
assert!(h.app.session.active().unwrap().selection.objects.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_modal_blocks_native_windows_and_takes_the_keyboard_from_them() {
|
||||
let mut h = Harness::new();
|
||||
let viewport = h.float("swatches");
|
||||
h.app.ui.about = true;
|
||||
h.frames(6);
|
||||
let focus_commands =
|
||||
|h: &Harness| h.host.borrow().commands.iter().filter(|(id, c)| *id == ViewportId::ROOT && *c == ViewportCommand::Focus).count();
|
||||
assert_eq!(focus_commands(&h), 1, "the main window gets the keyboard");
|
||||
h.key_in(viewport, Key::P);
|
||||
assert_eq!(h.app.session.tool_id(), "selection", "a key typed in the blocked window does nothing");
|
||||
assert_eq!(focus_commands(&h), 1, "once");
|
||||
|
||||
h.app.ui.about = false;
|
||||
h.frames(3);
|
||||
h.key_in(viewport, Key::P);
|
||||
assert_eq!(h.app.session.tool_id(), "pen", "the window takes keys again once the modal closes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_dropped_on_a_native_window_opens() {
|
||||
let mut h = Harness::new();
|
||||
let viewport = h.float("layers");
|
||||
let documents = h.app.session.documents().len();
|
||||
let svg = br#"<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect width="10" height="10"/></svg>"#;
|
||||
if let Some(window) = h.host.borrow_mut().windows.get_mut(&viewport) {
|
||||
window.dropped.push(Arc::new(MockDrop(svg.to_vec())));
|
||||
}
|
||||
h.frames(4);
|
||||
assert_eq!(h.app.session.documents().len(), documents + 1, "the SVG opens as a document");
|
||||
}
|
||||
|
||||
fn button(pos: Pos2, pressed: bool) -> Event {
|
||||
Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }
|
||||
}
|
||||
|
||||
/// Art dragged off the canvas onto a panel in an OS window of its own, which lies over the
|
||||
/// canvas: that panel takes it, and the art stays where it was.
|
||||
#[test]
|
||||
fn art_dragged_from_the_canvas_onto_a_native_panel_window_drops_there() {
|
||||
let mut h = Harness::new();
|
||||
let id = h.app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap()["id"].as_u64().unwrap();
|
||||
let id = vectorcraft_doc::NodeId(id);
|
||||
let viewport = h.float("graphicStyles");
|
||||
h.host.borrow_mut().focus = ViewportId::ROOT;
|
||||
h.frames(2);
|
||||
let styles = h.app.session.active().unwrap().doc.graphic_styles.len();
|
||||
let art = |h: &Harness| format!("{:?}", h.app.session.active().unwrap().doc.node(id).map(|n| &n.kind));
|
||||
let before = art(&h);
|
||||
let xf = crate::canvas::Xf::new(h.app.canvas_rect.unwrap(), h.app.view().unwrap());
|
||||
let from = xf.to_screen(vectorcraft_geom::Point::new(35.0, 30.0));
|
||||
let window = h.host.borrow().windows.get(&viewport).map(|w| w.rect).unwrap();
|
||||
let to = window.min + vec2(60.0, 120.0) - ORIGIN.to_vec2();
|
||||
assert!(h.app.canvas_rect.unwrap().contains(to), "the window lies over the canvas");
|
||||
h.frame(vec![Event::PointerMoved(from), button(from, true)]);
|
||||
for k in 1..=8 {
|
||||
h.frame(vec![Event::PointerMoved(from + (to - from) * (k as f32 / 8.0))]);
|
||||
}
|
||||
h.frame(vec![Event::PointerMoved(to), button(to, false)]);
|
||||
h.frames(4);
|
||||
let st = h.app.session.active().unwrap();
|
||||
assert_eq!(st.doc.graphic_styles.len(), styles + 1, "the art became a graphic style");
|
||||
assert!(st.interaction.is_none(), "no move is left open");
|
||||
assert_eq!(art(&h), before, "the art stays where it was");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_docking_leaves_out_the_controls_of_os_windows() {
|
||||
let mut h = Harness::new();
|
||||
h.float("swatches");
|
||||
let report = inspect(&h.app, &h.ctx);
|
||||
let hits = report["hitRects"].as_array().unwrap();
|
||||
assert!(hits.iter().any(|hit| hit["hit"] == json!({"Tab": "layers"})), "the main window's tabs are there");
|
||||
let swatches = |hit: &Value| hit["hit"].as_object().is_some_and(|o| o.values().any(|v| v == "swatches"));
|
||||
assert!(!hits.iter().any(swatches), "none of the OS window's: {hits:?}");
|
||||
assert!(report["layout"]["floating"][0]["native"].is_array(), "its place is the window's native rect");
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -610,7 +610,7 @@ mod tests {
|
||||
app.run("imageTrace.release", json!({})).unwrap();
|
||||
app.ui.image_trace_advanced = false;
|
||||
trace_and_open(&mut app, "Sketched Art");
|
||||
assert_eq!(app.ui.open_panel.as_deref(), Some("imageTrace"));
|
||||
assert!(crate::panel_docking::showing(&app.ui, "imageTrace"));
|
||||
assert!(app.ui.image_trace_advanced);
|
||||
assert_eq!(selected_trace(&app).unwrap().0, "Sketched Art");
|
||||
let ctx = egui::Context::default();
|
||||
@@ -625,7 +625,7 @@ mod tests {
|
||||
// Opening controls again neither closes the flyout nor adds a document undo step.
|
||||
let steps = app.session.active().unwrap().history.undo.len();
|
||||
open_settings(&mut app);
|
||||
assert_eq!(app.ui.open_panel.as_deref(), Some("imageTrace"));
|
||||
assert!(crate::panel_docking::showing(&app.ui, "imageTrace"));
|
||||
assert_eq!(app.session.active().unwrap().history.undo.len(), steps);
|
||||
// Optional local visual QA; no reference images or generated captures are committed.
|
||||
if let Some(path) = std::env::var_os("VECTORCRAFT_TRACE_CAPTURE") {
|
||||
@@ -668,7 +668,7 @@ mod tests {
|
||||
trace_and_open(&mut app, "Sketched Art");
|
||||
let after = app.ui.docking.as_ref().unwrap();
|
||||
assert_eq!(after.floating.len(), before.floating.len());
|
||||
assert!(after.floating.iter().any(|g| g.panels == ["imageTrace"]));
|
||||
assert!(after.floating.iter().any(|g| crate::panel_docking::window_panels(g) == ["imageTrace"]));
|
||||
assert!(app.ui.open_panel.is_none(), "the existing floating group owns the panel");
|
||||
}
|
||||
|
||||
@@ -932,7 +932,7 @@ mod tests {
|
||||
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| super::super::properties::show(&mut app, ui));
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
assert_eq!(app.ui.open_panel.as_deref(), Some("imageTrace"));
|
||||
assert!(crate::panel_docking::showing(&app.ui, "imageTrace"));
|
||||
}
|
||||
|
||||
/// #1358: in Properties the View row fits the panel, so the panel doesn't widen and clip its
|
||||
|
||||
@@ -2866,7 +2866,7 @@ mod tests {
|
||||
let mut frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
||||
time += 0.05;
|
||||
let input = egui::RawInput { time: Some(time), events, screen_rect: Some(screen), ..Default::default() };
|
||||
let mut out = ctx.run_ui(input, |ui| crate::dock::floating_panel(app, ui.ctx()));
|
||||
let mut out = ctx.run_ui(input, |ui| crate::panel_docking::show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
let mut c: Vec<egui::Pos2> = out
|
||||
.shapes
|
||||
|
||||
@@ -208,14 +208,14 @@ fn registered(panel: &str) -> Result<&'static str, String> {
|
||||
|
||||
/// Is panel `panel` open (in the dock or floating)?
|
||||
pub(crate) fn is_open(ui: &UiState, panel: &str) -> bool {
|
||||
ui.docking.as_ref().is_some_and(|l| l.panels().iter().any(|p| p.as_str() == panel)) || crate::floating::group_of(ui, panel).is_some()
|
||||
crate::panel_docking::layout_of(ui).contains(&panel.to_string())
|
||||
}
|
||||
|
||||
/// The open library panels.
|
||||
fn open_panels(ui: &UiState) -> Vec<String> {
|
||||
let mut out: Vec<String> = vec![];
|
||||
let docked = ui.docking.iter().flat_map(|l| l.panels());
|
||||
for p in docked.chain(ui.floating_panels.iter().flat_map(|g| &g.panels)) {
|
||||
for p in docked {
|
||||
if p.starts_with(PREFIX) && !out.contains(p) {
|
||||
out.push(p.clone());
|
||||
}
|
||||
@@ -367,15 +367,21 @@ fn rename(ui: &mut UiState, from: &str, to: &str) -> Result<(), String> {
|
||||
serde_json::from_value(v).map_err(|e| e.to_string())
|
||||
}
|
||||
registered(to)?;
|
||||
let docking = ui.docking.as_ref().map(|l| renamed(l, from, to)).transpose()?;
|
||||
let mut docking = ui.docking.as_ref().map(|l| renamed(l, from, to)).transpose()?;
|
||||
// A floating window keeps its id, and with it its OS window.
|
||||
if let (Some(new), Some(old)) = (docking.as_mut(), ui.docking.as_ref()) {
|
||||
for (window, was) in new.floating.iter_mut().zip(&old.floating) {
|
||||
window.id = was.id;
|
||||
}
|
||||
}
|
||||
let mut hidden = std::collections::BTreeMap::new();
|
||||
for (p, at) in &ui.docking_hidden {
|
||||
hidden.insert(if p == from { to.to_string() } else { p.clone() }, renamed(at, from, to)?);
|
||||
}
|
||||
ui.docking = docking;
|
||||
ui.docking_hidden = hidden;
|
||||
for p in ui.floating_panels.iter_mut().flat_map(|g| g.panels.iter_mut()).chain(ui.open_panel.as_mut()).filter(|p| p.as_str() == from) {
|
||||
*p = to.into();
|
||||
if ui.open_panel.as_deref() == Some(from) {
|
||||
ui.open_panel = Some(to.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1132,7 +1138,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn groups(app: &VectorcraftApp) -> Vec<Vec<String>> {
|
||||
docking(app).floating.iter().map(|g| g.panels.clone()).collect()
|
||||
docking(app).floating.iter().map(crate::panel_docking::window_panels).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1154,7 +1160,7 @@ mod tests {
|
||||
assert_eq!(groups(&app).len(), 3);
|
||||
assert_eq!(last_open(&app.ui).map(LibraryPanel::panel), Some(pastels.clone()));
|
||||
// They group, dock and close as any panel, and `window.panel` shows one again.
|
||||
app.run("window.panel.float", json!({"panel": brights, "onto": pastels})).unwrap();
|
||||
app.run("window.panel.move", json!({"panel": brights, "anchor": pastels})).unwrap();
|
||||
assert!(groups(&app).contains(&vec![pastels.clone(), brights.clone()]), "{:?}", groups(&app));
|
||||
app.run("window.panel.dock", json!({"panel": styles})).unwrap();
|
||||
assert!(craft_ui::docking::Layout { root: docking(&app).root.clone(), floating: vec![] }.contains(&styles));
|
||||
@@ -1242,7 +1248,7 @@ mod tests {
|
||||
let next = restarted(&app);
|
||||
assert!(is_open(&next.ui, &pastels) && is_open(&next.ui, &brand), "{}", next.ui.status);
|
||||
assert!(!is_open(&next.ui, &brights), "a panel that isn't persistent closed with the app");
|
||||
let group = docking(&next).floating.iter().find(|g| g.panels.contains(&pastels)).unwrap();
|
||||
let group = docking(&next).floating.iter().find(|g| g.contains(&pastels)).unwrap();
|
||||
assert_eq!(group.rect[..2], [40.0, 90.0], "where it was");
|
||||
let s = settings(&next.ui, "swatches", "pastels").unwrap();
|
||||
assert_eq!((s.view, s.hide_find, s.persistent), (Some(View::LargeList), true, true));
|
||||
@@ -1427,8 +1433,8 @@ mod tests {
|
||||
step::<SwatchLibraries>(&mut app, "pastels", 1);
|
||||
let next = panel_id("swatches", SWATCH_IDS[5]);
|
||||
assert!(!is_open(&app.ui, &first) && is_open(&app.ui, &next), "{}", app.ui.status);
|
||||
let group = docking(&app).floating.iter().find(|g| g.panels.contains(&next)).unwrap();
|
||||
assert_eq!((group.panels.len(), group.rect[0], group.rect[1]), (1, 50.0, 60.0), "in its place");
|
||||
let group = docking(&app).floating.iter().find(|g| g.contains(&next)).unwrap();
|
||||
assert_eq!((group.panels().count(), group.rect[0], group.rect[1]), (1, 50.0, 60.0), "in its place");
|
||||
let s = settings(&app.ui, "swatches", SWATCH_IDS[5]).unwrap();
|
||||
assert_eq!((s.view, s.persistent), (Some(View::SmallList), true), "the panel keeps its settings");
|
||||
assert!(settings(&app.ui, "swatches", "pastels").is_none());
|
||||
|
||||
@@ -249,7 +249,7 @@ pub fn panel_menu(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, rect: Rect) {
|
||||
ui.separator();
|
||||
}
|
||||
// The panel floats out of the dock or goes back in, as dragging its tab does.
|
||||
let (label, cmd) = if crate::floating::group_of(&app.ui, id).is_some() {
|
||||
let (label, cmd) = if crate::panel_docking::floats(&app.ui, id) {
|
||||
(tl!("Dock Panel"), "window.panel.dock")
|
||||
} else {
|
||||
(tl!("Float Panel"), "window.panel.float")
|
||||
|
||||
@@ -80,15 +80,10 @@ fn above_text(app: &mut VectorcraftApp, ctx: &egui::Context, n: &Node, span: f64
|
||||
return None;
|
||||
}
|
||||
let width = span as f32 * per + RULER_LEFT * 2.0;
|
||||
let height = ctx.memory(|m| m.area_rect(crate::floating::area_id("tabs"))).map_or(240.0, |r| r.height());
|
||||
let height = crate::panel_docking::layout_of(&app.ui).floating.iter().find(|w| w.contains(&"tabs".to_string())).map_or(240.0, |w| w.rect[3]);
|
||||
let pos = egui::pos2(on_screen.left() - RULER_LEFT, on_screen.top() - height - 6.0);
|
||||
if crate::floating::group_of(&app.ui, "tabs").is_none() {
|
||||
crate::floating::float(&mut app.ui, &["tabs"], "tabs", pos);
|
||||
}
|
||||
let gi = crate::floating::group_of(&app.ui, "tabs")?;
|
||||
let g = app.ui.floating_panels.get_mut(gi)?;
|
||||
g.pos = [pos.x, pos.y];
|
||||
g.width = Some(width);
|
||||
let _ = ctx;
|
||||
crate::panel_docking::place_floating(app, "tabs", pos, width).ok()?;
|
||||
Some(())
|
||||
}
|
||||
|
||||
@@ -952,16 +947,17 @@ mod tests {
|
||||
let (node, _, span) = current(&app).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
above_text(&mut app, &ctx, &node, span).unwrap();
|
||||
let g = &app.ui.floating_panels[crate::floating::group_of(&app.ui, "tabs").unwrap()];
|
||||
let layout = app.ui.docking.clone().unwrap();
|
||||
let g = layout.floating.iter().find(|w| w.contains(&"tabs".to_string())).unwrap();
|
||||
let xf = crate::canvas::Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
|
||||
let frame = xf.rect_to_screen(node.geometric_bounds().unwrap());
|
||||
assert!((g.pos[0] + RULER_LEFT - frame.left()).abs() < 0.01, "0 on the frame's left edge");
|
||||
assert!(g.pos[1] < frame.top(), "above the text");
|
||||
assert!((g.rect[0] + RULER_LEFT - frame.left()).abs() < 0.01, "0 on the frame's left edge");
|
||||
assert!(g.rect[1] < frame.top(), "above the text");
|
||||
// The ruler (the panel less its margins) spans the frame at this zoom.
|
||||
let ruler = g.width.unwrap() - RULER_LEFT * 2.0;
|
||||
let ruler = g.rect[2] - RULER_LEFT * 2.0;
|
||||
assert!((ruler - frame.width()).abs() < 0.01, "{ruler} {}", frame.width());
|
||||
assert!((ruler - 300.0 * zoom as f32).abs() < 0.01);
|
||||
}
|
||||
assert_eq!(app.ui.floating_panels.len(), 1, "placed again, not floated twice");
|
||||
assert_eq!(app.ui.docking.unwrap().floating.len(), 1, "placed again, not floated twice");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ fn window_blend_opens_the_panel() {
|
||||
assert_eq!(item.command.as_deref(), Some("window.panel"));
|
||||
assert_eq!(item.params, json!({"panel": ID, "toggle": true}));
|
||||
crate::menus::invoke(&mut app, "window.panel", item.params.clone());
|
||||
assert_eq!(app.ui.open_panel.as_deref(), Some(ID));
|
||||
assert!(crate::panel_docking::showing(&app.ui, ID));
|
||||
let text = crate::tests_labels::painted_text(&mut app, |app, ui| super::show_icon_panel(app, ui, ID));
|
||||
for label in ["Make", "Spacing:", "Orientation:", "Direction:", "Reverse Front to Back", "Reverse Spine", "Replace Spine", "Expand", "Release"] {
|
||||
assert!(text.contains(label), "{label} in {text}");
|
||||
|
||||
@@ -508,7 +508,7 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
if !crate::take_frame_once(&mut app.shortcuts_frames, frame) {
|
||||
return;
|
||||
}
|
||||
if !ctx.input(|input| input.focused) {
|
||||
if !ctx.input(|input| input.focused) && !(app.forwarded_frame && ctx.viewport_id() == egui::ViewportId::ROOT) {
|
||||
crate::ime::controls_without_editor(app, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
+21
-73
@@ -363,30 +363,29 @@ pub struct UiState {
|
||||
/// Edit Toolbar's All Tools drawer is open beside the toolbar (`window.editToolbar`).
|
||||
#[serde(skip)]
|
||||
pub tools_drawer: bool,
|
||||
/// Panels dragged out of the dock: each group floats as its own stack of tabs.
|
||||
#[serde(default)]
|
||||
/// Floating panel groups as files from before the canvas joined [`Self::docking`] saved
|
||||
/// them. Read only to convert those files ([`crate::layout_migration`]), never written.
|
||||
#[serde(default, skip_serializing)]
|
||||
pub floating_panels: Vec<FloatingPanels>,
|
||||
/// Shared panel membership and placement; older preferences use the legacy dock.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub docking: Option<craft_ui::docking::Layout<String>>,
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
|
||||
pub docking_hidden: std::collections::BTreeMap<String, craft_ui::docking::Location<String>>,
|
||||
/// Panels whose saved return destination is the iconic rail.
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
|
||||
/// Panels dropped on the icon column, in older files (read only, like [`Self::floating_panels`]).
|
||||
#[serde(default, skip_serializing)]
|
||||
pub docking_icons: std::collections::BTreeSet<String>,
|
||||
/// The panels shown by dock groups collapsed to their tabs (a double-click on a tab); a group
|
||||
/// showing another of its panels is expanded.
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
|
||||
/// The panels shown by dock groups collapsed to their tabs, in older files (read only, like
|
||||
/// [`Self::floating_panels`]); a group showing another of its panels was expanded.
|
||||
#[serde(default, skip_serializing)]
|
||||
pub docking_collapsed: std::collections::BTreeSet<String>,
|
||||
/// Invalidates in-flight gestures when a workspace is replaced.
|
||||
#[serde(skip)]
|
||||
pub docking_generation: u64,
|
||||
/// The main dock column's outer width in screen points (legacy or shared docking).
|
||||
/// The main dock column's width in screen points while the workspace has no layout of its
|
||||
/// own (`docking` is none); a customized layout keeps its widths in it.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub dock_width: Option<f32>,
|
||||
/// Next frame should seed egui [`PanelState`] from [`Self::dock_width`] (workspace switch).
|
||||
#[serde(skip)]
|
||||
pub dock_width_pending: bool,
|
||||
/// The Tools panel floats with its top-left corner here (dragged out by its title bar); `None`:
|
||||
/// docked at the window's left edge.
|
||||
#[serde(default)]
|
||||
@@ -698,12 +697,10 @@ pub struct TabHid {
|
||||
pub control_bar: bool,
|
||||
}
|
||||
|
||||
/// A group of panels dragged out of the dock: it floats as a stack of tabs, one panel shown.
|
||||
/// A group of panels dragged out of the dock, as files from before the canvas joined the panel
|
||||
/// layout saved it: it floated as a stack of tabs, one panel shown.
|
||||
///
|
||||
/// Groups sharing a `column` float as one set, stacked top to bottom in list order, like the panel
|
||||
/// groups of a dock in Illustrator's workspace
|
||||
/// (<https://helpx.adobe.com/illustrator/using/workspace-basics.html>): the set moves by the top
|
||||
/// group's title bar, and a group dragged by its tab strip leaves it.
|
||||
/// Groups sharing a `column` floated as one set, stacked top to bottom in list order.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FloatingPanels {
|
||||
/// Panel ids (`window.panel`), in tab order.
|
||||
@@ -729,54 +726,10 @@ pub struct FloatingPanels {
|
||||
pub docked: Option<u32>,
|
||||
}
|
||||
|
||||
impl FloatingPanels {
|
||||
/// Keep known panels, each in one group, drop the groups left empty and the toolbar position
|
||||
/// that isn't a number (a hand-edited preferences file or workspace).
|
||||
pub fn sanitize(groups: &mut Vec<FloatingPanels>, toolbar_pos: &mut Option<[f32; 2]>) {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
groups.retain_mut(|g| {
|
||||
g.panels.retain(|id| crate::menus::normalize_panel(id) == Some(id.as_str()) && seen.insert(id.clone()));
|
||||
g.active = g.active.min(g.panels.len().saturating_sub(1));
|
||||
!g.panels.is_empty()
|
||||
});
|
||||
Self::tidy(groups);
|
||||
if toolbar_pos.is_some_and(|p| !p.iter().all(|v| v.is_finite())) {
|
||||
*toolbar_pos = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep each set's groups together in the list, after its first one and in the order they
|
||||
/// come, at its first one's position and in its dock column; a set left with one group floats
|
||||
/// (or stays docked) on its own; the dock columns are numbered 1, 2… without gaps.
|
||||
pub fn tidy(groups: &mut Vec<FloatingPanels>) {
|
||||
let mut out: Vec<FloatingPanels> = Vec::with_capacity(groups.len());
|
||||
for mut g in groups.drain(..) {
|
||||
match g.column.and_then(|c| out.iter().rposition(|o| o.column == Some(c))) {
|
||||
Some(i) => {
|
||||
if let Some(first) = out.iter().find(|o| o.column == g.column) {
|
||||
g.pos = first.pos;
|
||||
g.docked = first.docked;
|
||||
}
|
||||
out.insert(i + 1, g);
|
||||
}
|
||||
None => out.push(g),
|
||||
}
|
||||
}
|
||||
let alone: Vec<u32> = out.iter().filter_map(|g| g.column).filter(|c| out.iter().filter(|g| g.column == Some(*c)).count() < 2).collect();
|
||||
for g in &mut out {
|
||||
if g.column.is_some_and(|c| alone.contains(&c)) {
|
||||
g.column = None;
|
||||
}
|
||||
}
|
||||
let mut numbers: Vec<u32> = out.iter().filter_map(|g| g.docked).collect();
|
||||
numbers.sort_unstable();
|
||||
numbers.dedup();
|
||||
for g in &mut out {
|
||||
if let Some(d) = g.docked {
|
||||
g.docked = numbers.iter().position(|n| *n == d).and_then(|k| u32::try_from(k + 1).ok());
|
||||
}
|
||||
}
|
||||
*groups = out;
|
||||
/// Drop a Tools panel position that isn't a number (a hand-edited preferences file or workspace).
|
||||
pub fn sanitize_toolbar_pos(toolbar_pos: &mut Option<[f32; 2]>) {
|
||||
if toolbar_pos.is_some_and(|p| !p.iter().all(|v| v.is_finite())) {
|
||||
*toolbar_pos = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -799,16 +752,12 @@ impl UiState {
|
||||
f.tools.retain(|id| vectorcraft_tools::tool_info(id).is_some());
|
||||
f.tools.first().is_some_and(|k| seen.insert(k.clone()))
|
||||
});
|
||||
FloatingPanels::sanitize(&mut self.floating_panels, &mut self.toolbar_pos);
|
||||
sanitize_toolbar_pos(&mut self.toolbar_pos);
|
||||
crate::edit_toolbar::sanitize(&mut self.toolbar_edits);
|
||||
// `read_preferences` converted the file's older layouts already; this covers a state read
|
||||
// any other way. What it reports lost was dropped there.
|
||||
let _ = crate::layout_migration::migrate_ui(&mut self);
|
||||
self.docking = self.docking.filter(crate::panel_docking::valid);
|
||||
// The tabbed group's panels live in the icon column too once dropped there.
|
||||
self.docking_icons.retain(|id| rail_panel(id).is_some());
|
||||
self.docking_collapsed = std::mem::take(&mut self.docking_collapsed)
|
||||
.into_iter()
|
||||
.filter(|id| crate::menus::normalize_panel(id) == Some(id.as_str()))
|
||||
.take(64)
|
||||
.collect();
|
||||
// Changes kept for workspaces that still exist, under their saved spelling and one per
|
||||
// workspace whatever the case (a hand-edited file).
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
@@ -863,7 +812,6 @@ impl Default for UiState {
|
||||
docking_collapsed: Default::default(),
|
||||
docking_generation: 0,
|
||||
dock_width: None,
|
||||
dock_width_pending: false,
|
||||
toolbar_pos: None,
|
||||
status_bar: true,
|
||||
dock: true,
|
||||
|
||||
@@ -221,9 +221,9 @@ fn the_window_menu_panel_keys() {
|
||||
(egui::Key::T, cmd | alt | shift, "openType"),
|
||||
] {
|
||||
press(&mut app, key, m);
|
||||
assert_eq!(app.ui.open_panel.as_deref(), Some(panel), "{m:?}+{key:?}");
|
||||
assert!(crate::panel_docking::showing(&app.ui, panel), "{m:?}+{key:?}");
|
||||
press(&mut app, key, m);
|
||||
assert_eq!(app.ui.open_panel, None, "{m:?}+{key:?} again hides it");
|
||||
assert!(!crate::panel_docking::showing(&app.ui, panel), "{m:?}+{key:?} again hides it");
|
||||
}
|
||||
press(&mut app, egui::Key::F7, Modifiers::NONE);
|
||||
assert_eq!(app.ui.dock_tab, crate::state::DockTab::Layers);
|
||||
|
||||
@@ -103,7 +103,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let w = if cols == 2 { 80.0 } else { WIDTH };
|
||||
// Where the panel docks: along the window's left edge, under the bars.
|
||||
let edge = ui.available_rect_before_wrap();
|
||||
ui.ctx().data_mut(|d| d.insert_temp(crate::floating::tools_zone_id(), egui::Rect::from_min_size(edge.min, vec2(WIDTH, edge.height()))));
|
||||
ui.ctx().data_mut(|d| d.insert_temp(crate::tools_float::tools_zone_id(), egui::Rect::from_min_size(edge.min, vec2(WIDTH, edge.height()))));
|
||||
match app.ui.toolbar_pos {
|
||||
None => {
|
||||
let margin = egui::Margin { left: 6, right: 0, top: 6, bottom: 6 };
|
||||
@@ -118,7 +118,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let screen = ctx.content_rect();
|
||||
// Kept on screen by its title bar and first tools: the rest scrolls.
|
||||
let size = ctx.memory(|m| m.area_rect(id)).map_or(vec2(w, 160.0), |r| vec2(r.width(), r.height().min(160.0)));
|
||||
let at = crate::floating::clamp(pos, size, screen);
|
||||
let at = crate::tools_float::clamp(pos, size, screen);
|
||||
egui::Area::new(id).order(egui::Order::Middle).fixed_pos(at).show(&ctx, |ui| {
|
||||
flyout_frame(&t, false).inner_margin(egui::Margin { left: 0, right: 0, top: 0, bottom: 4 }).show(ui, |ui| {
|
||||
ui.set_width(w);
|
||||
@@ -160,7 +160,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egu
|
||||
Stroke::new(0.4, t.text_disabled),
|
||||
);
|
||||
}
|
||||
crate::floating::drag_tools(app, ui.ctx(), &hresp.union(gresp.clone()), bounds, floating);
|
||||
crate::tools_float::drag_tools(app, ui.ctx(), &hresp.union(gresp.clone()), bounds, floating);
|
||||
hresp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, ui.is_enabled(), tl!("Toggle single/double column")));
|
||||
if hresp.on_hover_text(tl!("Toggle single/double column")).clicked()
|
||||
// It only fails on bad params, which this never sends; show it all the same.
|
||||
@@ -344,7 +344,7 @@ pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::
|
||||
Ok(json!({ "dialog": dialog }))
|
||||
}
|
||||
// The Gradient and Magic Wand tools: their panels.
|
||||
"gradient" | "magicWand" if app.ui.open_panel.as_deref() == Some(tool) => Ok(json!({ "open": tool })),
|
||||
"gradient" | "magicWand" if crate::panel_docking::showing(&app.ui, tool) => Ok(json!({ "open": tool })),
|
||||
"gradient" | "magicWand" => app.run("window.panel", json!({ "panel": tool })),
|
||||
"artboard" => crate::dialogs::artboard_options::open(app),
|
||||
"eyedropper" => {
|
||||
@@ -796,7 +796,7 @@ fn floating(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
// Kept on screen (a smaller window, or a position saved on a bigger one).
|
||||
let size = ctx.memory(|m| m.area_rect(id)).map_or(vec2(strip_width(&f.tools), PITCH), |r| r.size());
|
||||
let pos = crate::floating::clamp(f.pos, size, screen);
|
||||
let pos = crate::tools_float::clamp(f.pos, size, screen);
|
||||
let mut input = FlyoutInput::default();
|
||||
egui::Area::new(id).order(egui::Order::Middle).fixed_pos(pos).show(ctx, |ui| {
|
||||
flyout_frame(&t, flash).show(ui, |ui| input = flyout_body(ui, &t, active, &f.tools, true));
|
||||
@@ -993,9 +993,9 @@ pub(crate) mod tests {
|
||||
assert_eq!((d.str("__command"), d.str("type")), ("graph.setType".to_string(), "column".to_string()));
|
||||
// The Magic Wand panel opens, and stays open on a second double-click.
|
||||
open_options(&mut app, "magicWand").unwrap();
|
||||
assert_eq!(app.ui.open_panel.as_deref(), Some("magicWand"));
|
||||
assert_eq!(open_options(&mut app, "magicWand").unwrap()["open"], "magicWand");
|
||||
assert_eq!(app.ui.open_panel.as_deref(), Some("magicWand"));
|
||||
assert!(crate::panel_docking::showing(&app.ui, "magicWand"));
|
||||
open_options(&mut app, "magicWand").unwrap();
|
||||
assert!(crate::panel_docking::showing(&app.ui, "magicWand"));
|
||||
}
|
||||
|
||||
/// A double-click at `at`, `time` seconds in (a second apart from the last).
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
//! 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 strip along the window's left edge (lit up) docks it there. 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
|
||||
//! on a bigger window is clamped into this one when drawn.
|
||||
|
||||
use egui::{CornerRadius, Id, Pos2, Rect, Stroke, Vec2, pos2, vec2};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::VectorcraftApp;
|
||||
use crate::theme::Tokens;
|
||||
|
||||
/// Where `window.panel.float {panel: "tools"}` floats the Tools panel given no position.
|
||||
const TOOLS_AT: [f32; 2] = [60.0, 120.0];
|
||||
/// Points the pointer travels from its press before a drop counts (a click on the title bar of a
|
||||
/// panel floating over the left edge doesn't dock it).
|
||||
const TRAVEL: f32 = 4.0;
|
||||
|
||||
/// A move in progress: where the pointer holds the panel from its top-left corner, where the press
|
||||
/// began, and where the panel was before it (to put it back on Escape).
|
||||
#[derive(Clone, Debug)]
|
||||
struct Move {
|
||||
grab: Vec2,
|
||||
from: Pos2,
|
||||
original: Option<[f32; 2]>,
|
||||
}
|
||||
|
||||
fn move_id() -> Id {
|
||||
Id::unique("tools-float-move")
|
||||
}
|
||||
|
||||
/// The strip along the window's left edge where the Tools panel docks, as last laid out.
|
||||
pub(crate) fn tools_zone_id() -> Id {
|
||||
Id::unique("tools-float-zone")
|
||||
}
|
||||
|
||||
/// `pos` moved so a panel of `size` stays inside `screen` (a position saved on a bigger window).
|
||||
pub fn clamp(pos: [f32; 2], size: Vec2, screen: Rect) -> Pos2 {
|
||||
let [x, y] = pos;
|
||||
let (x, y) = if x.is_finite() && y.is_finite() { (x, y) } else { (screen.left() + 60.0, screen.top() + 100.0) };
|
||||
let fit = |v: f32, max: f32| if v > max { max.floor() } else { v };
|
||||
pos2(fit(x, screen.right() - size.x).max(screen.left()), fit(y, screen.bottom() - size.y).max(screen.top()))
|
||||
}
|
||||
|
||||
fn gesture_allowed(ctx: &egui::Context) -> bool {
|
||||
ctx.input(|input| input.focused && !input.key_pressed(egui::Key::Escape))
|
||||
}
|
||||
|
||||
fn start(app: &VectorcraftApp, ctx: &egui::Context, grab: Vec2) {
|
||||
let Some(from) = ctx.input(|i| i.pointer.press_origin().or(i.pointer.interact_pos())) else { return };
|
||||
ctx.data_mut(|d| d.insert_temp(move_id(), Move { grab, from, original: app.ui.toolbar_pos }));
|
||||
}
|
||||
|
||||
/// The Tools panel's title bar was pressed (`title`, the panel's bounds `bounds`): docked, dragging
|
||||
/// it out of the panel floats the panel under the pointer; floating at `floating`, a drag moves it.
|
||||
pub(crate) fn drag_tools(app: &mut VectorcraftApp, ctx: &egui::Context, title: &egui::Response, bounds: Rect, floating: Option<Pos2>) {
|
||||
if !gesture_allowed(ctx) || ctx.data(|d| d.get_temp::<Move>(move_id()).is_some()) {
|
||||
return;
|
||||
}
|
||||
let Some(at) = ctx.input(|i| i.pointer.interact_pos()) else { return };
|
||||
match floating {
|
||||
Some(pos) if title.drag_started() => start(app, ctx, at - pos),
|
||||
None if title.dragged() && !bounds.contains(at) => {
|
||||
// Held where the press grabbed the title bar.
|
||||
let grab = ctx.input(|i| i.pointer.press_origin()).map_or(vec2(10.0, 7.0), |p| p - bounds.min);
|
||||
start(app, ctx, grab);
|
||||
let p = at - grab;
|
||||
app.ui.toolbar_pos = Some([p.x, p.y]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The docking strip under `at`, if any.
|
||||
fn drop_zone(ctx: &egui::Context, at: Pos2) -> Option<Rect> {
|
||||
ctx.data(|d| d.get_temp::<Rect>(tools_zone_id())).filter(|r| r.contains(at))
|
||||
}
|
||||
|
||||
fn highlight(ctx: &egui::Context, zone: Rect) {
|
||||
let t = Tokens::get(ctx);
|
||||
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Foreground, Id::unique("tools-float-drop")));
|
||||
painter.rect(zone.shrink(1.0), CornerRadius::same(2), t.accent.gamma_multiply(0.2), Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
|
||||
/// Carry a move on (at the start of a frame, before the panels are drawn): the panel follows the
|
||||
/// pointer and the docking strip lights up under it; the release docks it there or leaves it
|
||||
/// floating where it is.
|
||||
pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let Some(m) = ctx.data(|d| d.get_temp::<Move>(move_id())) else { return };
|
||||
if !gesture_allowed(ctx) {
|
||||
app.ui.toolbar_pos = m.original;
|
||||
ctx.data_mut(|d| d.remove::<Move>(move_id()));
|
||||
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));
|
||||
if ctx.input(|i| i.pointer.primary_down()) {
|
||||
if let Some(p) = at.map(|at| at - m.grab)
|
||||
&& p.x.is_finite()
|
||||
&& p.y.is_finite()
|
||||
&& app.ui.toolbar_pos.is_some()
|
||||
{
|
||||
app.ui.toolbar_pos = Some([p.x, p.y]);
|
||||
}
|
||||
if let Some(zone) = zone {
|
||||
highlight(ctx, zone);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ctx.data_mut(|d| d.remove::<Move>(move_id()));
|
||||
if zone.is_some() {
|
||||
app.ui.toolbar_pos = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The `x`, `y` params: a top-left corner in screen points, if given.
|
||||
fn position(p: &Value) -> Result<Option<Pos2>, String> {
|
||||
let coord = |k: &str| match p.get(k) {
|
||||
None | Some(Value::Null) => Some(None),
|
||||
Some(v) => v.as_f64().filter(|f| f.abs() < 1e6).map(|f| Some(f as f32)),
|
||||
};
|
||||
match (coord("x"), coord("y")) {
|
||||
(Some(Some(x)), Some(Some(y))) => Ok(Some(pos2(x, y))),
|
||||
(Some(None), Some(None)) => Ok(None),
|
||||
_ => Err("x and y must both be numbers".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// `window.panel.float` / `window.panel.dock` for the Tools panel (`panel: "tools"`); `None` for
|
||||
/// any other panel.
|
||||
pub(crate) fn command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<Value, String>> {
|
||||
let float = match id {
|
||||
"window.panel.float" => true,
|
||||
"window.panel.dock" => false,
|
||||
_ => return None,
|
||||
};
|
||||
let raw = p.get("panel").and_then(Value::as_str)?.trim();
|
||||
if !(raw.eq_ignore_ascii_case("tools") || raw.eq_ignore_ascii_case("toolbar")) {
|
||||
return None;
|
||||
}
|
||||
Some(position(p).map(|at| {
|
||||
app.ui.toolbar_pos = float.then(|| at.map_or(TOOLS_AT, |p| [p.x, p.y]));
|
||||
app.ui.toolbar |= float;
|
||||
json!({ "panel": "tools", "floating": float, "pos": app.ui.toolbar_pos })
|
||||
}))
|
||||
}
|
||||
@@ -34,22 +34,24 @@ pub struct Workspace {
|
||||
pub status_bar: bool,
|
||||
/// Tool groups torn off the toolbar into floating strips (built-in workspaces: none).
|
||||
pub floating_flyouts: Vec<FloatingFlyout>,
|
||||
/// Panels dragged out of the dock, where they float (built-in workspaces: none).
|
||||
/// Floating panel groups in workspaces saved before the canvas joined `docking`; read only
|
||||
/// to convert them ([`crate::layout_migration`]).
|
||||
#[serde(skip_serializing)]
|
||||
pub floating_panels: Vec<FloatingPanels>,
|
||||
/// Custom docking tree, including floating tab groups.
|
||||
/// The workspace's own panel layout; none: the default layout from its flags.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub docking: Option<craft_ui::docking::Layout<String>>,
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
|
||||
pub docking_hidden: std::collections::BTreeMap<String, craft_ui::docking::Location<String>>,
|
||||
/// Panels whose saved return destination is the iconic rail.
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
|
||||
/// Panels dropped on the icon column, in older workspaces (read only).
|
||||
#[serde(default, skip_serializing)]
|
||||
pub docking_icons: std::collections::BTreeSet<String>,
|
||||
/// The panels shown by dock groups collapsed to their tabs.
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
|
||||
/// The panels shown by dock groups collapsed to their tabs, in older workspaces (read only).
|
||||
#[serde(default, skip_serializing)]
|
||||
pub docking_collapsed: std::collections::BTreeSet<String>,
|
||||
/// Where the Tools panel floats (built-in workspaces: docked).
|
||||
pub toolbar_pos: Option<[f32; 2]>,
|
||||
/// Main dock column width in screen points (#1753).
|
||||
/// Main dock column width in screen points while `docking` is none (#1753).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub dock_width: Option<f32>,
|
||||
}
|
||||
@@ -179,11 +181,11 @@ pub fn capture(ui: &UiState, name: &str) -> Workspace {
|
||||
open_panel: ui.open_panel.clone(),
|
||||
status_bar: ui.status_bar,
|
||||
floating_flyouts: ui.floating_flyouts.clone(),
|
||||
floating_panels: ui.floating_panels.clone(),
|
||||
floating_panels: Vec::new(),
|
||||
docking: ui.docking.clone(),
|
||||
docking_hidden: ui.docking_hidden.clone(),
|
||||
docking_icons: ui.docking_icons.clone(),
|
||||
docking_collapsed: ui.docking_collapsed.clone(),
|
||||
docking_icons: Default::default(),
|
||||
docking_collapsed: Default::default(),
|
||||
toolbar_pos: ui.toolbar_pos,
|
||||
dock_width: ui.dock_width,
|
||||
}
|
||||
@@ -204,16 +206,12 @@ pub fn apply(ui: &mut UiState, w: &Workspace) {
|
||||
ui.open_panel = w.open_panel.clone();
|
||||
ui.status_bar = w.status_bar;
|
||||
ui.floating_flyouts = w.floating_flyouts.clone();
|
||||
ui.floating_panels = w.floating_panels.clone();
|
||||
ui.docking_generation = ui.docking_generation.wrapping_add(1);
|
||||
ui.docking = w.docking.clone().filter(crate::panel_docking::valid);
|
||||
ui.docking_hidden = w.docking_hidden.clone();
|
||||
ui.docking_icons = w.docking_icons.iter().filter(|id| crate::state::rail_panel(id).is_some()).take(64).cloned().collect();
|
||||
ui.docking_collapsed = w.docking_collapsed.iter().filter(|id| crate::menus::normalize_panel(id) == Some(id.as_str())).take(64).cloned().collect();
|
||||
ui.toolbar_pos = w.toolbar_pos;
|
||||
ui.dock_width = w.dock_width;
|
||||
ui.dock_width_pending = true;
|
||||
FloatingPanels::sanitize(&mut ui.floating_panels, &mut ui.toolbar_pos);
|
||||
crate::state::sanitize_toolbar_pos(&mut ui.toolbar_pos);
|
||||
ui.flyout = None;
|
||||
ui.workspace = w.name.clone();
|
||||
}
|
||||
@@ -562,14 +560,19 @@ mod tests {
|
||||
// Changes made in it after saving (a panel moved, the control bar shown)…
|
||||
ui.control_bar = true;
|
||||
ui.dock_tab = DockTab::Layers;
|
||||
ui.docking_icons.insert("layers".into());
|
||||
ui.docking_collapsed.insert("layers".into());
|
||||
let mut layout = crate::panel_docking::default_layout(&ui);
|
||||
if let Some(craft_ui::docking::Node::Split { second, .. }) = layout.root.as_mut()
|
||||
&& let craft_ui::docking::Node::Dock { columns, .. } = second.as_mut()
|
||||
{
|
||||
columns[0].groups[0].minimized = true;
|
||||
}
|
||||
ui.docking = Some(layout.clone());
|
||||
// …are still there after switching away and back.
|
||||
switch(&mut ui, ESSENTIALS).unwrap();
|
||||
assert!(!ui.control_bar && ui.docking_icons.is_empty() && ui.docking_collapsed.is_empty(), "Essentials comes as saved");
|
||||
assert!(!ui.control_bar && ui.docking.is_none(), "Essentials comes as saved");
|
||||
switch(&mut ui, "Görkem").unwrap();
|
||||
assert!(ui.control_bar && ui.dock_tab == DockTab::Layers && ui.docking_icons.contains("layers"));
|
||||
assert!(ui.docking_collapsed.contains("layers"), "its collapsed group too");
|
||||
assert!(ui.control_bar && ui.dock_tab == DockTab::Layers);
|
||||
assert_eq!(ui.docking, Some(layout), "its panel layout too");
|
||||
// Built-in workspaces remember theirs too.
|
||||
switch(&mut ui, ESSENTIALS).unwrap();
|
||||
ui.status_bar = false;
|
||||
@@ -583,7 +586,7 @@ mod tests {
|
||||
assert!(ui.control_bar);
|
||||
// Reset Görkem: back to the way it was saved.
|
||||
assert_eq!(reset(&mut ui), "Görkem");
|
||||
assert!(!ui.control_bar && ui.docking_icons.is_empty());
|
||||
assert!(!ui.control_bar && ui.docking.is_none());
|
||||
switch(&mut ui, ESSENTIALS).unwrap();
|
||||
switch(&mut ui, "Görkem").unwrap();
|
||||
assert!(!ui.control_bar, "a reset workspace stays reset");
|
||||
@@ -723,7 +726,6 @@ mod tests {
|
||||
let wide = find(&ui, "Wide").unwrap();
|
||||
apply(&mut ui, &wide);
|
||||
assert_eq!(ui.dock_width, Some(420.0));
|
||||
assert!(ui.dock_width_pending);
|
||||
let narrow = find(&ui, "Narrow").unwrap();
|
||||
apply(&mut ui, &narrow);
|
||||
assert_eq!(ui.dock_width, Some(260.0));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+60
-62
@@ -1,6 +1,6 @@
|
||||
# Control protocol
|
||||
|
||||
> **Last updated:** 2026-10-24 · **Change:** `ui.screenshot`'s 64 MiB inline-PNG cap and its path-plus-data downgrade; previously: symbol library panels (`window.symbolLibrary`, `window.symbolLibrary.other`, `ui.saveSymbolLibrary`); synthetic input budgets (#1709); bounded transport waits (#1726).
|
||||
> **Last updated:** 2026-10-25 · **Change:** panel docking on craft-ui's layout: `ui.docking`, the parameters of `window.panel.float` and `window.panel.dock`; previously: `ui.screenshot`'s 64 MiB inline-PNG cap and its path-plus-data downgrade; symbol library panels (`window.symbolLibrary`, `window.symbolLibrary.other`, `ui.saveSymbolLibrary`); synthetic input budgets (#1709); bounded transport waits (#1726).
|
||||
|
||||
`vectorcraft --control <port>` listens on `127.0.0.1:<port>` (loopback only). The port must be a number
|
||||
from 1 to 65535 (`--control=<port>` works too), given once; otherwise the app prints an error and exits
|
||||
@@ -309,12 +309,10 @@ Edit › Keyboard Shortcuts (entry `panel:<id>`, every panel) and pressed with `
|
||||
the Window menu's items.
|
||||
`window.panel` takes a panel id in any case or the panel's display label (`"Layers"`, `"Color Guide"`).
|
||||
|
||||
Collapsing the dock: `window.collapseDock {collapsed?}` (the » at the top of the dock; omitted toggles) hides the
|
||||
Properties | Layers | Libraries group and puts its three panels as icons at the top of the icon column, under a «
|
||||
that expands them again. While collapsed, those icons and `window.panel {panel: "properties"|"layers"|"libraries"}`
|
||||
pop the panel out next to the column like the other icon panels (`ui.dock_collapsed`, `ui.open_panel` in
|
||||
`ui.inspect`); expanding with one popped out shows its tab. The state is saved with the preferences and in user
|
||||
workspaces; the built-in workspaces expand the dock.
|
||||
Collapsing the dock: `window.collapseDock {collapsed?}` (the » / « on the dock's column bar; omitted toggles the
|
||||
outer column) collapses every expanded dock column to icons (`true`) or expands the outer column, Properties |
|
||||
Layers | Libraries (`false`). A panel in a collapsed column opens as a flyout from its icon, as `window.panel` does
|
||||
(`ui.open_panel` in `ui.inspect`, `flyout` in `ui.docking`); the state is `ui.dock_collapsed`.
|
||||
|
||||
Floating tool groups: dragging or clicking the tear-off bar down a tool group's flyout (or releasing the long press
|
||||
that opened it over the bar) floats the group as a strip of tool buttons, moved by the same bar and put back in the
|
||||
@@ -360,32 +358,12 @@ and can't be picked, moved or deleted, and no guide goes onto a locked layer; Du
|
||||
Selected and Flatten Artwork keep them on the merged layer (Flatten discards a hidden layer's). Guides in files saved
|
||||
before guides had layers go on the top visible, unlocked layer when the document opens.
|
||||
|
||||
Floating panels: dragging a dock tab, a panel icon or a popped-out panel's title out of the dock floats that panel
|
||||
inside the window, and the strip right of the dock's tabs floats the whole Properties | Layers | Libraries group. A
|
||||
floating group moves by its title bar (or the strip right of its tabs); a tab dragged out of it floats on its own;
|
||||
dropped on another group's title bar or tabs it stacks with that group, and dropped on the dock (lit up while the
|
||||
pointer is over it) or closed with its × its panels go back to the tabbed group or the icon column. Dropped on the upper or
|
||||
lower half of another group, floating or docked (a line shows where), a group joins it in a set right above or below
|
||||
it: the groups stack top to bottom, each
|
||||
showing one of its panels, and move together by the top group's title bar, whose × docks them all; a group dragged by
|
||||
its tab strip (a lone panel's tab or the strip right of the tabs) leaves the set. A double-click on a tab collapses a
|
||||
group to its tabs, and another expands it. Dropped on the left edge of the icon column or of a dock column, a set
|
||||
docks as a new dock column there: its groups stack in it as in a floating set, take groups dropped on their edges and
|
||||
tabs, and leave it dragged by their tab strip; the strip along the column's top floats the whole column again, and
|
||||
its × puts its panels back where they live.
|
||||
`window.panel.float {panel, x?, y?, onto?, below?, above?, group?, collapsed?}` floats a panel (with `group`, its whole
|
||||
group) with its top-left corner at `x`, `y` (window points; default cascaded), or stacks it as a tab with the floating
|
||||
group holding `onto`, or puts its group in a set right `below` or `above` the group holding that panel; `collapsed`
|
||||
collapses or expands its group;
|
||||
`window.panel.dock {panel, group?, column?}` puts it back, or with `column` (`true`, or the place: 1 next to the icon
|
||||
column, counting left) docks its group and set as a dock column; `window.panel.float` on a docked panel floats its
|
||||
column. `panel` takes the `window.panel` ids and labels, and `"tools"` for
|
||||
the Tools panel, which floats by its title bar and docks on the window's left edge. `window.panel` on a floating
|
||||
panel shows its tab (expanding its group). `ui.floating_panels` in `ui.inspect` lists the groups (panel ids, the
|
||||
index of the tab shown, the top-left corner, `column`: the set it is stacked in, top to bottom in list order,
|
||||
`collapsed`, and `docked`: the dock column it stands in) and `ui.toolbar_pos` the Tools panel's corner (null: docked); both are saved with the
|
||||
preferences and in user workspaces (the built-in workspaces dock everything), and a corner saved on a bigger window
|
||||
is clamped into this one when drawn. Floating panels stay inside the app window (no separate OS windows).
|
||||
Floating panels: the window's panels and the canvas are one craft-ui docking layout (see Shared panel docking
|
||||
below). Dragging a tab, a group's drag bar or an icon out of the dock and dropping it on the canvas or on no target
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -858,39 +836,58 @@ distortions or mesh rows and columns, Reset, Envelope Options); each runs an `ob
|
||||
|
||||
## Shared panel docking
|
||||
|
||||
Panel arrangements support tab grouping and reordering, horizontal and vertical splits,
|
||||
and movable, resizable floating groups inside the application window. Drag a panel tab,
|
||||
or use its context menu. Floating groups remain within the application viewport.
|
||||
The canvas and the panels form one craft-ui docking layout: the protected `canvas` panel beside a dock at the
|
||||
window's right edge. The dock's columns, from the window edge inwards, are the Properties | Layers | Libraries
|
||||
column, the icon column (one group per row of icon panels) and any columns the user adds. Floating windows hold
|
||||
groups stacked top to bottom or split side by side. Workspaces save the layout (`ui.docking` in `ui.inspect`;
|
||||
absent while the workspace was never customized and its layout follows `ui.dock_tab`, `ui.dock_collapsed` and
|
||||
`ui.dock_width`) and where each hidden panel was (`ui.docking_hidden`). `ui.inspect` has no `floating_panels`.
|
||||
|
||||
The shared layout begins with the existing workspace on the first docking operation.
|
||||
Workspace saves retain its groups, active tabs, split sizes, floating geometry, and the
|
||||
positions of hidden panels. Resetting a built-in workspace restores its default dock.
|
||||
Commands (`panel` and every panel parameter take a panel id in any case or its display label):
|
||||
|
||||
- `window.panel.move {panel, anchor, zone?, before?}` moves a panel into the target group.
|
||||
`zone` is `tab` (default), `center`, `left`, `right`, `top`, or `bottom`; `before` reorders
|
||||
tabs by panel ID. The destination must already be visible.
|
||||
- `window.panel.float {panel, x?, y?, width?, height?}` detaches a panel.
|
||||
- `window.panel.dock {panel, anchor?, zone?, before?}` returns a panel to its previous
|
||||
dock placement when possible, or to the supplied destination.
|
||||
- `window.panel.activate {panel}` reveals and selects a panel, restoring its saved
|
||||
position when possible. `window.panel.close {panel}` hides it.
|
||||
- `window.panel.collapse {panel, collapsed?}` shows the dock group holding a panel as its tabs
|
||||
only, as a double-click on a tab does (without `collapsed` it toggles), and makes the panel the
|
||||
group's shown one; showing another of its panels expands the group. Only a group that is the
|
||||
whole dock or sits above or below another collapses (one beside another is refused). Floating
|
||||
groups collapse with `window.panel.float {panel, collapsed}`. Returns `{panel, collapsed}`.
|
||||
| Command | Params (`?` optional) | Does |
|
||||
|---|---|---|
|
||||
| `window.panel.move` | `{panel, anchor, zone?, before?, group?, placement?}` | into the group holding `anchor`: `zone` is `tab` (default), `center`, `left`, `right`, `top`, `bottom`, `above` or `below` (a group of its own above or below it); `before` reorders tabs |
|
||||
| `window.panel.float` | `{panel, x?, y?, width?, height?, group?, column?, onto?, above?, below?, collapsed?}` | floats the panel, its group (`group`) or its dock column or floating window (`column`; a number counts as `true`) at the rect given, else inside the dock area at the size a drag floats it at |
|
||||
| `window.panel.dock` | `{panel, anchor?, zone?, before?, group?, column?, side?, onto?, above?, below?, icon?, collapsed?}` | without a destination, a floating panel goes back where it floated from (else joins the first dock group) and a hidden one shows where it was; `anchor` and `zone` as for `move`; `side` (`Start`, `End`) makes it a new innermost column of that dock |
|
||||
| `window.panel.activate`, `.close`, `.closeGroup`, `.closeOthers` | `{panel}` | shows and selects, or hides |
|
||||
| `window.panel.minimize` | `{panel, minimized?}` | minimizes a dock group to its tabs, or a floating window to its title bar; omitted toggles |
|
||||
| `window.panel.collapse` | `{panel, collapsed?}` | `window.panel.minimize`; returns `{panel, collapsed, layout}` |
|
||||
| `window.panel.iconic` | `{panel, iconic?}` | collapses the panel's dock column to icons, or expands it |
|
||||
| `window.panel.layout`, `ui.dock` | `{action}` | one craft-ui `Action`, externally tagged, e.g. `{"MoveFloating":{"panel":"swatches","rect":[44,66,360,450]}}` or `{"ResizeSplit":{"path":[],"size":{"Ratio":0.35}}}`; `ui.dock` also takes craft-ui's `{operation, …}` |
|
||||
|
||||
Panel IDs are stable across themes. Invalid IDs, non-finite or invalid geometry, and
|
||||
invalid destinations return an error without changing the arrangement.
|
||||
Parameters from VectorCraft's own docking, on `window.panel.float` and `window.panel.dock`:
|
||||
|
||||
`window.panel.layout {action}` also accepts the shared, externally tagged action schema.
|
||||
For example, `{"action":{"MoveFloating":{"panel":"properties","rect":[44,66,360,450]}}}`
|
||||
updates a floating group's position and size; `{"action":{"ResizeSplit":{"path":[],"size":{"Ratio":0.35}}}}`
|
||||
resizes the root divider (`false`/`true` path entries select first/second children).
|
||||
`Move` with `placement: {"Tab":{"before":"swatches"}}` reorders tabs, and
|
||||
`SetStackOpen {panel,open}` / `ResizeStack {panel,height}` control accordion entries.
|
||||
The UI dispatches these same commands. Unknown panel IDs, invalid geometry, and stale
|
||||
split paths return an error without changing the workspace.
|
||||
- `onto`: as a tab of the group holding that panel. On `float` that group must float; on `dock` it is `anchor`.
|
||||
- `above`, `below`: as a group of its own above or below the group holding that panel, in its floating window or
|
||||
dock column.
|
||||
- `column: n` on `dock` (with no other destination): its group (or the panel, when `group` is false and its group
|
||||
holds others) as a new dock column at place `n`, 1 next to the icon column, counting inwards. A panel whose group
|
||||
floats takes its whole floating window along. `column: true` with no `anchor` or `side` docks at the innermost place.
|
||||
- `icon: true` on `dock`: in a row of its own at the end of the icon column.
|
||||
- `collapsed`: then minimizes (`true`) or restores (`false`) its group or floating window.
|
||||
|
||||
Give at most one of `onto`, `above`, `below`, `icon` and a column place, and none of them with `anchor`, `side`,
|
||||
`zone`, `before`, `x`, `y`, `width` or `height`. The commands return `{layout, closed, floating}`: the layout,
|
||||
the panels the command closed, and the floating panels. A hidden panel is shown first. Unknown panels, invalid
|
||||
geometry or destinations, and stale split paths return an error and leave the layout unchanged.
|
||||
|
||||
`ui.docking` (a control method, read only) returns:
|
||||
|
||||
- `layout`: the layout in use; `customized`: whether the workspace has a saved layout; `hidden`: the hidden panels
|
||||
with a remembered place.
|
||||
- `flyout`: the panel open as a flyout from an icon column; `focused`: the panel the user last clicked or pressed
|
||||
in; `autoCollapse`: flyouts close on a press outside them; `panelsShown`: false while Tab or a full screen mode
|
||||
hides the panels.
|
||||
- `hitRects`: `[{hit, rect}]`, every docking control the main window drew last frame (tabs, drag bars, borders,
|
||||
column bars, » buttons, icons, icon grips, floating title bars and close buttons, resize edges, dividers) as a
|
||||
craft-ui `Hit`, e.g. `{"Tab":"layers"}`, with `[x, y, width, height]` in window points. Groups, columns and
|
||||
windows are named by their first panel.
|
||||
|
||||
Floating windows in OS windows of their own (the desktop): `hitRects` leaves out their controls, and `ui.pointer`,
|
||||
`ui.click` and `ui.drag` reach the main window only, so drive them with commands. Such a window's place on screen is
|
||||
`floating[].native` in the layout (`rect` is where it would float inside the app window). Keys typed in one run the
|
||||
main window's shortcuts; modal dialogs block them.
|
||||
|
||||
## Synthetic input hardening checklist
|
||||
|
||||
@@ -928,6 +925,7 @@ hidden tab (whose frames the browser pauses) answers when it is shown again.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-25 | minor | Panel docking on craft-ui's layout: `ui.docking`, the floating and dock parameters, native floating windows. |
|
||||
| 2026-10-24 | minor | Documented `ui.screenshot`'s 64 MiB inline-PNG reply cap: with `path` the file is still written and `pngBase64` is omitted when over the cap; a path-less `data: true` request fails before encoding. |
|
||||
| 2026-10-23 | minor | Documented the symbol library panel commands (`window.symbolLibrary`, `.other`, `ui.saveSymbolLibrary`, `window.userSymbolLibrary1`…`10`). |
|
||||
| 2026-10-13 | minor | Documented count, pending-event and per-frame request budgets (#1709). |
|
||||
|
||||
+4
-2
File diff suppressed because one or more lines are too long
+2
-2
@@ -1,7 +1,7 @@
|
||||
# PDF backend
|
||||
|
||||
PDF writing uses `craft-pdf` from `storytold/craft-libs`, pinned to
|
||||
`2eca9b3a53a98ac52efed2a27353c6a6927ead10`. Its `write` feature supplies Krilla configuration,
|
||||
`a0916808f7d78bed8c7b90cdf937176015454a7f`. Its `write` feature supplies Krilla configuration,
|
||||
metadata construction, fallible font preparation and serialization, and native unwind guards.
|
||||
The dependency disables default features and enables `write` and `interpret`.
|
||||
The production writer retains Krilla’s raster-image support without its optional text shaper.
|
||||
@@ -54,7 +54,7 @@ is known-incomplete rejection rather than a general PDF validity certificate.
|
||||
The five patched Hayro crates and Krilla are selected in the root `[patch.crates-io]`
|
||||
from the same immutable revision as `craft-pdf`. `cargo xtask pdf-deps` rejects missing,
|
||||
duplicate, local-path, registry and mismatched revisions for those seven packages.
|
||||
`craft-print` and `craft-ui` keep their existing independent revisions.
|
||||
`craft-print`, `craft-ui` and the other craft-libs crates use the same revision.
|
||||
|
||||
Dependency asset hashes and sources are in `ASSETS.md`; redistributed license texts are
|
||||
in `licenses/pdf-backend`. Package staging carries those texts together with `NOTICE`
|
||||
|
||||
File diff suppressed because one or more lines are too long
+5
-4
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@ use std::process::Command;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
const REV: &str = "2eca9b3a53a98ac52efed2a27353c6a6927ead10";
|
||||
const REV: &str = "a0916808f7d78bed8c7b90cdf937176015454a7f";
|
||||
const PACKAGES: &[(&str, &str)] = &[
|
||||
("craft-pdf", "0.1.0"),
|
||||
("krilla", "0.8.2"),
|
||||
|
||||
Reference in New Issue
Block a user