Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Alejandro Alonso 2026-03-20 10:20:43 +01:00
commit f068842a6c
93 changed files with 2844 additions and 1645 deletions

View File

@ -32,6 +32,9 @@
### :sparkles: New features & Enhancements
- Access Tokens look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114)
- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112), [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114)
- Access Tokens look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114)
- Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714)
### :bug: Bugs fixed
@ -43,6 +46,8 @@
- Update copy on penpot update message [Taiga #12924](https://tree.taiga.io/project/penpot/issue/12924)
- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534)
- Fix tooltip shown on tab change [Taiga #13627](https://tree.taiga.io/project/penpot/issue/13627)
- Fix tooltip activated when tab change [Taiga #13627](https://tree.taiga.io/project/penpot/issue/13627)
## 2.14.0 (Unreleased)

View File

@ -88,7 +88,6 @@
:columns [:id :name :perms :type :created-at :updated-at :expires-at]})
(mapv decode-row)))
(def ^:private schema:get-current-mcp-token
[:map {:title "get-current-mcp-token"}])
@ -101,6 +100,7 @@
:type "mcp"}
{:order-by [[:expires-at :asc] [:created-at :asc]]
:columns [:token :expires-at]})
(remove #(ct/is-after? (:expires-at %) request-at))
(remove #(and (some? (:expires-at %))
(ct/is-after? request-at (:expires-at %))))
(map decode-row)
(first)))

View File

@ -136,6 +136,8 @@
:webhooks
;; TODO: deprecate this flag and consolidate the code
:render-wasm-dpr
;; Show WASM renderer info label (hidden by default).
:render-wasm-info
:hide-release-modal
:subscriptions
:subscriptions-old
@ -162,7 +164,8 @@
;; Activates the nitrate module
:nitrate
:mcp})
:mcp
:background-blur})
(def all-flags
(set/union email login varia))

View File

@ -0,0 +1,160 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/objects-map",
"text-editor/v2",
"render-wasm/v1",
"text-editor-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~ud7430f09-4f59-8049-8007-6277bb7586f6",
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "test_feature",
"~:revn": 39,
"~:modified-at": "~m1773253429056",
"~:vern": 0,
"~:id": "~u93bfc923-66b2-813c-8007-b2725507ba08",
"~:is-shared": false,
"~:migrations": {
"~#ordered-set": [
"legacy-2",
"legacy-3",
"legacy-5",
"legacy-6",
"legacy-7",
"legacy-8",
"legacy-9",
"legacy-10",
"legacy-11",
"legacy-12",
"legacy-13",
"legacy-14",
"legacy-16",
"legacy-17",
"legacy-18",
"legacy-19",
"legacy-25",
"legacy-26",
"legacy-27",
"legacy-28",
"legacy-29",
"legacy-31",
"legacy-32",
"legacy-33",
"legacy-34",
"legacy-36",
"legacy-37",
"legacy-38",
"legacy-39",
"legacy-40",
"legacy-41",
"legacy-42",
"legacy-43",
"legacy-44",
"legacy-45",
"legacy-46",
"legacy-47",
"legacy-48",
"legacy-49",
"legacy-50",
"legacy-51",
"legacy-52",
"legacy-53",
"legacy-54",
"legacy-55",
"legacy-56",
"legacy-57",
"legacy-59",
"legacy-62",
"legacy-65",
"legacy-66",
"legacy-67",
"0001-remove-tokens-from-groups",
"0002-normalize-bool-content-v2",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content-v2",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0008-fix-library-colors-v4",
"0009-clean-library-colors",
"0009-add-partial-text-touched-flags",
"0010-fix-swap-slots-pointing-non-existent-shapes",
"0011-fix-invalid-text-touched-flags",
"0012-fix-position-data",
"0013-fix-component-path",
"0013-clear-invalid-strokes-and-fills",
"0014-fix-tokens-lib-duplicate-ids",
"0014-clear-components-nil-objects",
"0015-fix-text-attrs-blank-strings",
"0015-clean-shadow-color",
"0016-copy-fills-from-position-data-to-text-node"
]
},
"~:version": 67,
"~:project-id": "~ud7430f09-4f59-8049-8007-6277bb765abd",
"~:created-at": "~m1773229633566",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~u93bfc923-66b2-813c-8007-b2725507ba09"
],
"~:pages-index": {
"~u93bfc923-66b2-813c-8007-b2725507ba09": {
"~:objects": {
"~#penpot/objects-map/v2": {
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~u11813dac-4dc3-80d8-8007-b2804a521773\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ac\",\"~u11813dac-4dc3-80d8-8007-b28053db9865\",\"~u11813dac-4dc3-80d8-8007-b2806dfd8a36\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a4\",\"~u5b58e018-fa5e-805d-8007-b27294ad5af1\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a7\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14af\",\"~u11813dac-4dc3-80d8-8007-b28053db9864\",\"~u11813dac-4dc3-80d8-8007-b2806dfde19d\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14aa\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ab\"]]]",
"~u11813dac-4dc3-80d8-8007-b2806dfd8a36": "[\"~#shape\",[\"^ \",\"~:y\",93.99999856948853,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:index\",2,\"~:name\",\"Group\",\"~:width\",303,\"~:type\",\"~:group\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1307.9999368190765,\"~:y\",93.99999856948853]],[\"^:\",[\"^ \",\"~:x\",1610.9999368190765,\"~:y\",93.99999856948853]],[\"^:\",[\"^ \",\"~:x\",1610.9999368190765,\"~:y\",396.9999985694885]],[\"^:\",[\"^ \",\"~:x\",1307.9999368190765,\"~:y\",396.9999985694885]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b2806dfd8a36\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1307.9999368190765,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1307.9999368190765,\"~:y\",93.99999856948853,\"^6\",303,\"~:height\",303,\"~:x1\",1307.9999368190765,\"~:y1\",93.99999856948853,\"~:x2\",1610.9999368190765,\"~:y2\",396.9999985694885]],\"~:fills\",[],\"~:flip-x\",null,\"^D\",303,\"~:flip-y\",null,\"~:shapes\",[\"~u11813dac-4dc3-80d8-8007-b2806dfd8a37\",\"~u11813dac-4dc3-80d8-8007-b2806dfde19c\"]]]",
"~u11813dac-4dc3-80d8-8007-b2806dfd8a37": "[\"~#shape\",[\"^ \",\"~:y\",93.99999856948853,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",194,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1416.9999368190765,\"~:y\",93.99999856948853]],[\"^<\",[\"^ \",\"~:x\",1610.9999368190765,\"~:y\",93.99999856948853]],[\"^<\",[\"^ \",\"~:x\",1610.9999368190765,\"~:y\",293.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1416.9999368190765,\"~:y\",293.9999985694885]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b2806dfd8a37\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b2806dfd8a36\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1416.9999368190765,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1416.9999368190765,\"~:y\",93.99999856948853,\"^8\",194,\"~:height\",200,\"~:x1\",1416.9999368190765,\"~:y1\",93.99999856948853,\"~:x2\",1610.9999368190765,\"~:y2\",293.9999985694885]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#174be9\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^F\",200,\"~:flip-y\",null]]",
"~u5b58e018-fa5e-805d-8007-b27294ad5af1": "[\"~#shape\",[\"^ \",\"~:y\",-22.00000122055286,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",445.0000023918125,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",162.9999992063131,\"~:y\",-22.000001220552832]],[\"^<\",[\"^ \",\"~:x\",608.0000015981256,\"~:y\",-22.000001220552832]],[\"^<\",[\"^ \",\"~:x\",608.0000015981256,\"~:y\",423.0000182011825]],[\"^<\",[\"^ \",\"~:x\",162.9999992063131,\"~:y\",423.0000182011825]]],\"~:r2\",20,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",20,\"~:blur\",[\"^ \",\"~:id\",\"~u5b58e018-fa5e-805d-8007-b2729af5ed71\",\"^9\",\"~:background-blur\",\"~:value\",4,\"~:hidden\",false],\"~:r1\",20,\"^B\",\"~u5b58e018-fa5e-805d-8007-b27294ad5af1\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",162.99999920631308,\"~:proportion\",1,\"~:r4\",20,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",162.99999920631308,\"~:y\",-22.00000122055286,\"^8\",445.0000023918125,\"~:height\",445.0000194217354,\"~:x1\",162.99999920631308,\"~:y1\",-22.00000122055286,\"~:x2\",608.0000015981256,\"~:y2\",423.0000182011825]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffffff\",\"~:fill-opacity\",0.30392156862745096]],\"~:flip-x\",null,\"^N\",445.0000194217354,\"~:flip-y\",null]]",
"~u11813dac-4dc3-80d8-8007-b2804a521773": "[\"~#shape\",[\"^ \",\"~:y\",49.00000846385956,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:index\",2,\"~:name\",\"Group\",\"~:width\",303,\"~:type\",\"~:group\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",49.00000846385956]],[\"^:\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",49.00000846385956]],[\"^:\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",352.00000846385956]],[\"^:\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",352.00000846385956]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b2804a521773\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",234.00000047683716,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",49.00000846385956,\"^6\",303,\"~:height\",303,\"~:x1\",234.00000047683716,\"~:y1\",49.00000846385956,\"~:x2\",537.0000004768372,\"~:y2\",352.00000846385956]],\"~:fills\",[],\"~:flip-x\",null,\"^D\",303,\"~:flip-y\",null,\"~:shapes\",[\"~u5b58e018-fa5e-805d-8007-b27288b0cd80\",\"~u5b58e018-fa5e-805d-8007-b2728b102fe6\"]]]",
"~u11813dac-4dc3-80d8-8007-b2806dfde19c": "[\"~#shape\",[\"^ \",\"~:y\",189.99999856948853,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",206,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1307.9999368190765,\"~:y\",189.99999856948853]],[\"^<\",[\"^ \",\"~:x\",1513.9999368190765,\"~:y\",189.99999856948853]],[\"^<\",[\"^ \",\"~:x\",1513.9999368190765,\"~:y\",396.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1307.9999368190765,\"~:y\",396.9999985694885]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u11813dac-4dc3-80d8-8007-b2806dfde19c\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b2806dfd8a36\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1307.9999368190765,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1307.9999368190765,\"~:y\",189.99999856948853,\"^8\",206,\"~:height\",207,\"~:x1\",1307.9999368190765,\"~:y1\",189.99999856948853,\"~:x2\",1513.9999368190765,\"~:y2\",396.9999985694885]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#c8f00d\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",207,\"~:flip-y\",null]]",
"~u11813dac-4dc3-80d8-8007-b2806dfde19d": "[\"~#shape\",[\"^ \",\"~:y\",-32.00000238488809,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",302.9999596227117,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1307.9999330492521,\"~:y\",-32.00000238488809]],[\"^<\",[\"^ \",\"~:x\",1610.9998926719638,\"~:y\",-32.00000238488809]],[\"^<\",[\"^ \",\"~:x\",1610.9998926719638,\"~:y\",268.00000735984116]],[\"^<\",[\"^ \",\"~:x\",1307.9999330492521,\"~:y\",268.00000735984116]]],\"~:r2\",20,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",20,\"~:blur\",[\"^ \",\"~:id\",\"~u5b58e018-fa5e-805d-8007-b2729af5ed71\",\"^9\",\"~:background-blur\",\"~:value\",4,\"~:hidden\",false],\"~:r1\",20,\"^B\",\"~u11813dac-4dc3-80d8-8007-b2806dfde19d\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1307.9999330492521,\"~:proportion\",1,\"~:r4\",20,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1307.9999330492521,\"~:y\",-32.00000238488809,\"^8\",302.9999596227117,\"~:height\",300.00000974472925,\"~:x1\",1307.9999330492521,\"~:y1\",-32.00000238488809,\"~:x2\",1610.9998926719638,\"~:y2\",268.00000735984116]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffffff\",\"~:fill-opacity\",0.30392156862745096]],\"~:flip-x\",null,\"^N\",300.00000974472925,\"~:flip-y\",null]]",
"~u11813dac-4dc3-80d8-8007-b280a6cd14a4": "[\"~#shape\",[\"^ \",\"~:y\",592.0000023841858,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:index\",2,\"~:name\",\"Group\",\"~:width\",303,\"~:type\",\"~:group\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",592.0000023841858]],[\"^:\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",592.0000023841858]],[\"^:\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",895.0000023841858]],[\"^:\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",895.0000023841858]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a4\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",817.9999825954437,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",592.0000023841858,\"^6\",303,\"~:height\",303,\"~:x1\",817.9999825954437,\"~:y1\",592.0000023841858,\"~:x2\",1120.9999825954437,\"~:y2\",895.0000023841858]],\"~:fills\",[],\"~:flip-x\",null,\"^D\",303,\"~:flip-y\",null,\"~:shapes\",[\"~u11813dac-4dc3-80d8-8007-b280a6cd14a5\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a6\"]]]",
"~u11813dac-4dc3-80d8-8007-b28053db9864": "[\"~#shape\",[\"^ \",\"~:y\",97.00000524450644,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",299.9999982638824,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",770.9999871603877,\"~:y\",97.00000524450644]],[\"^<\",[\"^ \",\"~:x\",1070.99998542427,\"~:y\",97.00000524450644]],[\"^<\",[\"^ \",\"~:x\",1070.99998542427,\"~:y\",397.0000149892357]],[\"^<\",[\"^ \",\"~:x\",770.9999871603877,\"~:y\",397.0000149892357]]],\"~:r2\",20,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",20,\"~:blur\",[\"^ \",\"~:id\",\"~u5b58e018-fa5e-805d-8007-b2729af5ed71\",\"^9\",\"~:background-blur\",\"~:value\",4,\"~:hidden\",false],\"~:r1\",20,\"^B\",\"~u11813dac-4dc3-80d8-8007-b28053db9864\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",770.9999871603877,\"~:proportion\",1,\"~:r4\",20,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",770.9999871603877,\"~:y\",97.00000524450644,\"^8\",299.9999982638824,\"~:height\",300.00000974472925,\"~:x1\",770.9999871603877,\"~:y1\",97.00000524450644,\"~:x2\",1070.99998542427,\"~:y2\",397.0000149892357]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffffff\",\"~:fill-opacity\",0.30392156862745096]],\"~:flip-x\",null,\"^N\",300.00000974472925,\"~:flip-y\",null]]",
"~u5b58e018-fa5e-805d-8007-b2728b102fe6": "[\"~#shape\",[\"^ \",\"~:y\",145.00000846385956,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",206,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",145.00000846385956]],[\"^<\",[\"^ \",\"~:x\",440.00000047683716,\"~:y\",145.00000846385956]],[\"^<\",[\"^ \",\"~:x\",440.00000047683716,\"~:y\",352.00000846385956]],[\"^<\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",352.00000846385956]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u5b58e018-fa5e-805d-8007-b2728b102fe6\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b2804a521773\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",234.00000047683716,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",145.00000846385956,\"^8\",206,\"~:height\",207,\"~:x1\",234.00000047683716,\"~:y1\",145.00000846385956,\"~:x2\",440.00000047683716,\"~:y2\",352.00000846385956]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#c8f00d\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",207,\"~:flip-y\",null]]",
"~u11813dac-4dc3-80d8-8007-b280a6cd14a5": "[\"~#shape\",[\"^ \",\"~:y\",592.0000023841858,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",194,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",926.9999825954437,\"~:y\",592.0000023841858]],[\"^<\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",592.0000023841858]],[\"^<\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",792.0000023841858]],[\"^<\",[\"^ \",\"~:x\",926.9999825954437,\"~:y\",792.0000023841858]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a5\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a4\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",926.9999825954437,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",926.9999825954437,\"~:y\",592.0000023841858,\"^8\",194,\"~:height\",200,\"~:x1\",926.9999825954437,\"~:y1\",592.0000023841858,\"~:x2\",1120.9999825954437,\"~:y2\",792.0000023841858]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#174be9\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^F\",200,\"~:flip-y\",null]]",
"~u11813dac-4dc3-80d8-8007-b28053db9865": "[\"~#shape\",[\"^ \",\"~:y\",48.00000238418579,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:index\",2,\"~:name\",\"Group\",\"~:width\",303,\"~:type\",\"~:group\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",48.00000238418579]],[\"^:\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",48.00000238418579]],[\"^:\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",351.0000023841858]],[\"^:\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",351.0000023841858]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b28053db9865\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",817.9999825954437,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",48.00000238418579,\"^6\",303,\"~:height\",303,\"~:x1\",817.9999825954437,\"~:y1\",48.00000238418579,\"~:x2\",1120.9999825954437,\"~:y2\",351.0000023841858]],\"~:fills\",[],\"~:flip-x\",null,\"^D\",303,\"~:flip-y\",null,\"~:shapes\",[\"~u11813dac-4dc3-80d8-8007-b28053db9866\",\"~u11813dac-4dc3-80d8-8007-b28053db9867\"]]]",
"~u11813dac-4dc3-80d8-8007-b280a6cd14a6": "[\"~#shape\",[\"^ \",\"~:y\",688.0000023841858,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",206,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",688.0000023841858]],[\"^<\",[\"^ \",\"~:x\",1023.9999825954437,\"~:y\",688.0000023841858]],[\"^<\",[\"^ \",\"~:x\",1023.9999825954437,\"~:y\",895.0000023841858]],[\"^<\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",895.0000023841858]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a6\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a4\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",817.9999825954437,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",688.0000023841858,\"^8\",206,\"~:height\",207,\"~:x1\",817.9999825954437,\"~:y1\",688.0000023841858,\"~:x2\",1023.9999825954437,\"~:y2\",895.0000023841858]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#c8f00d\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",207,\"~:flip-y\",null]]",
"~u11813dac-4dc3-80d8-8007-b28053db9866": "[\"~#shape\",[\"^ \",\"~:y\",48.00000238418579,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",194,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",926.9999825954437,\"~:y\",48.00000238418579]],[\"^<\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",48.00000238418579]],[\"^<\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",248.0000023841858]],[\"^<\",[\"^ \",\"~:x\",926.9999825954437,\"~:y\",248.0000023841858]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b28053db9866\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b28053db9865\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",926.9999825954437,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",926.9999825954437,\"~:y\",48.00000238418579,\"^8\",194,\"~:height\",200,\"~:x1\",926.9999825954437,\"~:y1\",48.00000238418579,\"~:x2\",1120.9999825954437,\"~:y2\",248.0000023841858]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#174be9\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^F\",200,\"~:flip-y\",null]]",
"~u11813dac-4dc3-80d8-8007-b280a6cd14a7": "[\"~#shape\",[\"^ \",\"~:y\",637.9999985694885,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:index\",2,\"~:name\",\"Group\",\"~:width\",303,\"~:type\",\"~:group\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1308.000058889389,\"~:y\",637.9999985694885]],[\"^:\",[\"^ \",\"~:x\",1611.000058889389,\"~:y\",637.9999985694885]],[\"^:\",[\"^ \",\"~:x\",1611.000058889389,\"~:y\",940.9999985694885]],[\"^:\",[\"^ \",\"~:x\",1308.000058889389,\"~:y\",940.9999985694885]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a7\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1308.000058889389,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1308.000058889389,\"~:y\",637.9999985694885,\"^6\",303,\"~:height\",303,\"~:x1\",1308.000058889389,\"~:y1\",637.9999985694885,\"~:x2\",1611.000058889389,\"~:y2\",940.9999985694885]],\"~:fills\",[],\"~:flip-x\",null,\"^D\",303,\"~:flip-y\",null,\"~:shapes\",[\"~u11813dac-4dc3-80d8-8007-b280a6cd14a8\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a9\"]]]",
"~u11813dac-4dc3-80d8-8007-b28053db9867": "[\"~#shape\",[\"^ \",\"~:y\",144.0000023841858,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",206,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",144.0000023841858]],[\"^<\",[\"^ \",\"~:x\",1023.9999825954437,\"~:y\",144.0000023841858]],[\"^<\",[\"^ \",\"~:x\",1023.9999825954437,\"~:y\",351.0000023841858]],[\"^<\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",351.0000023841858]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u11813dac-4dc3-80d8-8007-b28053db9867\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b28053db9865\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",817.9999825954437,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",144.0000023841858,\"^8\",206,\"~:height\",207,\"~:x1\",817.9999825954437,\"~:y1\",144.0000023841858,\"~:x2\",1023.9999825954437,\"~:y2\",351.0000023841858]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#c8f00d\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",207,\"~:flip-y\",null]]",
"~u5b58e018-fa5e-805d-8007-b27288b0cd80": "[\"~#shape\",[\"^ \",\"~:y\",49.00000846385956,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",194,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",343.00000047683716,\"~:y\",49.00000846385956]],[\"^<\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",49.00000846385956]],[\"^<\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",249.00000846385956]],[\"^<\",[\"^ \",\"~:x\",343.00000047683716,\"~:y\",249.00000846385956]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u5b58e018-fa5e-805d-8007-b27288b0cd80\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b2804a521773\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",343.00000047683716,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",343.00000047683716,\"~:y\",49.00000846385956,\"^8\",194,\"~:height\",200,\"~:x1\",343.00000047683716,\"~:y1\",49.00000846385956,\"~:x2\",537.0000004768372,\"~:y2\",249.00000846385956]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#174be9\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^F\",200,\"~:flip-y\",null]]",
"~u11813dac-4dc3-80d8-8007-b280a6cd14ac": "[\"~#shape\",[\"^ \",\"~:y\",593.0000084638596,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:index\",2,\"~:name\",\"Group\",\"~:width\",303,\"~:type\",\"~:group\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",593.0000084638596]],[\"^:\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",593.0000084638596]],[\"^:\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",896.0000084638596]],[\"^:\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",896.0000084638596]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ac\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",234.00000047683716,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",593.0000084638596,\"^6\",303,\"~:height\",303,\"~:x1\",234.00000047683716,\"~:y1\",593.0000084638596,\"~:x2\",537.0000004768372,\"~:y2\",896.0000084638596]],\"~:fills\",[],\"~:flip-x\",null,\"^D\",303,\"~:flip-y\",null,\"~:shapes\",[\"~u11813dac-4dc3-80d8-8007-b280a6cd14ad\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ae\"]]]",
"~u11813dac-4dc3-80d8-8007-b280a6cd14ad": "[\"~#shape\",[\"^ \",\"~:y\",593.0000084638596,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",194,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",343.00000047683716,\"~:y\",593.0000084638596]],[\"^<\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",593.0000084638596]],[\"^<\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",793.0000084638596]],[\"^<\",[\"^ \",\"~:x\",343.00000047683716,\"~:y\",793.0000084638596]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ad\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ac\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",343.00000047683716,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",343.00000047683716,\"~:y\",593.0000084638596,\"^8\",194,\"~:height\",200,\"~:x1\",343.00000047683716,\"~:y1\",593.0000084638596,\"~:x2\",537.0000004768372,\"~:y2\",793.0000084638596]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#174be9\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^F\",200,\"~:flip-y\",null]]",
"~u11813dac-4dc3-80d8-8007-b280a6cd14ae": "[\"~#shape\",[\"^ \",\"~:y\",689.0000084638596,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",206,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",689.0000084638596]],[\"^<\",[\"^ \",\"~:x\",440.00000047683716,\"~:y\",689.0000084638596]],[\"^<\",[\"^ \",\"~:x\",440.00000047683716,\"~:y\",896.0000084638596]],[\"^<\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",896.0000084638596]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ae\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ac\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",234.00000047683716,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",689.0000084638596,\"^8\",206,\"~:height\",207,\"~:x1\",234.00000047683716,\"~:y1\",689.0000084638596,\"~:x2\",440.00000047683716,\"~:y2\",896.0000084638596]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#c8f00d\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",207,\"~:flip-y\",null]]",
"~u11813dac-4dc3-80d8-8007-b280a6cd14af": "[\"~#shape\",[\"^ \",\"~:y\",512.0000283168957,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",445.0000023918125,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",157.99998871589563,\"~:y\",512.0000283168957]],[\"^<\",[\"^ \",\"~:x\",602.9999911077081,\"~:y\",512.0000283168957]],[\"^<\",[\"^ \",\"~:x\",602.9999911077081,\"~:y\",957.000047738631]],[\"^<\",[\"^ \",\"~:x\",157.99998871589563,\"~:y\",957.000047738631]]],\"~:r2\",20,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",20,\"~:blur\",[\"^ \",\"~:id\",\"~u5b58e018-fa5e-805d-8007-b2729af5ed71\",\"^9\",\"~:background-blur\",\"~:value\",50,\"~:hidden\",false],\"~:r1\",20,\"^B\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14af\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",157.9999887158956,\"~:proportion\",1,\"~:r4\",20,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",157.9999887158956,\"~:y\",512.0000283168957,\"^8\",445.0000023918125,\"~:height\",445.00001942173526,\"~:x1\",157.9999887158956,\"~:y1\",512.0000283168957,\"~:x2\",602.9999911077081,\"~:y2\",957.000047738631]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffffff\",\"~:fill-opacity\",0.30392156862745096]],\"~:flip-x\",null,\"^N\",445.00001942173526,\"~:flip-y\",null]]",
"~u11813dac-4dc3-80d8-8007-b280a6cd14a8": "[\"~#shape\",[\"^ \",\"~:y\",637.9999985694885,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",194,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1417.000058889389,\"~:y\",637.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1611.000058889389,\"~:y\",637.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1611.000058889389,\"~:y\",837.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1417.000058889389,\"~:y\",837.9999985694885]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a8\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a7\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1417.000058889389,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1417.000058889389,\"~:y\",637.9999985694885,\"^8\",194,\"~:height\",200,\"~:x1\",1417.000058889389,\"~:y1\",637.9999985694885,\"~:x2\",1611.000058889389,\"~:y2\",837.9999985694885]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#174be9\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^F\",200,\"~:flip-y\",null]]",
"~u11813dac-4dc3-80d8-8007-b280a6cd14a9": "[\"~#shape\",[\"^ \",\"~:y\",733.9999985694885,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",206,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1308.000058889389,\"~:y\",733.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1514.000058889389,\"~:y\",733.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1514.000058889389,\"~:y\",940.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1308.000058889389,\"~:y\",940.9999985694885]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a9\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a7\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1308.000058889389,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1308.000058889389,\"~:y\",733.9999985694885,\"^8\",206,\"~:height\",207,\"~:x1\",1308.000058889389,\"~:y1\",733.9999985694885,\"~:x2\",1514.000058889389,\"~:y2\",940.9999985694885]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#c8f00d\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",207,\"~:flip-y\",null]]",
"~u11813dac-4dc3-80d8-8007-b280a6cd14aa": "[\"~#shape\",[\"^ \",\"~:y\",511.9999976151119,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",302.9999596227117,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1308.0000551195646,\"~:y\",511.9999976151119]],[\"^<\",[\"^ \",\"~:x\",1611.0000147422763,\"~:y\",511.9999976151119]],[\"^<\",[\"^ \",\"~:x\",1611.0000147422763,\"~:y\",812.0000073598412]],[\"^<\",[\"^ \",\"~:x\",1308.0000551195646,\"~:y\",812.0000073598412]]],\"~:r2\",20,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",20,\"~:blur\",[\"^ \",\"~:id\",\"~u5b58e018-fa5e-805d-8007-b2729af5ed71\",\"^9\",\"~:background-blur\",\"~:value\",50,\"~:hidden\",false],\"~:r1\",20,\"^B\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14aa\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1308.0000551195646,\"~:proportion\",1,\"~:r4\",20,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1308.0000551195646,\"~:y\",511.9999976151119,\"^8\",302.9999596227117,\"~:height\",300.0000097447293,\"~:x1\",1308.0000551195646,\"~:y1\",511.9999976151119,\"~:x2\",1611.0000147422763,\"~:y2\",812.0000073598412]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffffff\",\"~:fill-opacity\",0.30392156862745096]],\"~:flip-x\",null,\"^N\",300.0000097447293,\"~:flip-y\",null]]",
"~u11813dac-4dc3-80d8-8007-b280a6cd14ab": "[\"~#shape\",[\"^ \",\"~:y\",512.00002813269,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",299.9999982638824,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",743.9999566428096,\"~:y\",512.00002813269]],[\"^<\",[\"^ \",\"~:x\",1043.999954906692,\"~:y\",512.00002813269]],[\"^<\",[\"^ \",\"~:x\",1043.999954906692,\"~:y\",812.0000378774193]],[\"^<\",[\"^ \",\"~:x\",743.9999566428096,\"~:y\",812.0000378774193]]],\"~:r2\",20,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",20,\"~:blur\",[\"^ \",\"~:id\",\"~u5b58e018-fa5e-805d-8007-b2729af5ed71\",\"^9\",\"~:background-blur\",\"~:value\",50,\"~:hidden\",false],\"~:r1\",20,\"^B\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ab\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",743.9999566428096,\"~:proportion\",1,\"~:r4\",20,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",743.9999566428096,\"~:y\",512.00002813269,\"^8\",299.9999982638824,\"~:height\",300.00000974472937,\"~:x1\",743.9999566428096,\"~:y1\",512.00002813269,\"~:x2\",1043.999954906692,\"~:y2\",812.0000378774193]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffffff\",\"~:fill-opacity\",0.30392156862745096]],\"~:flip-x\",null,\"^N\",300.00000974472937,\"~:flip-y\",null]]"
}
},
"~:id": "~u93bfc923-66b2-813c-8007-b2725507ba09",
"~:name": "Page 1"
}
},
"~:id": "~u93bfc923-66b2-813c-8007-b2725507ba08",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@ -215,6 +215,7 @@ export class WorkspacePage extends BaseWebSocketPage {
async goToWorkspace({
fileId = this.fileId ?? WorkspacePage.anyFileId,
pageId = this.pageId ?? WorkspacePage.anyPageId,
pageName = "Page 1",
} = {}) {
await this.page.goto(
`/#/workspace?team-id=${WorkspacePage.anyTeamId}&file-id=${fileId}&page-id=${pageId}`,
@ -222,12 +223,12 @@ export class WorkspacePage extends BaseWebSocketPage {
this.#ws = await this.waitForNotificationsWebSocket();
await this.#ws.mockOpen();
await this.#waitForWebSocketReadiness();
await this.#waitForWebSocketReadiness(pageName);
}
async #waitForWebSocketReadiness() {
async #waitForWebSocketReadiness(pageName) {
// TODO: find a better event to settle whether the app is ready to receive notifications via ws
await expect(this.pageName).toHaveText("Page 1", { timeout: 30000 });
await expect(this.pageName).toHaveText(pageName, { timeout: 30000 })
}
async sendPresenceMessage(fixture) {

View File

@ -545,5 +545,22 @@ test("BUG 13610 - Huge inner strokes", async ({
pageId: "effcbebc-b8c8-802f-8007-b11dd34fe191",
});
await workspace.waitForFirstRenderWithoutUI();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders background blur on shapes overlapping other shapes", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-background-blur.json");
await workspace.goToWorkspace({
id: "93bfc923-66b2-813c-8007-b2725507ba08",
pageId: "93bfc923-66b2-813c-8007-b2725507ba09",
});
await workspace.waitForFirstRenderWithoutUI();
await expect(workspace.canvas).toHaveScreenshot();
});

View File

@ -231,6 +231,90 @@ test("BUG 9061 - Group blur visibility toggle icon not updating", async ({
await expect(blurIcon).toHaveAttribute("href", "#icon-hide");
});
test.describe("Background blur", () => {
test("Shows background blur option in blur type select when both render-wasm and background-blur flags are active", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.mockConfigFlags(["enable-background-blur"]);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-background-blur.json");
await workspace.goToWorkspace({
fileId: "93bfc923-66b2-813c-8007-b2725507ba08",
pageId: "93bfc923-66b2-813c-8007-b2725507ba09",
});
// Click the first Rectangle (which has background-blur type)
await workspace.clickLeafLayer("Rectangle");
// The blur type select should show "Background blur" as the current value
const blurTypeSelect = workspace.page
.getByTestId("blur-info")
.getByRole("combobox");
await expect(blurTypeSelect).toBeVisible();
await expect(blurTypeSelect).toContainText("Background blur");
});
test("Shows both layer-blur and background-blur options in the blur type dropdown", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.mockConfigFlags(["enable-background-blur"]);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-background-blur.json");
await workspace.goToWorkspace({
fileId: "93bfc923-66b2-813c-8007-b2725507ba08",
pageId: "93bfc923-66b2-813c-8007-b2725507ba09",
});
await workspace.clickLeafLayer("Rectangle");
// Open the blur type dropdown
const blurTypeSelect = workspace.page
.getByTestId("blur-info")
.getByRole("combobox");
await blurTypeSelect.click();
// Both options should be visible
const layerBlurOption = workspace.page.getByRole("option", {
name: "Layer blur",
});
const backgroundBlurOption = workspace.page.getByRole("option", {
name: "Background blur",
});
await expect(layerBlurOption).toBeVisible();
await expect(backgroundBlurOption).toBeVisible();
});
test("Does not show background blur option when background-blur flag is not active", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
// No enable-background-blur flag
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-background-blur.json");
await workspace.goToWorkspace({
fileId: "93bfc923-66b2-813c-8007-b2725507ba08",
pageId: "93bfc923-66b2-813c-8007-b2725507ba09",
});
await workspace.clickLeafLayer("Rectangle");
// Without the background-blur flag, no blur type dropdown should appear.
// Instead, a plain "Blur" label is shown.
const blurTypeSelect = workspace.page
.getByTestId("blur-info")
.getByRole("combobox");
await expect(blurTypeSelect).not.toBeVisible();
const blurLabel = workspace.page.getByTestId("blur-info").getByText("Blur");
await expect(blurLabel).toBeVisible();
});
});
test("BUG 9543 - Layout padding inputs not showing 'mixed' when needed", async ({
page,
}) => {

View File

@ -58,6 +58,14 @@
[]
(mf/render! app-root (mf/element ui/app)))
(defn- initialize-rasterizer
[]
(ptk/reify ::initialize-rasterizer
ptk/EffectEvent
(effect [_ state _]
(when (feat/active-feature? state "render-wasm/v1")
(thr/init!)))))
(defn initialize
[]
(ptk/reify ::initialize
@ -93,12 +101,12 @@
(rx/map deref)
(rx/filter dp/is-authenticated?)
(rx/take 1)
(rx/map #(ws/initialize)))))
(rx/map #(ws/initialize)))
ptk/EffectEvent
(effect [_ state _]
(when-not (feat/active-feature? state "render-wasm/v1")
(thr/init!)))))
(->> stream
(rx/filter (ptk/type? ::feat/initialize))
(rx/take 1)
(rx/map #(initialize-rasterizer)))))))
(defn ^:export init
[options]

View File

@ -41,6 +41,7 @@
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.texts :as dwtxt]
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.wasm-text :as dwwt]
[app.main.errors]
[app.main.features :as features]
[app.main.refs :as refs]
@ -970,10 +971,11 @@
text (.-textContent root)
content (tc/dom->cljs root)]
(when (types.text/valid-content? content)
(let [id (uuid/next)
width (max 8 (min (* 7 (count text)) 700))
height 16
(let [id (uuid/next)
width (max 8 (min (* 7 (count text)) 700))
height 16
{:keys [x y]} (calculate-paste-position state)
skip-edition? (features/active-feature? state "text-editor-wasm/v1")
shape {:id id
:type :text
@ -985,9 +987,14 @@
:grow-type (if (> (count text) 100) :auto-height :auto-width)
:content content}
undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id)
(dwsh/create-and-add-shape :text x y shape)
(dwu/commit-undo-transaction undo-id))))))))
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id)
(dwsh/create-and-add-shape :text x y shape
(when skip-edition? {:skip-edition? true})))
(if skip-edition?
(rx/of (dwwt/resize-wasm-text-debounce id {:undo-group id
:undo-id undo-id}))
(rx/of (dwu/commit-undo-transaction undo-id))))))))))
(defn- paste-text
[text]
@ -995,10 +1002,11 @@
(ptk/reify ::paste-text
ptk/WatchEvent
(watch [_ state _]
(let [id (uuid/next)
width (max 8 (min (* 7 (count text)) 700))
height 16
(let [id (uuid/next)
width (max 8 (min (* 7 (count text)) 700))
height 16
{:keys [x y]} (calculate-paste-position state)
skip-edition? (features/active-feature? state "text-editor-wasm/v1")
shape {:id id
:type :text
@ -1011,9 +1019,14 @@
:content (as-content text)}
undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id)
(dwsh/create-and-add-shape :text x y shape)
(dwu/commit-undo-transaction undo-id))))))
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id)
(dwsh/create-and-add-shape :text x y shape
(when skip-edition? {:skip-edition? true})))
(if skip-edition?
(rx/of (dwwt/resize-wasm-text-debounce id {:undo-group id
:undo-id undo-id}))
(rx/of (dwu/commit-undo-transaction undo-id))))))))
;; TODO: why not implement it in terms of upload-media-workspace?
(defn- paste-svg-text

View File

@ -53,21 +53,28 @@
(rx/take 1)
(rx/map #(ptk/data-event ::connect))))))
(defn manage-notification
[mcp-enabled? mcp-connected?]
(if mcp-enabled?
(if mcp-connected?
(rx/of (ntf/hide))
(rx/of (ntf/dialog :content (tr "notifications.mcp.active-tab-switching.text")
:cancel {:label (tr "labels.dismiss")
:callback #(st/emit! (ntf/hide)
(ptk/event ::ev/event {::ev/name "confirm-mcp-tab-switch"
::ev/origin "workspace-notification"}))}
:accept {:label (tr "labels.switch")
:callback #(st/emit! (connect-mcp)
(ptk/event ::ev/event {::ev/name "dismiss-mcp-tab-switch"
::ev/origin "workspace-notification"}))})))
(rx/of (ntf/hide))))
(defn manage-mcp-notification
[]
(ptk/reify ::manage-mcp-notification
ptk/WatchEvent
(watch [_ state _]
(let [mcp-connected? (true? (-> state :workspace-local :mcp :connection))
mcp-enabled? (true? (-> state :profile :props :mcp-enabled))
num-sessions (-> state :workspace-presence count)
multi-session? (> num-sessions 1)]
(if (and mcp-enabled? multi-session?)
(if mcp-connected?
(rx/of (ntf/hide))
(rx/of (ntf/dialog :content (tr "notifications.mcp.active-in-another-tab")
:cancel {:label (tr "labels.dismiss")
:callback #(st/emit! (ntf/hide)
(ptk/event ::ev/event {::ev/name "confirm-mcp-tab-switch"
::ev/origin "workspace-notification"}))}
:accept {:label (tr "labels.switch")
:callback #(st/emit! (connect-mcp)
(ptk/event ::ev/event {::ev/name "dismiss-mcp-tab-switch"
::ev/origin "workspace-notification"}))})))
(rx/of (ntf/hide)))))))
(defn update-mcp-status
[value]
@ -77,26 +84,24 @@
(update-in state [:profile :props] assoc :mcp-enabled value))
ptk/WatchEvent
(watch [_ state _]
(watch [_ _ _]
(rx/merge
(let [mcp-connected? (-> state :workspace-local :mcp :connected)]
(manage-notification value mcp-connected?))
(case value
true (rx/of (ptk/data-event ::connect))
false (rx/of (ptk/data-event ::disconnect))
nil)))))
(rx/of (manage-mcp-notification)))
(case value
true (rx/of (ptk/data-event ::connect))
false (rx/of (ptk/data-event ::disconnect))
nil))))
(defn update-mcp-connection
[value]
(ptk/reify ::update-mcp-plugin-connection
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-local :mcp] assoc :connected value))
(update-in state [:workspace-local :mcp] assoc :connection value))
ptk/WatchEvent
(watch [_ state _]
(let [mcp-enabled? (-> state :profile :props :mcp-enabled)]
(manage-notification mcp-enabled? value)))))
(watch [_ _ _]
(rx/of (manage-mcp-notification)))))
(defn init-mcp!
[stream]
@ -116,11 +121,12 @@
:getServerUrl #(str cf/mcp-ws-uri)
:setMcpStatus
(fn [status]
(let [mcp-connected? (case status
(let [mcp-connection (case status
"connected" true
"disconnected" false
nil)]
(st/emit! (update-mcp-connection mcp-connected?))
"error" nil
"")]
(st/emit! (update-mcp-connection mcp-connection))
(log/info :hint "MCP STATUS" :status status)))
:on

View File

@ -23,6 +23,7 @@
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.layout :as dwly]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.mcp :as mcp]
[app.main.data.workspace.texts :as dwt]
[app.main.router :as rt]
[app.util.globals :refer [global]]
@ -212,7 +213,11 @@
(update [_ state]
(if (or (= :disconnect type) (= :leave-file type))
(update state :workspace-presence dissoc session-id)
(update state :workspace-presence update-presence))))))
(update state :workspace-presence update-presence)))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (mcp/manage-mcp-notification))))))
(defn handle-pointer-update
[{:keys [page-id session-id position zoom zoom-inverse vbox vport] :as msg}]

View File

@ -109,7 +109,7 @@
(defn add-shape
([shape]
(add-shape shape {}))
([shape {:keys [no-select? no-update-layout?]}]
([shape {:keys [no-select? no-update-layout? skip-edition?]}]
(cts/check-shape shape)
@ -139,7 +139,11 @@
(js/Symbol)
parent-type
(cfh/get-shape-type objects (:parent-id shape))]
(cfh/get-shape-type objects (:parent-id shape))
;; Skip edition when using embedded editor (v3) and shape already has content (e.g. paste)
start-edition? (and (cfh/text-shape? shape)
(not (and skip-edition? (some? (:content shape)))))]
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id)
@ -149,7 +153,7 @@
(when-not no-select?
(dws/select-shapes (d/ordered-set (:id shape))))
(dwu/commit-undo-transaction undo-id))
(when (cfh/text-shape? shape)
(when start-edition?
(->> (rx/of (dwe/start-edition-mode (:id shape)))
(rx/observe-on :async)))
@ -217,40 +221,42 @@
(dwu/commit-undo-transaction undo-id)))))))
(defn create-and-add-shape
[type frame-x frame-y {:keys [width height] :as attrs}]
(ptk/reify ::create-and-add-shape
ptk/WatchEvent
(watch [_ state _]
(let [vbc (dsh/get-viewport-center state)
x (:x attrs (- (:x vbc) (/ width 2)))
y (:y attrs (- (:y vbc) (/ height 2)))
page-id (:current-page-id state)
objects (dsh/lookup-page-objects state page-id)
frame-id (-> (dsh/lookup-page-objects state page-id)
(ctst/top-nested-frame {:x frame-x :y frame-y}))
([type frame-x frame-y attrs]
(create-and-add-shape type frame-x frame-y attrs nil))
([type frame-x frame-y {:keys [width height] :as attrs} {:keys [skip-edition?]}]
(ptk/reify ::create-and-add-shape
ptk/WatchEvent
(watch [_ state _]
(let [vbc (dsh/get-viewport-center state)
x (:x attrs (- (:x vbc) (/ width 2)))
y (:y attrs (- (:y vbc) (/ height 2)))
page-id (:current-page-id state)
objects (dsh/lookup-page-objects state page-id)
frame-id (-> (dsh/lookup-page-objects state page-id)
(ctst/top-nested-frame {:x frame-x :y frame-y}))
selected (dsh/lookup-selected state)
base (cfh/get-base-shape objects selected)
selected (dsh/lookup-selected state)
base (cfh/get-base-shape objects selected)
parent-id (if (or (and (= 1 (count selected))
(cfh/frame-shape? (get objects (first selected))))
(empty? selected))
frame-id
(:parent-id base))
parent-id (if (or (and (= 1 (count selected))
(cfh/frame-shape? (get objects (first selected))))
(empty? selected))
frame-id
(:parent-id base))
;; If the parent-id or the frame-id are component-copies, we need to get the first not copy parent
parent-id (:id (ctn/get-first-valid-parent objects parent-id)) ;; We don't want to change the structure of component copies
frame-id (:id (ctn/get-first-valid-parent objects frame-id))
;; If the parent-id or the frame-id are component-copies, we need to get the first not copy parent
parent-id (:id (ctn/get-first-valid-parent objects parent-id)) ;; We don't want to change the structure of component copies
frame-id (:id (ctn/get-first-valid-parent objects frame-id))
shape (cts/setup-shape
(-> attrs
(assoc :type type)
(assoc :x x)
(assoc :y y)
(assoc :frame-id frame-id)
(assoc :parent-id parent-id)))]
shape (cts/setup-shape
(-> attrs
(assoc :type type)
(assoc :x x)
(assoc :y y)
(assoc :frame-id frame-id)
(assoc :parent-id parent-id)))]
(rx/of (add-shape shape))))))
(rx/of (add-shape shape {:skip-edition? skip-edition?})))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Artboard

View File

@ -966,7 +966,11 @@
ptk/WatchEvent
(watch [it state _]
(if (features/active-feature? state "render-wasm/v1")
(let [objects (dsh/lookup-page-objects state)
(let [;; v3 editor always passes :finalize? from keyword opts; when absent
;; that binds nil and :or defaults do not apply — coerce so undo flags
;; stay strict booleans for changes-builder schema validation.
finalize? (boolean finalize?)
objects (dsh/lookup-page-objects state)
shape (get objects id)
new-shape? (contains? (:workspace-new-text-shapes state) id)
prev-content (:content shape)
@ -978,8 +982,7 @@
(select-keys shape [:selrect :points :width :height]))
content-has-text? (v2-content-has-text? content)
prev-content-has-text? (v2-content-has-text? prev-content)
new-size (when (and (not= :fixed (:grow-type shape))
content-has-text?)
new-size (when (not= :fixed (:grow-type shape))
(dwwt/get-wasm-text-new-size shape content))
;; New shapes: single undo on finalize only (no per-keystroke undo)
effective-save-undo? (if new-shape? finalize? save-undo?)

View File

@ -49,14 +49,14 @@
;; (note that dwsh/update-shapes function returns an event)
(defn update-shape-radius-all
([value shape-ids attributes] (update-shape-radius-all value shape-ids attributes nil))
([value shape-ids _attributes page-id] ; The attributes param is needed to have the same arity that other update functions
(defn update-shape-radius
([value shape-ids attributes] (update-shape-radius value shape-ids attributes nil))
([value shape-ids attributes page-id]
(when (number? value)
(let [value (max 0 value)]
(dwsh/update-shapes shape-ids
(fn [shape]
(ctsr/set-radius-to-all-corners shape value))
(ctsr/set-radius-for-corners shape attributes value))
{:reg-objects? true
:ignore-touched true
:page-id page-id
@ -531,7 +531,7 @@
(some attributes #{:r1 :r2 :r3 :r4})
(conj #(if (= attributes #{:r1 :r2 :r3 :r4})
(update-shape-radius-all value shape-ids attributes page-id)
(update-shape-radius value shape-ids attributes page-id)
(update-shape-radius-for-corners
value shape-ids
(set (filter attributes #{:r1 :r2 :r3 :r4}))
@ -862,7 +862,7 @@
:border-radius
{:title "Border Radius"
:attributes ctt/border-radius-keys
:on-update-shape update-shape-radius-all
:on-update-shape update-shape-radius
:modal {:key :tokens/border-radius
:fields [{:label "Border Radius"
:key :border-radius}]}}

View File

@ -17,6 +17,7 @@
[app.common.types.modifiers :as ctm]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.modifiers :as dwm]
[app.main.data.workspace.undo :as dwu]
[app.render-wasm.api :as wasm.api]
[app.render-wasm.api.fonts :as wasm.fonts]
[beicon.v2.core :as rx]
@ -76,82 +77,115 @@
(rx/empty))))))
(defn resize-wasm-text-debounce-commit
[]
(ptk/reify ::resize-wasm-text-debounce-commit
ptk/WatchEvent
(watch [_ state _]
(let [ids (get state ::resize-wasm-text-debounce-ids)
objects (dsh/lookup-page-objects state)
([]
(resize-wasm-text-debounce-commit nil nil))
([undo-group undo-id]
(ptk/reify ::resize-wasm-text-debounce-commit
ptk/WatchEvent
(watch [_ state _]
(let [ids (get state ::resize-wasm-text-debounce-ids)
objects (dsh/lookup-page-objects state)
modifiers
(reduce
(fn [modifiers id]
(let [shape (get objects id)]
(cond-> modifiers
(and (some? shape)
(cfh/text-shape? shape)
(not= :fixed (:grow-type shape)))
(merge (resize-wasm-text-modifiers shape)))))
{}
ids)]
(if (not (empty? modifiers))
(rx/of (dwm/apply-wasm-modifiers modifiers))
(rx/empty))))))
modifiers
(reduce
(fn [modifiers id]
(let [shape (get objects id)]
(cond-> modifiers
(and (some? shape)
(cfh/text-shape? shape)
(not= :fixed (:grow-type shape)))
(merge (resize-wasm-text-modifiers shape)))))
{}
ids)
;; When undo-id is present, extend the current undo transaction instead of
;; creating a new one, and commit it after the resize (single undo action).
extend-tx? (some? undo-id)
apply-opts (cond-> {}
(some? undo-group) (assoc :undo-group undo-group)
extend-tx? (assoc :undo-transation? false))]
(cond
(not (empty? modifiers))
(if extend-tx?
(rx/concat
(rx/of (dwm/apply-wasm-modifiers modifiers apply-opts))
(rx/of (dwu/commit-undo-transaction undo-id)))
(rx/of (dwm/apply-wasm-modifiers modifiers apply-opts)))
extend-tx?
;; No resize needed (e.g. :fixed grow-type) but we must commit the add
(rx/of (dwu/commit-undo-transaction undo-id))
:else
(rx/empty)))))))
;; This event will debounce the resize events so, if there are many, they
;; are processed at the same time and not one-by-one. This will improve
;; performance because it's better to make only one layout calculation instead
;; of (potentialy) hundreds.
(defn resize-wasm-text-debounce-inner
[id]
(let [cur-event (js/Symbol)]
(ptk/reify ::resize-wasm-text-debounce-inner
ptk/UpdateEvent
(update [_ state]
(-> state
(update ::resize-wasm-text-debounce-ids (fnil conj []) id)
(cond-> (nil? (::resize-wasm-text-debounce-event state))
(assoc ::resize-wasm-text-debounce-event cur-event))))
([id]
(resize-wasm-text-debounce-inner id nil))
([id {:keys [undo-group undo-id]}]
(let [cur-event (js/Symbol)]
(ptk/reify ::resize-wasm-text-debounce-inner
ptk/UpdateEvent
(update [_ state]
(-> state
(update ::resize-wasm-text-debounce-ids (fnil conj []) id)
(cond-> (nil? (::resize-wasm-text-debounce-event state))
(assoc ::resize-wasm-text-debounce-event cur-event))))
ptk/WatchEvent
(watch [_ state stream]
(if (= (::resize-wasm-text-debounce-event state) cur-event)
(let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))]
(rx/concat
(rx/merge
(->> stream
(rx/filter (ptk/type? ::resize-wasm-text-debounce-inner))
(rx/debounce 40)
(rx/take 1)
(rx/map #(resize-wasm-text-debounce-commit))
(rx/take-until stopper))
(rx/of (resize-wasm-text-debounce-inner id)))
(rx/of #(dissoc %
::resize-wasm-text-debounce-ids
::resize-wasm-text-debounce-event))))
(rx/empty))))))
ptk/WatchEvent
(watch [_ state stream]
(if (= (::resize-wasm-text-debounce-event state) cur-event)
(let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))]
(rx/concat
(rx/merge
(->> stream
(rx/filter (ptk/type? ::resize-wasm-text-debounce-inner))
(rx/debounce 40)
(rx/take 1)
(rx/map (fn [evt]
(resize-wasm-text-debounce-commit
(some-> evt meta :undo-group)
(some-> evt meta :undo-id))))
(rx/take-until stopper))
(rx/of (with-meta
(resize-wasm-text-debounce-inner id)
{:undo-group undo-group :undo-id undo-id})))
(rx/of #(dissoc %
::resize-wasm-text-debounce-ids
::resize-wasm-text-debounce-event))))
(rx/empty)))))))
(defn resize-wasm-text-debounce
[id]
(ptk/reify ::resize-wasm-text-debounce
ptk/WatchEvent
(watch [_ state _]
(let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state page-id)
content (dm/get-in objects [id :content])
fonts (wasm.fonts/get-content-fonts content)
([id]
(resize-wasm-text-debounce id nil))
([id {:keys [undo-group undo-id] :as opts}]
(ptk/reify ::resize-wasm-text-debounce
ptk/WatchEvent
(watch [_ state _]
(let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state page-id)
content (dm/get-in objects [id :content])
fonts (wasm.fonts/get-content-fonts content)
fonts-loaded?
(->> fonts
(every?
(fn [font]
(let [font-data (wasm.fonts/make-font-data font)]
(wasm.fonts/font-stored? font-data (:emoji? font-data))))))]
fonts-loaded?
(->> fonts
(every?
(fn [font]
(let [font-data (wasm.fonts/make-font-data font)]
(wasm.fonts/font-stored? font-data (:emoji? font-data))))))]
(if (not fonts-loaded?)
(->> (rx/of (resize-wasm-text-debounce id))
(rx/delay 20))
(rx/of (resize-wasm-text-debounce-inner id)))))))
(if (not fonts-loaded?)
(->> (rx/of (resize-wasm-text-debounce id opts))
(rx/delay 20))
(let [pass-opts (when (or (some? undo-group) (some? undo-id))
(cond-> {}
(some? undo-group) (assoc :undo-group undo-group)
(some? undo-id) (assoc :undo-id undo-id)))]
(rx/of (resize-wasm-text-debounce-inner id pass-opts)))))))))
(defn resize-wasm-text-all
"Resize all text shapes (auto-width/auto-height) from a collection of ids."

View File

@ -188,7 +188,7 @@
(when-not (.-hidden js/document)
(let [trigger-el (mf/ref-val trigger-ref)]
(clear-schedule schedule-ref)
(add-schedule schedule-ref delay
(add-schedule schedule-ref (d/nilv delay 300)
(fn []
(when-let [active @active-tooltip]
(when (not= (:id active) tooltip-id)

View File

@ -17,6 +17,11 @@
(defn- has-blur? [shape]
(:blur shape))
(defn- blur-css-property [shape]
(if (= :background-blur (get-in shape [:blur :type]))
:backdrop-filter
:filter))
(mf/defc blur-panel
[{:keys [objects shapes]}]
(let [shapes (->> shapes (filter has-blur?))]
@ -27,15 +32,18 @@
:class (stl/css :title-wrapper)
:title-class (stl/css :blur-attr-title)}
(when (= (count shapes) 1)
[:> copy-button* {:data (css/get-css-property objects (first shapes) :filter)
:class (stl/css :copy-btn-title)}])]
(let [prop (blur-css-property (first shapes))]
[:> copy-button* {:data (css/get-css-property objects (first shapes) prop)
:class (stl/css :copy-btn-title)}]))]
[:div {:class (stl/css :attributes-content)}
(for [shape shapes]
[:div {:class (stl/css :blur-row)
:key (dm/str "block-" (:id shape) "-blur")}
[:div {:class (stl/css :global/attr-label)} "Filter"]
[:div {:class (stl/css :global/attr-value)}
[:> copy-button* {:data (css/get-css-property objects shape :filter)}
[:div {:class (stl/css :button-children)}
(css/get-css-value objects shape :filter)]]]])]])))
(let [prop (blur-css-property shape)]
[:div {:class (stl/css :blur-row)
:key (dm/str "block-" (:id shape) "-blur")}
[:div {:class (stl/css :global/attr-label)}
(if (= prop :backdrop-filter) "Backdrop Filter" "Filter")]
[:div {:class (stl/css :global/attr-value)}
[:> copy-button* {:data (css/get-css-property objects shape prop)}
[:div {:class (stl/css :button-children)}
(css/get-css-value objects shape prop)]]]]))]])))

View File

@ -7,7 +7,6 @@
(ns app.main.ui.settings.integrations
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.common.time :as ct]
@ -410,7 +409,7 @@
profile (mf/deref refs/profile)
mcp-key (some #(when (= (:type %) "mcp") %) tokens)
mcp-enabled? (d/nilv (-> profile :props :mcp-enabled) false)
mcp-enabled? (true? (-> profile :props :mcp-enabled))
expires-at (:expires-at mcp-key)
expired? (and (some? expires-at) (> (ct/now) expires-at))

View File

@ -21,7 +21,9 @@
(defn filter-str
[filter-id shape]
(when (or (seq (->> (:shadow shape) (remove :hidden)))
(and (:blur shape) (-> shape :blur :hidden not)))
(and (:blur shape)
(-> shape :blur :hidden not)
(= :layer-blur (-> shape :blur :type))))
(str/ffmt "url(#%)" filter-id)))
(mf/defc color-matrix

View File

@ -749,8 +749,8 @@
profile (mf/deref refs/profile)
workspace-local (mf/deref refs/workspace-local)
mcp-enabled? (-> profile :props :mcp-enabled)
mcp-connected? (-> workspace-local :mcp :connected)
mcp-enabled? (true? (-> profile :props :mcp-enabled))
mcp-connected? (true? (-> workspace-local :mcp :connection))
on-nav-to-integrations
(mf/use-fn
@ -978,9 +978,10 @@
:class (stl/css :item-arrow)}]])
(when (contains? cf/flags :mcp)
(let [mcp-enabled? (-> profile :props :mcp-enabled)
mcp-connected? (-> workspace-local :mcp :connected)
mcp-active? (and mcp-enabled? mcp-connected?)]
(let [mcp-enabled? (true? (-> profile :props :mcp-enabled))
mcp-connection (-> workspace-local :mcp :connection)
mcp-connected? (true? mcp-connection)
mcp-error? (nil? mcp-connection)]
[:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item)
:on-click on-menu-click
:on-key-down (fn [event]
@ -992,7 +993,8 @@
[:span {:class (stl/css :item-name)}
(tr "workspace.header.menu.option.mcp")]
[:span {:class (stl/css-case :item-indicator true
:active mcp-active?)}]
:active (and mcp-enabled? mcp-connected?)
:failed (and mcp-enabled? mcp-error?))}]
[:> icon* {:icon-id i/arrow-right
:class (stl/css :item-arrow)}]]))

View File

@ -134,6 +134,10 @@
&.active {
--menu-indicator-color: var(--color-accent-primary);
}
&.failed {
--menu-indicator-color: var(--color-foreground-error);
}
}
.item-arrow {

View File

@ -75,7 +75,22 @@
on-composition-start
(mf/use-fn
(fn [_event]
(reset! composing? true)))
(reset! composing? true)
(text-editor/text-editor-composition-start)))
on-composition-update
(mf/use-fn
(fn [event]
(when-not composing?
(reset! composing? true))
(let [data (.-data event)]
(when data
(text-editor/text-editor-composition-update data)
(sync-wasm-text-editor-content!)
(wasm.api/request-render "text-composition"))
(when-let [node (mf/ref-val contenteditable-ref)]
(set! (.-textContent node) "")))))
on-composition-end
(mf/use-fn
@ -83,7 +98,7 @@
(reset! composing? false)
(let [data (.-data event)]
(when data
(text-editor/text-editor-insert-text data)
(text-editor/text-editor-composition-end data)
(sync-wasm-text-editor-content!)
(wasm.api/request-render "text-composition"))
(when-let [node (mf/ref-val contenteditable-ref)]
@ -326,6 +341,7 @@
:contentEditable true
:suppressContentEditableWarning true
:on-composition-start on-composition-start
:on-composition-update on-composition-update
:on-composition-end on-composition-end
:on-key-down on-key-down
:on-input on-input

View File

@ -7,11 +7,15 @@
(ns app.main.ui.workspace.sidebar.options.menus.blur
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.workspace :as udw]
[app.main.data.workspace.shapes :as dwsh]
[app.main.features :as features]
[app.main.store :as st]
[app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.components.title-bar :refer [title-bar*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
@ -31,6 +35,9 @@
(mf/defc blur-menu [{:keys [ids type values]}]
(let [blur (:blur values)
has-value? (not (nil? blur))
render-wasm? (features/use-feature "render-wasm/v1")
bg-blur? (and render-wasm?
(contains? cf/flags :background-blur))
state* (mf/use-state {:show-content true
:show-more-options false})
@ -75,12 +82,25 @@
:always
(assoc-in [:blur :value] value)))))
handle-type-change
(mf/use-fn
(mf/deps change! ids)
(fn [value]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(change! #(assoc-in % [:blur :type] (keyword value)))))
handle-toggle-visibility
(mf/use-fn
(mf/deps change! ids)
(fn []
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(change! #(update-in % [:blur :hidden] not))))]
(change! #(update-in % [:blur :hidden] not))))
type-options
(mf/with-memo [bg-blur?]
(cond-> [{:value "layer-blur" :label (tr "workspace.options.blur-options.layer-blur")}]
bg-blur?
(conj {:value "background-blur" :label (tr "workspace.options.blur-options.background-blur")})))]
[:div {:class (stl/css :element-set)}
[:div {:class (stl/css :element-title)}
@ -102,13 +122,20 @@
[:div {:class (stl/css :element-set-content)}
[:div {:class (stl/css-case :first-row true
:hidden hidden?)}
[:div {:class (stl/css :blur-info)}
[:div {:class (stl/css :blur-info)
:data-testid "blur-info"}
[:button {:class (stl/css-case :show-more true
:selected more-options?)
:on-click toggle-more-options}
deprecated-icon/menu]
[:span {:class (stl/css :label)}
(tr "workspace.options.blur-options.title")]]
(if bg-blur?
[:& select {:class (stl/css :blur-type-select)
:default-value (d/name (:type blur))
:options type-options
:disabled hidden?
:on-change handle-type-change}]
[:span {:class (stl/css :label)}
(tr "workspace.options.blur-options.title")])]
[:div {:class (stl/css :actions)}
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.blur-options.toggle-blur")

View File

@ -66,6 +66,10 @@
box-sizing: border-box;
border: deprecated.$s-1 solid var(--input-border-color);
}
.blur-type-select {
flex-grow: 1;
border-radius: 0 deprecated.$br-8 deprecated.$br-8 0;
}
}
.actions {
@include deprecated.flexRow;

View File

@ -291,7 +291,7 @@
:r4 "Bottom Left"
:r3 "Bottom Right"}
:hint (tr "workspace.tokens.radius")
:on-update-shape-all dwta/update-shape-radius-all
:on-update-shape-all dwta/update-shape-radius
:on-update-shape update-shape-radius-for-corners})
shadow (partial generic-attribute-actions #{:shadow} "Shadow")]
{:border-radius border-radius

View File

@ -18,6 +18,7 @@
[app.common.types.shape :as cts]
[app.common.types.text :as txt]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.changes :as ch]
[app.main.data.common :as dcm]
[app.main.data.helpers :as dsh]
@ -84,6 +85,10 @@
:$plugin {:enumerable false :get (fn [] plugin-id)}
;; Public properties
:version
{:this true
:get (constantly (:base cf/version))}
:root
{:this true
:get #(.getRoot ^js %)}
@ -112,7 +117,7 @@
(fn [_ shapes]
(cond
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
(u/display-not-valid :selection shapes)
(u/not-valid plugin-id :selection shapes)
:else
(let [ids (into (d/ordered-set) (map #(obj/get % "$id")) shapes)]
@ -177,7 +182,7 @@
(fn [shapes]
(cond
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
(u/display-not-valid :shapesColors-shapes shapes)
(u/not-valid plugin-id :shapesColors-shapes shapes)
:else
(let [objects (u/locate-objects)
@ -197,13 +202,13 @@
new-color (parser/parse-color-data new-color)]
(cond
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
(u/display-not-valid :replaceColor-shapes shapes)
(u/not-valid plugin-id :replaceColor-shapes shapes)
(not (sm/validate ctc/schema:color old-color))
(u/display-not-valid :replaceColor-oldColor old-color)
(u/not-valid plugin-id :replaceColor-oldColor old-color)
(not (sm/validate ctc/schema:color new-color))
(u/display-not-valid :replaceColor-newColor new-color)
(u/not-valid plugin-id :replaceColor-newColor new-color)
:else
(let [file-id (:current-file-id @st/state)
@ -256,10 +261,10 @@
(fn [name url]
(cond
(not (string? name))
(u/display-not-valid :uploadMedia-name name)
(u/not-valid plugin-id :uploadMedia-name name)
(not (string? url))
(u/display-not-valid :uploadMedia-url url)
(u/not-valid plugin-id :uploadMedia-url url)
:else
(let [file-id (:current-file-id @st/state)]
@ -290,7 +295,7 @@
(fn [shapes]
(cond
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
(u/display-not-valid :group-shapes shapes)
(u/not-valid plugin-id :group-shapes shapes)
:else
(let [file-id (:current-file-id @st/state)
@ -305,10 +310,10 @@
(fn [group & rest]
(cond
(not (shape/shape-proxy? group))
(u/display-not-valid :ungroup group)
(u/not-valid plugin-id :ungroup group)
(and (some? rest) (not (every? shape/shape-proxy? rest)))
(u/display-not-valid :ungroup rest)
(u/not-valid plugin-id :ungroup rest)
:else
(let [shapes (concat [group] rest)
@ -348,7 +353,7 @@
(fn [text]
(cond
(or (not (string? text)) (empty? text))
(u/display-not-valid :createText text)
(u/not-valid plugin-id :createText text)
:else
(let [page (dsh/lookup-page @st/state)
@ -379,7 +384,7 @@
(fn [svg-string]
(cond
(or (not (string? svg-string)) (empty? svg-string))
(u/display-not-valid :createShapeFromSvg svg-string)
(u/not-valid plugin-id :createShapeFromSvg svg-string)
:else
(let [id (uuid/next)
@ -396,7 +401,7 @@
(cond
(or (not (string? svg-string)) (empty? svg-string))
(do
(u/display-not-valid :createShapeFromSvg "Svg not valid")
(u/not-valid plugin-id :createShapeFromSvg "Svg not valid")
(reject "Svg not valid"))
:else
@ -414,10 +419,10 @@
(let [bool-type (keyword bool-type)]
(cond
(not (contains? cts/bool-types bool-type))
(u/display-not-valid :createBoolean-boolType bool-type)
(u/not-valid plugin-id :createBoolean-boolType bool-type)
(or (not (array? shapes)) (empty? shapes) (not (every? shape/shape-proxy? shapes)))
(u/display-not-valid :createBoolean-shapes shapes)
(u/not-valid plugin-id :createBoolean-shapes shapes)
:else
(let [ids (into #{} (map #(obj/get % "$id")) shapes)
@ -431,10 +436,10 @@
(let [type (d/nilv (obj/get options "type") "html")]
(cond
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
(u/display-not-valid :generateMarkup-shapes shapes)
(u/not-valid plugin-id :generateMarkup-shapes shapes)
(and (some? type) (not (contains? #{"html" "svg"} type)))
(u/display-not-valid :generateMarkup-type type)
(u/not-valid plugin-id :generateMarkup-type type)
:else
(let [resolved-code
@ -466,16 +471,16 @@
children? (d/nilv (obj/get options "includeChildren") true)]
(cond
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
(u/display-not-valid :generateStyle-shapes shapes)
(u/not-valid plugin-id :generateStyle-shapes shapes)
(and (some? type) (not (contains? #{"css"} type)))
(u/display-not-valid :generateStyle-type type)
(u/not-valid plugin-id :generateStyle-type type)
(and (some? prelude?) (not (boolean? prelude?)))
(u/display-not-valid :generateStyle-withPrelude prelude?)
(u/not-valid plugin-id :generateStyle-withPrelude prelude?)
(and (some? children?) (not (boolean? children?)))
(u/display-not-valid :generateStyle-includeChildren children?)
(u/not-valid plugin-id :generateStyle-includeChildren children?)
:else
(let [resolved-styles
@ -548,7 +553,7 @@
:else nil)
new-window (if (boolean? new-window) new-window false)]
(if (nil? id)
(u/display-not-valid :openPage "Expected a Page object or a page UUID string")
(u/not-valid plugin-id :openPage "Expected a Page object or a page UUID string")
(st/emit! (dcm/go-to-workspace :page-id id ::rt/new-window new-window)))))
:alignHorizontal
@ -560,10 +565,10 @@
nil)]
(cond
(nil? dir)
(u/display-not-valid :alignHorizontal-direction "Direction not valid")
(u/not-valid plugin-id :alignHorizontal-direction "Direction not valid")
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
(u/display-not-valid :alignHorizontal-shapes "Not valid shapes")
(u/not-valid plugin-id :alignHorizontal-shapes "Not valid shapes")
:else
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
@ -578,10 +583,10 @@
nil)]
(cond
(nil? dir)
(u/display-not-valid :alignVertical-direction "Direction not valid")
(u/not-valid plugin-id :alignVertical-direction "Direction not valid")
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
(u/display-not-valid :alignVertical-shapes "Not valid shapes")
(u/not-valid plugin-id :alignVertical-shapes "Not valid shapes")
:else
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
@ -591,7 +596,7 @@
(fn [shapes]
(cond
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
(u/display-not-valid :distributeHorizontal-shapes "Not valid shapes")
(u/not-valid plugin-id :distributeHorizontal-shapes "Not valid shapes")
:else
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
@ -601,7 +606,7 @@
(fn [shapes]
(cond
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
(u/display-not-valid :distributeVertical-shapes "Not valid shapes")
(u/not-valid plugin-id :distributeVertical-shapes "Not valid shapes")
:else
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
@ -611,7 +616,7 @@
(fn [shapes]
(cond
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
(u/display-not-valid :flatten-shapes "Not valid shapes")
(u/not-valid plugin-id :flatten-shapes "Not valid shapes")
:else
(let [ids (into #{} (map #(obj/get % "$id")) shapes)]
@ -622,7 +627,7 @@
(cond
(or (not (seq shapes))
(not (every? u/is-main-component-proxy? shapes)))
(u/display-not-valid :shapes shapes)
(u/not-valid plugin-id :shapes shapes)
:else
(let [file-id (obj/get (first shapes) "$file")

View File

@ -60,13 +60,13 @@
(let [profile (:profile @st/state)]
(cond
(or (not (string? content)) (empty? content))
(u/display-not-valid :content "Not valid")
(u/not-valid plugin-id :content "Not valid")
(not= (:id profile) (:owner-id data))
(u/display-not-valid :content "Cannot change content from another user's comments")
(u/not-valid plugin-id :content "Cannot change content from another user's comments")
(not (r/check-permission plugin-id "comment:write"))
(u/display-not-valid :content "Plugin doesn't have 'comment:write' permission")
(u/not-valid plugin-id :content "Plugin doesn't have 'comment:write' permission")
:else
(->> (rp/cmd! :update-comment {:id (:id data) :content content})
@ -81,7 +81,7 @@
(cond
(not (r/check-permission plugin-id "comment:write"))
(do
(u/display-not-valid :remove "Plugin doesn't have 'comment:write' permission")
(u/not-valid plugin-id :remove "Plugin doesn't have 'comment:write' permission")
(reject "Plugin doesn't have 'comment:write' permission"))
:else
@ -120,10 +120,10 @@
(cond
(or (not (sm/valid-safe-number? (:x position)))
(not (sm/valid-safe-number? (:y position))))
(u/display-not-valid :position "Not valid point")
(u/not-valid plugin-id :position "Not valid point")
(not (r/check-permission plugin-id "comment:write"))
(u/display-not-valid :position "Plugin doesn't have 'comment:write' permission")
(u/not-valid plugin-id :position "Plugin doesn't have 'comment:write' permission")
:else
(do (st/emit! (dwc/update-comment-thread-position @data* [(:x position) (:y position)]))
@ -137,10 +137,10 @@
(fn [is-resolved]
(cond
(not (boolean? is-resolved))
(u/display-not-valid :resolved "Not a boolean type")
(u/not-valid plugin-id :resolved "Not a boolean type")
(not (r/check-permission plugin-id "comment:write"))
(u/display-not-valid :resolved "Plugin doesn't have 'comment:write' permission")
(u/not-valid plugin-id :resolved "Plugin doesn't have 'comment:write' permission")
:else
(do (st/emit! (dc/update-comment-thread (assoc @data* :is-resolved is-resolved)))
@ -153,7 +153,7 @@
(cond
(not (r/check-permission plugin-id "comment:read"))
(do
(u/display-not-valid :findComments "Plugin doesn't have 'comment:read' permission")
(u/not-valid plugin-id :findComments "Plugin doesn't have 'comment:read' permission")
(reject "Plugin doesn't have 'comment:read' permission"))
:else
@ -169,10 +169,10 @@
(fn [content]
(cond
(not (r/check-permission plugin-id "comment:write"))
(u/display-not-valid :reply "Plugin doesn't have 'comment:write' permission")
(u/not-valid plugin-id :reply "Plugin doesn't have 'comment:write' permission")
(or (not (string? content)) (empty? content))
(u/display-not-valid :reply "Not valid")
(u/not-valid plugin-id :reply "Not valid")
:else
(js/Promise.
@ -186,10 +186,10 @@
owner (dsh/lookup-profile @st/state (:owner-id data))]
(cond
(not (r/check-permission plugin-id "comment:write"))
(u/display-not-valid :remove "Plugin doesn't have 'comment:write' permission")
(u/not-valid plugin-id :remove "Plugin doesn't have 'comment:write' permission")
(not= (:id profile) owner)
(u/display-not-valid :remove "Cannot change content from another user's comments")
(u/not-valid plugin-id :remove "Cannot change content from another user's comments")
:else
(js/Promise.

View File

@ -45,10 +45,10 @@
(fn [value]
(cond
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :label "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :label "Plugin doesn't have 'content:write' permission")
(or (not (string? value)) (empty? value))
(u/display-not-valid :label value)
(u/not-valid plugin-id :label value)
:else
(do (swap! data assoc :label value :created-by "user")
@ -145,7 +145,7 @@
(fn [key]
(cond
(not (string? key))
(u/display-not-valid :getPluginData-key key)
(u/not-valid plugin-id :getPluginData-key key)
:else
(let [file (u/locate-file id)]
@ -155,13 +155,13 @@
(fn [key value]
(cond
(or (not (string? key)) (empty? key))
(u/display-not-valid :setPluginData-key key)
(u/not-valid plugin-id :setPluginData-key key)
(not (string? value))
(u/display-not-valid :setPluginData-value value)
(u/not-valid plugin-id :setPluginData-value value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :setPluginData "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :setPluginData "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dp/set-plugin-data id :file (keyword "plugin" (str plugin-id)) key value))))
@ -175,10 +175,10 @@
(fn [namespace key]
(cond
(not (string? namespace))
(u/display-not-valid :getSharedPluginData-namespace namespace)
(u/not-valid plugin-id :getSharedPluginData-namespace namespace)
(not (string? key))
(u/display-not-valid :getSharedPluginData-key key)
(u/not-valid plugin-id :getSharedPluginData-key key)
:else
(let [file (u/locate-file id)]
@ -188,16 +188,16 @@
(fn [namespace key value]
(cond
(or (not (string? namespace)) (empty? namespace))
(u/display-not-valid :setSharedPluginData-namespace namespace)
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
(or (not (string? key)) (empty? key))
(u/display-not-valid :setSharedPluginData-key key)
(u/not-valid plugin-id :setSharedPluginData-key key)
(not (string? value))
(u/display-not-valid :setSharedPluginData-value value)
(u/not-valid plugin-id :setSharedPluginData-value value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :setSharedPluginData "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dp/set-plugin-data id :file (keyword "shared" namespace) key value))))
@ -206,7 +206,7 @@
(fn [namespace]
(cond
(not (string? namespace))
(u/display-not-valid :getSharedPluginDataKeys namespace)
(u/not-valid plugin-id :getSharedPluginDataKeys namespace)
:else
(let [file (u/locate-file id)]
@ -216,7 +216,7 @@
(fn []
(cond
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :createPage "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :createPage "Plugin doesn't have 'content:write' permission")
:else
(let [page-id (uuid/next)]

View File

@ -6,17 +6,11 @@
(ns app.plugins.flags
(:require
[app.common.data.macros :as dm]
[app.main.store :as st]
[app.plugins.utils :as u]
[app.util.object :as obj]
[potok.v2.core :as ptk]))
(defn natural-child-ordering?
[plugin-id]
(boolean
(dm/get-in @st/state [:plugins :flags plugin-id :natural-child-ordering])))
(defn clear
[id]
(ptk/reify ::reset
@ -37,13 +31,27 @@
:naturalChildOrdering
{:this false
:get
(fn [] (natural-child-ordering? plugin-id))
(fn [] (u/natural-child-ordering? plugin-id))
:set
(fn [value]
(cond
(not (boolean? value))
(u/display-not-valid :naturalChildOrdering value)
(u/not-valid plugin-id :naturalChildOrdering value)
:else
(st/emit! (set-flag plugin-id :natural-child-ordering value))))}))
(st/emit! (set-flag plugin-id :natural-child-ordering value))))}
:throwValidationErrors
{:this false
:get
(fn [] (u/throw-validation-errors? plugin-id))
:set
(fn [value]
(cond
(not (boolean? value))
(u/not-valid plugin-id :throwValidationErrors value)
:else
(st/emit! (set-flag plugin-id :throw-validation-errors value))))}))

View File

@ -12,7 +12,6 @@
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh]
[app.main.store :as st]
[app.plugins.flags :refer [natural-child-ordering?]]
[app.plugins.register :as r]
[app.plugins.utils :as u]
[app.util.object :as obj]))
@ -39,10 +38,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/flex-direction-types value))
(u/display-not-valid :dir value)
(u/not-valid plugin-id :dir value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :dir "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :dir "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-flex-dir value})))))}
@ -55,10 +54,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/wrap-types value))
(u/display-not-valid :wrap value)
(u/not-valid plugin-id :wrap value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :wrap "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :wrap "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-wrap-type value})))))}
@ -71,10 +70,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/align-items-types value))
(u/display-not-valid :alignItems value)
(u/not-valid plugin-id :alignItems value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :alignItems "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :alignItems "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-align-items value})))))}
@ -87,10 +86,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/align-content-types value))
(u/display-not-valid :alignContent value)
(u/not-valid plugin-id :alignContent value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :alignContent "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :alignContent "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-align-content value})))))}
@ -103,10 +102,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/justify-items-types value))
(u/display-not-valid :justifyItems value)
(u/not-valid plugin-id :justifyItems value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :justifyItems "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :justifyItems "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-justify-items value})))))}
@ -119,10 +118,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/justify-content-types value))
(u/display-not-valid :justifyContent value)
(u/not-valid plugin-id :justifyContent value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :justifyContent "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :justifyContent "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-justify-content value})))))}
@ -134,10 +133,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :rowGap value)
(u/not-valid plugin-id :rowGap value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :rowGap "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :rowGap "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:row-gap value}}))))}
@ -149,10 +148,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :columnGap value)
(u/not-valid plugin-id :columnGap value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :columnGap "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :columnGap "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:column-gap value}}))))}
@ -164,10 +163,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :verticalPadding value)
(u/not-valid plugin-id :verticalPadding value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :verticalPadding "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :verticalPadding "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value :p3 value}}))))}
@ -179,10 +178,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :horizontalPadding value)
(u/not-valid plugin-id :horizontalPadding value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :horizontalPadding "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :horizontalPadding "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value :p4 value}}))))}
@ -195,10 +194,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :topPadding value)
(u/not-valid plugin-id :topPadding value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :topPadding "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :topPadding "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value}}))))}
@ -210,10 +209,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :rightPadding value)
(u/not-valid plugin-id :rightPadding value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :rightPadding "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :rightPadding "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value}}))))}
@ -225,10 +224,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :bottomPadding value)
(u/not-valid plugin-id :bottomPadding value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :bottomPadding "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :bottomPadding "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p3 value}}))))}
@ -240,10 +239,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :leftPadding value)
(u/not-valid plugin-id :leftPadding value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :leftPadding "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :leftPadding "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p4 value}}))))}
@ -256,13 +255,13 @@
(fn [child]
(cond
(not (shape-proxy? child))
(u/display-not-valid :appendChild child)
(u/not-valid plugin-id :appendChild child)
:else
(let [child-id (obj/get child "$id")
shape (u/locate-shape file-id page-id id)
index
(if (and (natural-child-ordering? plugin-id) (not (ctl/reverse? shape)))
(if (and (u/natural-child-ordering? plugin-id) (not (ctl/reverse? shape)))
0
(count (:shapes shape)))]
(st/emit! (dwsh/relocate-shapes #{child-id} id index)))))
@ -275,10 +274,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/item-h-sizing-types value))
(u/display-not-valid :horizontalSizing value)
(u/not-valid plugin-id :horizontalSizing value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :horizontalSizing "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :horizontalSizing "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-item-h-sizing value})))))}
@ -291,10 +290,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/item-v-sizing-types value))
(u/display-not-valid :verticalSizing value)
(u/not-valid plugin-id :verticalSizing value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :verticalSizing "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :verticalSizing "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-item-v-sizing value})))))}))
@ -317,10 +316,10 @@
(fn [_ value]
(cond
(not (boolean? value))
(u/display-not-valid :absolute value)
(u/not-valid plugin-id :absolute value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :absolute "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :absolute "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-item-absolute value}))))}
@ -332,10 +331,10 @@
(fn [_ value]
(cond
(sm/valid-safe-int? value)
(u/display-not-valid :zIndex value)
(u/not-valid plugin-id :zIndex value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :zIndex "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :zIndex "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-z-index value}))))}
@ -348,10 +347,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/item-h-sizing-types value))
(u/display-not-valid :horizontalPadding value)
(u/not-valid plugin-id :horizontalPadding value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :horizontalPadding "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :horizontalPadding "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-h-sizing value})))))}
@ -364,10 +363,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/item-v-sizing-types value))
(u/display-not-valid :verticalSizing value)
(u/not-valid plugin-id :verticalSizing value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :verticalSizing "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :verticalSizing "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-v-sizing value})))))}
@ -380,10 +379,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/item-align-self-types value))
(u/display-not-valid :alignSelf value)
(u/not-valid plugin-id :alignSelf value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :alignSelf "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :alignSelf "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-align-self value})))))}
@ -395,10 +394,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :verticalMargin value)
(u/not-valid plugin-id :verticalMargin value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :verticalMargin "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :verticalMargin "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m1 value :m3 value}}))))}
@ -410,10 +409,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :horizontalMargin value)
(u/not-valid plugin-id :horizontalMargin value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :horizontalMargin "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :horizontalMargin "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m2 value :m4 value}}))))}
@ -425,10 +424,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :topMargin value)
(u/not-valid plugin-id :topMargin value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :topMargin "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :topMargin "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m1 value}}))))}
@ -440,10 +439,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :rightMargin value)
(u/not-valid plugin-id :rightMargin value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :rightMargin "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :rightMargin "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m2 value}}))))}
@ -455,10 +454,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :bottomMargin value)
(u/not-valid plugin-id :bottomMargin value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :bottomMargin "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :bottomMargin "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m3 value}}))))}
@ -470,10 +469,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :leftMargin value)
(u/not-valid plugin-id :leftMargin value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :leftMargin "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :leftMargin "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m4 value}}))))}
@ -485,10 +484,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :maxWidth value)
(u/not-valid plugin-id :maxWidth value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :maxWidth "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :maxWidth "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-max-w value}))))}
@ -500,10 +499,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :minWidth value)
(u/not-valid plugin-id :minWidth value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :minWidth "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :minWidth "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-min-w value}))))}
@ -515,10 +514,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :maxHeight value)
(u/not-valid plugin-id :maxHeight value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :maxHeight "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :maxHeight "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-max-h value}))))}
@ -530,10 +529,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :minHeight value)
(u/not-valid plugin-id :minHeight value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :minHeight "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :minHeight "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-min-h value}))))}))

View File

@ -32,7 +32,7 @@
(obj/type-of? p "FontProxy"))
(defn font-proxy
[{:keys [id family name variants] :as font}]
[plugin-id {:keys [id family name variants] :as font}]
(when (some? font)
(let [default-variant (fonts/get-default-variant font)]
(obj/reify {:name "FontProxy"}
@ -55,10 +55,10 @@
(fn [text variant]
(cond
(not (shape/shape-proxy? text))
(u/display-not-valid :applyToText text)
(u/not-valid plugin-id :applyToText text)
(not (r/check-permission (obj/get text "$plugin") "content:write"))
(u/display-not-valid :applyToText "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :applyToText "Plugin doesn't have 'content:write' permission")
:else
(let [id (obj/get text "$id")
@ -73,10 +73,10 @@
(fn [range variant]
(cond
(not (text/text-range-proxy? range))
(u/display-not-valid :applyToRange range)
(u/not-valid plugin-id :applyToRange range)
(not (r/check-permission (obj/get range "$plugin") "content:write"))
(u/display-not-valid :applyToRange "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :applyToRange "Plugin doesn't have 'content:write' permission")
:else
(let [id (obj/get range "$id")
@ -98,53 +98,53 @@
{:get
(fn []
(format/format-array
font-proxy
(partial font-proxy plugin-id)
(vals @fonts/fontsdb)))}
:findById
(fn [id]
(cond
(not (string? id))
(u/display-not-valid :findbyId id)
(u/not-valid plugin-id :findbyId id)
:else
(->> (vals @fonts/fontsdb)
(d/seek #(str/includes? (str/lower (:id %)) (str/lower id)))
(font-proxy))))
(font-proxy plugin-id))))
:findByName
(fn [name]
(cond
(not (string? name))
(u/display-not-valid :findByName name)
(u/not-valid plugin-id :findByName name)
:else
(->> (vals @fonts/fontsdb)
(d/seek #(str/includes? (str/lower (:name %)) (str/lower name)))
(font-proxy))))
(font-proxy plugin-id))))
:findAllById
(fn [id]
(cond
(not (string? id))
(u/display-not-valid :findAllById name)
(u/not-valid plugin-id :findAllById name)
:else
(format/format-array
(fn [font]
(when (str/includes? (str/lower (:id font)) (str/lower id))
(font-proxy font)))
(font-proxy plugin-id font)))
(vals @fonts/fontsdb))))
:findAllByName
(fn [name]
(cond
(not (string? name))
(u/display-not-valid :findAllByName name)
(u/not-valid plugin-id :findAllByName name)
:else
(format/format-array
(fn [font]
(when (str/includes? (str/lower (:name font)) (str/lower name))
(font-proxy font)))
(font-proxy plugin-id font)))
(vals @fonts/fontsdb))))))

View File

@ -40,10 +40,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/grid-direction-types value))
(u/display-not-valid :dir value)
(u/not-valid plugin-id :dir value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :dir "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :dir "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-grid-dir value})))))}
@ -64,10 +64,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/align-items-types value))
(u/display-not-valid :alignItems value)
(u/not-valid plugin-id :alignItems value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :alignItems "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :alignItems "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-align-items value})))))}
@ -80,10 +80,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/align-content-types value))
(u/display-not-valid :alignContent value)
(u/not-valid plugin-id :alignContent value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :alignContent "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :alignContent "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-align-content value})))))}
@ -96,10 +96,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/justify-items-types value))
(u/display-not-valid :justifyItems value)
(u/not-valid plugin-id :justifyItems value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :justifyItems "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :justifyItems "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-justify-items value})))))}
@ -112,10 +112,10 @@
(let [value (keyword value)]
(cond
(not (contains? ctl/justify-content-types value))
(u/display-not-valid :justifyContent value)
(u/not-valid plugin-id :justifyContent value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :justifyContent "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :justifyContent "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-justify-content value})))))}
@ -127,10 +127,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :rowGap value)
(u/not-valid plugin-id :rowGap value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :rowGap "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :rowGap "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:row-gap value}}))))}
@ -142,10 +142,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :columnGap value)
(u/not-valid plugin-id :columnGap value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :columnGap "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :columnGap "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-gap {:column-gap value}}))))}
@ -157,10 +157,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :verticalPadding value)
(u/not-valid plugin-id :verticalPadding value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :verticalPadding "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :verticalPadding "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value :p3 value}}))))}
@ -172,10 +172,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :horizontalPadding value)
(u/not-valid plugin-id :horizontalPadding value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :horizontalPadding "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :horizontalPadding "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value :p4 value}}))))}
@ -187,10 +187,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :topPadding value)
(u/not-valid plugin-id :topPadding value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :topPadding "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :topPadding "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value}}))))}
@ -202,10 +202,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :rightPadding value)
(u/not-valid plugin-id :rightPadding value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :righPadding "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :righPadding "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value}}))))}
@ -217,10 +217,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :bottomPadding value)
(u/not-valid plugin-id :bottomPadding value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :bottomPadding "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :bottomPadding "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p3 value}}))))}
@ -232,10 +232,10 @@
(fn [_ value]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :leftPadding value)
(u/not-valid plugin-id :leftPadding value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :leftPadding "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :leftPadding "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-padding {:p4 value}}))))}
@ -245,14 +245,14 @@
(let [type (keyword type)]
(cond
(not (contains? ctl/grid-track-types type))
(u/display-not-valid :addRow-type type)
(u/not-valid plugin-id :addRow-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :addRow-value value)
(u/not-valid plugin-id :addRow-value value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :addRow "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :addRow "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/add-layout-track #{id} :row {:type type :value value})))))
@ -262,17 +262,17 @@
(let [type (keyword type)]
(cond
(not (sm/valid-safe-int? index))
(u/display-not-valid :addRowAtIndex-index index)
(u/not-valid plugin-id :addRowAtIndex-index index)
(not (contains? ctl/grid-track-types type))
(u/display-not-valid :addRowAtIndex-type type)
(u/not-valid plugin-id :addRowAtIndex-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :addRowAtIndex-value value)
(u/not-valid plugin-id :addRowAtIndex-value value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :addRowAtIndex "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :addRowAtIndex "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/add-layout-track #{id} :row {:type type :value value} index)))))
@ -282,14 +282,14 @@
(let [type (keyword type)]
(cond
(not (contains? ctl/grid-track-types type))
(u/display-not-valid :addColumn-type type)
(u/not-valid plugin-id :addColumn-type type)
(and (or (= :percent type) (= :flex type) (= :lex type))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :addColumn-value value)
(u/not-valid plugin-id :addColumn-value value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :addColumn "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :addColumn "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/add-layout-track #{id} :column {:type type :value value})))))
@ -298,17 +298,17 @@
(fn [index type value]
(cond
(not (sm/valid-safe-int? index))
(u/display-not-valid :addColumnAtIndex-index index)
(u/not-valid plugin-id :addColumnAtIndex-index index)
(not (contains? ctl/grid-track-types type))
(u/display-not-valid :addColumnAtIndex-type type)
(u/not-valid plugin-id :addColumnAtIndex-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :addColumnAtIndex-value value)
(u/not-valid plugin-id :addColumnAtIndex-value value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :addColumnAtIndex "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :addColumnAtIndex "Plugin doesn't have 'content:write' permission")
:else
(let [type (keyword type)]
@ -318,10 +318,10 @@
(fn [index]
(cond
(not (sm/valid-safe-int? index))
(u/display-not-valid :removeRow index)
(u/not-valid plugin-id :removeRow index)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :removeRow "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :removeRow "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/remove-layout-track #{id} :row index))))
@ -330,10 +330,10 @@
(fn [index]
(cond
(not (sm/valid-safe-int? index))
(u/display-not-valid :removeColumn index)
(u/not-valid plugin-id :removeColumn index)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :removeColumn "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :removeColumn "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/remove-layout-track #{id} :column index))))
@ -343,17 +343,17 @@
(let [type (keyword type)]
(cond
(not (sm/valid-safe-int? index))
(u/display-not-valid :setColumn-index index)
(u/not-valid plugin-id :setColumn-index index)
(not (contains? ctl/grid-track-types type))
(u/display-not-valid :setColumn-type type)
(u/not-valid plugin-id :setColumn-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :setColumn-value value)
(u/not-valid plugin-id :setColumn-value value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :setColumn "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :setColumn "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/change-layout-track #{id} :column index (d/without-nils {:type type :value value}))))))
@ -363,17 +363,17 @@
(let [type (keyword type)]
(cond
(not (sm/valid-safe-int? index))
(u/display-not-valid :setRow-index index)
(u/not-valid plugin-id :setRow-index index)
(not (contains? ctl/grid-track-types type))
(u/display-not-valid :setRow-type type)
(u/not-valid plugin-id :setRow-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :setRow-value value)
(u/not-valid plugin-id :setRow-value value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :setRow "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :setRow "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/change-layout-track #{id} :row index (d/without-nils {:type type :value value}))))))
@ -382,7 +382,7 @@
(fn []
(cond
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :remove "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/remove-layout #{id}))))
@ -391,16 +391,16 @@
(fn [child row column]
(cond
(not (shape-proxy? child))
(u/display-not-valid :appendChild-child child)
(u/not-valid plugin-id :appendChild-child child)
(or (< row 0) (not (sm/valid-safe-int? row)))
(u/display-not-valid :appendChild-row row)
(u/not-valid plugin-id :appendChild-row row)
(or (< column 0) (not (sm/valid-safe-int? column)))
(u/display-not-valid :appendChild-column column)
(u/not-valid plugin-id :appendChild-column column)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :appendChild "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :appendChild "Plugin doesn't have 'content:write' permission")
:else
(let [child-id (obj/get child "$id")]
@ -432,13 +432,13 @@
shape (u/proxy->shape self)]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :row-value value)
(u/not-valid plugin-id :row-value value)
(nil? cell)
(u/display-not-valid :row-cell "cell not found")
(u/not-valid plugin-id :row-cell "cell not found")
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :row "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :row "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:row value})))))}
@ -452,13 +452,13 @@
cell (locate-cell self)]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :rowSpan-value value)
(u/not-valid plugin-id :rowSpan-value value)
(nil? cell)
(u/display-not-valid :rowSpan-cell "cell not found")
(u/not-valid plugin-id :rowSpan-cell "cell not found")
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :rowSpan "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :rowSpan "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:row-span value})))))}
@ -472,13 +472,13 @@
cell (locate-cell self)]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :column-value value)
(u/not-valid plugin-id :column-value value)
(nil? cell)
(u/display-not-valid :column-cell "cell not found")
(u/not-valid plugin-id :column-cell "cell not found")
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :column "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :column "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:column value})))))}
@ -492,13 +492,13 @@
cell (locate-cell self)]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :columnSpan-value value)
(u/not-valid plugin-id :columnSpan-value value)
(nil? cell)
(u/display-not-valid :columnSpan-cell "cell not found")
(u/not-valid plugin-id :columnSpan-cell "cell not found")
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :columnSpan "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :columnSpan "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:column-span value})))))}
@ -512,13 +512,13 @@
cell (locate-cell self)]
(cond
(not (string? value))
(u/display-not-valid :areaName-value value)
(u/not-valid plugin-id :areaName-value value)
(nil? cell)
(u/display-not-valid :areaName-cell "cell not found")
(u/not-valid plugin-id :areaName-cell "cell not found")
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :areaName "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :areaName "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-grid-cells (:parent-id shape) #{(:id cell)} {:area-name value})))))}
@ -533,13 +533,13 @@
value (keyword value)]
(cond
(not (contains? ctl/grid-position-types value))
(u/display-not-valid :position-value value)
(u/not-valid plugin-id :position-value value)
(nil? cell)
(u/display-not-valid :position-cell "cell not found")
(u/not-valid plugin-id :position-cell "cell not found")
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :position "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :position "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/change-cells-mode (:parent-id shape) #{(:id cell)} value)))))}
@ -554,13 +554,13 @@
cell (locate-cell self)]
(cond
(not (contains? ctl/grid-cell-align-self-types value))
(u/display-not-valid :alignSelf-value value)
(u/not-valid plugin-id :alignSelf-value value)
(nil? cell)
(u/display-not-valid :alignSelf-cell "cell not found")
(u/not-valid plugin-id :alignSelf-cell "cell not found")
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :alignSelf "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :alignSelf "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-grid-cells (:parent-id shape) #{(:id cell)} {:align-self value})))))}
@ -575,13 +575,13 @@
cell (locate-cell self)]
(cond
(not (contains? ctl/grid-cell-justify-self-types value))
(u/display-not-valid :justifySelf-value value)
(u/not-valid plugin-id :justifySelf-value value)
(nil? cell)
(u/display-not-valid :justifySelf-cell "cell not found")
(u/not-valid plugin-id :justifySelf-cell "cell not found")
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :justifySelf "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :justifySelf "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-grid-cells (:parent-id shape) #{(:id cell)} {:justify-self value})))))})))

View File

@ -24,7 +24,7 @@
(fn []
(cond
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :resize "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :resize "Plugin doesn't have 'content:write' permission")
:else
(let [id (js/Symbol)]
@ -35,10 +35,10 @@
(fn [block-id]
(cond
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :resize "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :resize "Plugin doesn't have 'content:write' permission")
(not block-id)
(u/display-not-valid :undoBlockFinish block-id)
(u/not-valid plugin-id :undoBlockFinish block-id)
:else
(st/emit! (dwu/commit-undo-transaction block-id))))))

View File

@ -60,10 +60,10 @@
(fn [self value]
(cond
(not (string? value))
(u/display-not-valid :name value)
(u/not-valid plugin-id :name value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :name "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :name "Plugin doesn't have 'library:write' permission")
:else
(let [color (u/proxy->library-color self)
@ -77,10 +77,10 @@
(fn [self value]
(cond
(not (string? value))
(u/display-not-valid :path value)
(u/not-valid plugin-id :path value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :path "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :path "Plugin doesn't have 'library:write' permission")
:else
(let [color (-> (u/proxy->library-color self)
@ -94,10 +94,10 @@
(fn [self value]
(cond
(or (not (string? value)) (not (clr/valid-hex-color? value)))
(u/display-not-valid :color value)
(u/not-valid plugin-id :color value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :color "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :color "Plugin doesn't have 'library:write' permission")
:else
(let [color (-> (u/proxy->library-color self)
@ -111,10 +111,10 @@
(fn [self value]
(cond
(or (not (number? value)) (< value 0) (> value 1))
(u/display-not-valid :opacity value)
(u/not-valid plugin-id :opacity value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :opacity "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :opacity "Plugin doesn't have 'library:write' permission")
:else
(let [color (-> (u/proxy->library-color self)
@ -129,10 +129,10 @@
(let [value (parser/parse-gradient value)]
(cond
(not (sm/validate clr/schema:gradient value))
(u/display-not-valid :gradient value)
(u/not-valid plugin-id :gradient value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :gradient "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :gradient "Plugin doesn't have 'library:write' permission")
:else
(let [color (-> (u/proxy->library-color self)
@ -147,10 +147,10 @@
(let [value (parser/parse-image-data value)]
(cond
(not (sm/validate clr/schema:image value))
(u/display-not-valid :image value)
(u/not-valid plugin-id :image value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :image "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :image "Plugin doesn't have 'library:write' permission")
:else
(let [color (-> (u/proxy->library-color self)
@ -161,7 +161,7 @@
(fn []
(cond
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :remove "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :remove "Plugin doesn't have 'library:write' permission")
:else
(st/emit! (dwl/delete-color {:id id}))))
@ -170,7 +170,7 @@
(fn []
(cond
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :clone "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :clone "Plugin doesn't have 'library:write' permission")
:else
(let [color-id (uuid/next)
@ -207,7 +207,7 @@
(fn [key]
(cond
(not (string? key))
(u/display-not-valid :getPluginData-key key)
(u/not-valid plugin-id :getPluginData-key key)
:else
(let [color (u/locate-library-color file-id id)]
@ -217,16 +217,16 @@
(fn [key value]
(cond
(not= file-id (:current-file-id @st/state))
(u/display-not-valid :setPluginData-non-local-library file-id)
(u/not-valid plugin-id :setPluginData-non-local-library file-id)
(not (string? key))
(u/display-not-valid :setPluginData-key key)
(u/not-valid plugin-id :setPluginData-key key)
(and (some? value) (not (string? value)))
(u/display-not-valid :setPluginData-value value)
(u/not-valid plugin-id :setPluginData-value value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :setPluginData "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :setPluginData "Plugin doesn't have 'library:write' permission")
:else
(st/emit! (dp/set-plugin-data file-id :color id (keyword "plugin" (str plugin-id)) key value))))
@ -240,10 +240,10 @@
(fn [namespace key]
(cond
(not (string? namespace))
(u/display-not-valid :getSharedPluginData-namespace namespace)
(u/not-valid plugin-id :getSharedPluginData-namespace namespace)
(not (string? key))
(u/display-not-valid :getSharedPluginData-key key)
(u/not-valid plugin-id :getSharedPluginData-key key)
:else
(let [color (u/locate-library-color file-id id)]
@ -253,19 +253,19 @@
(fn [namespace key value]
(cond
(not= file-id (:current-file-id @st/state))
(u/display-not-valid :setSharedPluginData-non-local-library file-id)
(u/not-valid plugin-id :setSharedPluginData-non-local-library file-id)
(not (string? namespace))
(u/display-not-valid :setSharedPluginData-namespace namespace)
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
(not (string? key))
(u/display-not-valid :setSharedPluginData-key key)
(u/not-valid plugin-id :setSharedPluginData-key key)
(and (some? value) (not (string? value)))
(u/display-not-valid :setSharedPluginData-value value)
(u/not-valid plugin-id :setSharedPluginData-value value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :setSharedPluginData "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'library:write' permission")
:else
(st/emit! (dp/set-plugin-data file-id :color id (keyword "shared" namespace) key value))))
@ -274,7 +274,7 @@
(fn [namespace]
(cond
(not (string? namespace))
(u/display-not-valid :getSharedPluginDataKeys-namespace namespace)
(u/not-valid plugin-id :getSharedPluginDataKeys-namespace namespace)
:else
(let [color (u/locate-library-color file-id id)]
@ -301,10 +301,10 @@
(fn [self value]
(cond
(not (string? value))
(u/display-not-valid :name value)
(u/not-valid plugin-id :name value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :name "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :name "Plugin doesn't have 'library:write' permission")
:else
(let [typo (u/proxy->library-typography self)
@ -318,10 +318,10 @@
(fn [self value]
(cond
(not (string? value))
(u/display-not-valid :path value)
(u/not-valid plugin-id :path value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :path "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :path "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
@ -335,10 +335,10 @@
(fn [self value]
(cond
(not (string? value))
(u/display-not-valid :fontId value)
(u/not-valid plugin-id :fontId value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :fontId "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :fontId "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
@ -352,10 +352,10 @@
(fn [self value]
(cond
(not (string? value))
(u/display-not-valid :fontFamily value)
(u/not-valid plugin-id :fontFamily value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :fontFamily "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :fontFamily "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
@ -369,10 +369,10 @@
(fn [self value]
(cond
(not (string? value))
(u/display-not-valid :fontVariantId value)
(u/not-valid plugin-id :fontVariantId value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :fontVariantId "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :fontVariantId "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
@ -386,10 +386,10 @@
(fn [self value]
(cond
(not (string? value))
(u/display-not-valid :fontSize value)
(u/not-valid plugin-id :fontSize value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :fontSize "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :fontSize "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
@ -403,10 +403,10 @@
(fn [self value]
(cond
(not (string? value))
(u/display-not-valid :fontWeight value)
(u/not-valid plugin-id :fontWeight value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :fontWeight "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :fontWeight "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
@ -420,10 +420,10 @@
(fn [self value]
(cond
(not (string? value))
(u/display-not-valid :fontStyle value)
(u/not-valid plugin-id :fontStyle value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :fontStyle "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :fontStyle "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
@ -437,10 +437,10 @@
(fn [self value]
(cond
(not (string? value))
(u/display-not-valid :lineHeight value)
(u/not-valid plugin-id :lineHeight value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :lineHeight "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :lineHeight "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
@ -454,10 +454,10 @@
(fn [self value]
(cond
(not (string? value))
(u/display-not-valid :letterSpacing value)
(u/not-valid plugin-id :letterSpacing value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :letterSpacing "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :letterSpacing "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
@ -471,10 +471,10 @@
(fn [self value]
(cond
(not (string? value))
(u/display-not-valid :textTransform value)
(u/not-valid plugin-id :textTransform value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :textTransform "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :textTransform "Plugin doesn't have 'library:write' permission")
:else
(let [typo (-> (u/proxy->library-typography self)
@ -485,7 +485,7 @@
(fn []
(cond
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :remove "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :remove "Plugin doesn't have 'library:write' permission")
:else
(st/emit! (dwl/delete-typography {:id id}))))
@ -494,7 +494,7 @@
(fn []
(cond
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :clone "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :clone "Plugin doesn't have 'library:write' permission")
:else
(let [typo-id (uuid/next)
@ -507,10 +507,10 @@
(fn [shape]
(cond
(not (shape/shape-proxy? shape))
(u/display-not-valid :applyToText shape)
(u/not-valid plugin-id :applyToText shape)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :applyToText "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :applyToText "Plugin doesn't have 'content:write' permission")
:else
(let [shape-id (obj/get shape "$id")
@ -521,10 +521,10 @@
(fn [range]
(cond
(not (text/text-range-proxy? range))
(u/display-not-valid :applyToText range)
(u/not-valid plugin-id :applyToText range)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :applyToText "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :applyToText "Plugin doesn't have 'content:write' permission")
:else
(let [shape-id (obj/get range "$id")
@ -542,7 +542,7 @@
(fn [key]
(cond
(not (string? key))
(u/display-not-valid :typography-plugin-data-key key)
(u/not-valid plugin-id :typography-plugin-data-key key)
:else
(let [typography (u/locate-library-typography file-id id)]
@ -552,16 +552,16 @@
(fn [key value]
(cond
(not= file-id (:current-file-id @st/state))
(u/display-not-valid :setPluginData-non-local-library file-id)
(u/not-valid plugin-id :setPluginData-non-local-library file-id)
(not (string? key))
(u/display-not-valid :setPluginData-key key)
(u/not-valid plugin-id :setPluginData-key key)
(and (some? value) (not (string? value)))
(u/display-not-valid :setPluginData-value value)
(u/not-valid plugin-id :setPluginData-value value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :setPluginData "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :setPluginData "Plugin doesn't have 'library:write' permission")
:else
(st/emit! (dp/set-plugin-data file-id :typography id (keyword "plugin" (str plugin-id)) key value))))
@ -575,10 +575,10 @@
(fn [namespace key]
(cond
(not (string? namespace))
(u/display-not-valid :getSharedPluginData-namespace namespace)
(u/not-valid plugin-id :getSharedPluginData-namespace namespace)
(not (string? key))
(u/display-not-valid :getSharedPluginData-key key)
(u/not-valid plugin-id :getSharedPluginData-key key)
:else
(let [typography (u/locate-library-typography file-id id)]
@ -588,19 +588,19 @@
(fn [namespace key value]
(cond
(not= file-id (:current-file-id @st/state))
(u/display-not-valid :setSharedPluginData-non-local-library file-id)
(u/not-valid plugin-id :setSharedPluginData-non-local-library file-id)
(not (string? namespace))
(u/display-not-valid :setSharedPluginData-namespace namespace)
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
(not (string? key))
(u/display-not-valid :setSharedPluginData-key key)
(u/not-valid plugin-id :setSharedPluginData-key key)
(and (some? value) (not (string? value)))
(u/display-not-valid :setSharedPluginData-value value)
(u/not-valid plugin-id :setSharedPluginData-value value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :setSharedPluginData "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'library:write' permission")
:else
(st/emit! (dp/set-plugin-data file-id :typography id (keyword "shared" namespace) key value))))
@ -609,7 +609,7 @@
(fn [namespace]
(cond
(not (string? namespace))
(u/display-not-valid :getSharedPluginDataKeys-namespace namespace)
(u/not-valid plugin-id :getSharedPluginDataKeys-namespace namespace)
:else
(let [typography (u/locate-library-typography file-id id)]
@ -674,7 +674,7 @@
:removeProperty
(fn [pos]
(if (not (nat-int? pos))
(u/display-not-valid :pos pos)
(u/not-valid plugin-id :pos pos)
(st/emit!
(ev/event {::ev/name "remove-property" ::ev/origin "plugin:remove-property"})
(dwv/remove-property id pos))))
@ -683,10 +683,10 @@
(fn [pos name]
(cond
(not (nat-int? pos))
(u/display-not-valid :pos pos)
(u/not-valid plugin-id :pos pos)
(not (string? name))
(u/display-not-valid :name name)
(u/not-valid plugin-id :name name)
:else
(st/emit!
@ -715,10 +715,10 @@
(fn [self value]
(cond
(not (string? value))
(u/display-not-valid :name value)
(u/not-valid plugin-id :name value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :name "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :name "Plugin doesn't have 'library:write' permission")
:else
(let [component (u/proxy->library-component self)
@ -732,10 +732,10 @@
(fn [self value]
(cond
(not (string? value))
(u/display-not-valid :path value)
(u/not-valid plugin-id :path value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :path "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :path "Plugin doesn't have 'library:write' permission")
:else
(let [component (u/proxy->library-component self)
@ -746,7 +746,7 @@
(fn []
(cond
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :remove "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :remove "Plugin doesn't have 'library:write' permission")
:else
(st/emit! (dwl/delete-component {:id id}))))
@ -755,7 +755,7 @@
(fn []
(cond
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :instance "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :instance "Plugin doesn't have 'content:write' permission")
:else
(let [id-ref (atom nil)]
@ -766,7 +766,7 @@
(fn [key]
(cond
(not (string? key))
(u/display-not-valid :component-plugin-data-key key)
(u/not-valid plugin-id :component-plugin-data-key key)
:else
(let [component (u/locate-library-component file-id id)]
@ -776,16 +776,16 @@
(fn [key value]
(cond
(not= file-id (:current-file-id @st/state))
(u/display-not-valid :setPluginData-non-local-library file-id)
(u/not-valid plugin-id :setPluginData-non-local-library file-id)
(not (string? key))
(u/display-not-valid :setPluginData-key key)
(u/not-valid plugin-id :setPluginData-key key)
(and (some? value) (not (string? value)))
(u/display-not-valid :setPluginData-value value)
(u/not-valid plugin-id :setPluginData-value value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :setPluginData "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :setPluginData "Plugin doesn't have 'library:write' permission")
:else
(st/emit! (dp/set-plugin-data file-id :component id (keyword "plugin" (str plugin-id)) key value))))
@ -799,10 +799,10 @@
(fn [namespace key]
(cond
(not (string? namespace))
(u/display-not-valid :component-plugin-data-namespace namespace)
(u/not-valid plugin-id :component-plugin-data-namespace namespace)
(not (string? key))
(u/display-not-valid :component-plugin-data-key key)
(u/not-valid plugin-id :component-plugin-data-key key)
:else
(let [component (u/locate-library-component file-id id)]
@ -812,19 +812,19 @@
(fn [namespace key value]
(cond
(not= file-id (:current-file-id @st/state))
(u/display-not-valid :setSharedPluginData-non-local-library file-id)
(u/not-valid plugin-id :setSharedPluginData-non-local-library file-id)
(not (string? namespace))
(u/display-not-valid :setSharedPluginData-namespace namespace)
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
(not (string? key))
(u/display-not-valid :setSharedPluginData-key key)
(u/not-valid plugin-id :setSharedPluginData-key key)
(and (some? value) (not (string? value)))
(u/display-not-valid :setSharedPluginData-value value)
(u/not-valid plugin-id :setSharedPluginData-value value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :setSharedPluginData "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'library:write' permission")
:else
(st/emit! (dp/set-plugin-data file-id :component id (keyword "shared" namespace) key value))))
@ -833,7 +833,7 @@
(fn [namespace]
(cond
(not (string? namespace))
(u/display-not-valid :component-plugin-data-namespace namespace)
(u/not-valid plugin-id :component-plugin-data-namespace namespace)
:else
(let [component (u/locate-library-component file-id id)]
@ -901,10 +901,10 @@
(fn [pos value]
(cond
(not (nat-int? pos))
(u/display-not-valid :pos (str pos))
(u/not-valid plugin-id :pos (str pos))
(not (string? value))
(u/display-not-valid :name value)
(u/not-valid plugin-id :name value)
:else
(st/emit!
@ -970,7 +970,7 @@
(fn []
(cond
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :createColor "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :createColor "Plugin doesn't have 'library:write' permission")
:else
(let [color-id (uuid/next)]
@ -981,7 +981,7 @@
(fn []
(cond
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :createTypography "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :createTypography "Plugin doesn't have 'library:write' permission")
:else
(let [typography-id (uuid/next)]
@ -992,7 +992,7 @@
(fn [shapes]
(cond
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :createComponent "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :createComponent "Plugin doesn't have 'library:write' permission")
:else
(let [id-ref (atom nil)
@ -1005,7 +1005,7 @@
(fn [key]
(cond
(not (string? key))
(u/display-not-valid :file-plugin-data-key key)
(u/not-valid plugin-id :file-plugin-data-key key)
:else
(let [file (u/locate-file file-id)]
@ -1015,13 +1015,13 @@
(fn [key value]
(cond
(not (string? key))
(u/display-not-valid :setPluginData-key key)
(u/not-valid plugin-id :setPluginData-key key)
(and (some? value) (not (string? value)))
(u/display-not-valid :setPluginData-value value)
(u/not-valid plugin-id :setPluginData-value value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :setPluginData "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :setPluginData "Plugin doesn't have 'library:write' permission")
:else
(st/emit! (dp/set-plugin-data file-id :file (keyword "plugin" (str plugin-id)) key value))))
@ -1035,10 +1035,10 @@
(fn [namespace key]
(cond
(not (string? namespace))
(u/display-not-valid :file-plugin-data-namespace namespace)
(u/not-valid plugin-id :file-plugin-data-namespace namespace)
(not (string? key))
(u/display-not-valid :file-plugin-data-key key)
(u/not-valid plugin-id :file-plugin-data-key key)
:else
(let [file (u/locate-file file-id)]
@ -1048,16 +1048,16 @@
(fn [namespace key value]
(cond
(not (string? namespace))
(u/display-not-valid :setSharedPluginData-namespace namespace)
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
(not (string? key))
(u/display-not-valid :setSharedPluginData-key key)
(u/not-valid plugin-id :setSharedPluginData-key key)
(and (some? value) (not (string? value)))
(u/display-not-valid :setSharedPluginData-value value)
(u/not-valid plugin-id :setSharedPluginData-value value)
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :setSharedPluginData "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'library:write' permission")
:else
(st/emit! (dp/set-plugin-data file-id :file (keyword "shared" namespace) key value))))
@ -1066,7 +1066,7 @@
(fn [namespace]
(cond
(not (string? namespace))
(u/display-not-valid :namespace namespace)
(u/not-valid plugin-id :namespace namespace)
:else
(let [file (u/locate-file file-id)]
@ -1110,14 +1110,14 @@
(fn [library-id]
(cond
(not (r/check-permission plugin-id "library:write"))
(u/display-not-valid :connectLibrary "Plugin doesn't have 'library:write' permission")
(u/not-valid plugin-id :connectLibrary "Plugin doesn't have 'library:write' permission")
:else
(js/Promise.
(fn [resolve reject]
(cond
(not (string? library-id))
(do (u/display-not-valid :connectLibrary library-id)
(do (u/not-valid plugin-id :connectLibrary library-id)
(reject nil))
:else

View File

@ -30,10 +30,10 @@
(fn [key]
(cond
(not (r/check-permission plugin-id "allow:localstorage"))
(u/display-not-valid :getItem "Plugin doesn't have 'allow:localstorage' permission")
(u/not-valid plugin-id :getItem "Plugin doesn't have 'allow:localstorage' permission")
(not (string? key))
(u/display-not-valid :getItem "The key must be a string")
(u/not-valid plugin-id :getItem "The key must be a string")
:else
(.getItem ^js local-storage (prefix-key plugin-id key))))
@ -42,10 +42,10 @@
(fn [key value]
(cond
(not (r/check-permission plugin-id "allow:localstorage"))
(u/display-not-valid :setItem "Plugin doesn't have 'allow:localstorage' permission")
(u/not-valid plugin-id :setItem "Plugin doesn't have 'allow:localstorage' permission")
(not (string? key))
(u/display-not-valid :setItem "The key must be a string")
(u/not-valid plugin-id :setItem "The key must be a string")
:else
(.setItem ^js local-storage (prefix-key plugin-id key) value)))
@ -54,10 +54,10 @@
(fn [key]
(cond
(not (r/check-permission plugin-id "allow:localstorage"))
(u/display-not-valid :removeItem "Plugin doesn't have 'allow:localstorage' permission")
(u/not-valid plugin-id :removeItem "Plugin doesn't have 'allow:localstorage' permission")
(not (string? key))
(u/display-not-valid :removeItem "The key must be a string")
(u/not-valid plugin-id :removeItem "The key must be a string")
:else
(.getItem ^js local-storage (prefix-key plugin-id key))))

View File

@ -59,7 +59,7 @@
(fn [_ value]
(cond
(or (not (string? value)) (empty? value))
(u/display-not-valid :name value)
(u/not-valid plugin-id :name value)
:else
(st/emit! (dwi/update-flow page-id id #(assoc % :name value)))))}
@ -74,7 +74,7 @@
(fn [_ value]
(cond
(not (shape/shape-proxy? value))
(u/display-not-valid :startingBoard value)
(u/not-valid plugin-id :startingBoard value)
:else
(st/emit! (dwi/update-flow page-id id #(assoc % :starting-frame (obj/get value "$id"))))))}
@ -103,10 +103,10 @@
(fn [_ value]
(cond
(not (string? value))
(u/display-not-valid :name value)
(u/not-valid plugin-id :name value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :name "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dw/rename-page id value))))}
@ -127,10 +127,10 @@
(fn [_ value]
(cond
(or (not (string? value)) (not (cc/valid-hex-color? value)))
(u/display-not-valid :background value)
(u/not-valid plugin-id :background value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :background "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :background "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dw/change-canvas-color id {:color value}))))}
@ -158,7 +158,7 @@
(fn [shape-id]
(cond
(not (string? shape-id))
(u/display-not-valid :getShapeById shape-id)
(u/not-valid plugin-id :getShapeById shape-id)
:else
(let [shape-id (uuid/parse shape-id)
@ -195,7 +195,7 @@
(fn [key]
(cond
(not (string? key))
(u/display-not-valid :page-plugin-data-key key)
(u/not-valid plugin-id :page-plugin-data-key key)
:else
(let [page (u/locate-page file-id id)]
@ -205,13 +205,13 @@
(fn [key value]
(cond
(not (string? key))
(u/display-not-valid :setPluginData-key key)
(u/not-valid plugin-id :setPluginData-key key)
(and (some? value) (not (string? value)))
(u/display-not-valid :setPluginData-value value)
(u/not-valid plugin-id :setPluginData-value value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :setPluginData "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :setPluginData "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dp/set-plugin-data file-id :page id (keyword "plugin" (str plugin-id)) key value))))
@ -225,10 +225,10 @@
(fn [namespace key]
(cond
(not (string? namespace))
(u/display-not-valid :page-plugin-data-namespace namespace)
(u/not-valid plugin-id :page-plugin-data-namespace namespace)
(not (string? key))
(u/display-not-valid :page-plugin-data-key key)
(u/not-valid plugin-id :page-plugin-data-key key)
:else
(let [page (u/locate-page file-id id)]
@ -238,16 +238,16 @@
(fn [namespace key value]
(cond
(not (string? namespace))
(u/display-not-valid :setSharedPluginData-namespace namespace)
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
(not (string? key))
(u/display-not-valid :setSharedPluginData-key key)
(u/not-valid plugin-id :setSharedPluginData-key key)
(and (some? value) (not (string? value)))
(u/display-not-valid :setSharedPluginData-value value)
(u/not-valid plugin-id :setSharedPluginData-value value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :setSharedPluginData "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dp/set-plugin-data file-id :page id (keyword "shared" namespace) key value))))
@ -256,7 +256,7 @@
(fn [self namespace]
(cond
(not (string? namespace))
(u/display-not-valid :page-plugin-data-namespace namespace)
(u/not-valid plugin-id :page-plugin-data-namespace namespace)
:else
(let [page (u/proxy->page self)]
@ -266,7 +266,7 @@
(fn [new-window]
(cond
(not (r/check-permission plugin-id "content:read"))
(u/display-not-valid :openPage "Plugin doesn't have 'content:read' permission")
(u/not-valid plugin-id :openPage "Plugin doesn't have 'content:read' permission")
:else
(let [new-window (if (boolean? new-window) new-window false)]
@ -276,10 +276,10 @@
(fn [name frame]
(cond
(or (not (string? name)) (empty? name))
(u/display-not-valid :createFlow-name name)
(u/not-valid plugin-id :createFlow-name name)
(not (shape/shape-proxy? frame))
(u/display-not-valid :createFlow-frame frame)
(u/not-valid plugin-id :createFlow-frame frame)
:else
(let [flow-id (uuid/next)]
@ -290,7 +290,7 @@
(fn [flow]
(cond
(not (flow-proxy? flow))
(u/display-not-valid :removeFlow-flow flow)
(u/not-valid plugin-id :removeFlow-flow flow)
:else
(st/emit! (dwi/remove-flow id (obj/get flow "$id")))))
@ -300,18 +300,18 @@
(let [shape (u/proxy->shape board)]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :addRulerGuide "Value not a safe number")
(u/not-valid plugin-id :addRulerGuide "Value not a safe number")
(not (contains? #{"vertical" "horizontal"} orientation))
(u/display-not-valid :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'")
(u/not-valid plugin-id :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'")
(and (some? shape)
(or (not (shape/shape-proxy? board))
(not (cfh/frame-shape? shape))))
(u/display-not-valid :addRulerGuide "The shape is not a board")
(u/not-valid plugin-id :addRulerGuide "The shape is not a board")
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :addRulerGuide "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :addRulerGuide "Plugin doesn't have 'content:write' permission")
:else
(let [ruler-id (uuid/next)]
@ -328,10 +328,10 @@
(fn [value]
(cond
(not (rg/ruler-guide-proxy? value))
(u/display-not-valid :removeRulerGuide "Guide not provided")
(u/not-valid plugin-id :removeRulerGuide "Guide not provided")
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :removeRulerGuide "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :removeRulerGuide "Plugin doesn't have 'comment:write' permission")
:else
(let [guide (u/proxy->ruler-guide value)]
@ -343,17 +343,17 @@
position (parser/parse-point position)]
(cond
(or (not (string? content)) (empty? content))
(u/display-not-valid :addCommentThread "Content not valid")
(u/not-valid plugin-id :addCommentThread "Content not valid")
(or (not (sm/valid-safe-number? (:x position)))
(not (sm/valid-safe-number? (:y position))))
(u/display-not-valid :addCommentThread "Position not valid")
(u/not-valid plugin-id :addCommentThread "Position not valid")
(and (some? board) (or (not (shape/shape-proxy? board)) (not (cfh/frame-shape? shape))))
(u/display-not-valid :addCommentThread "Board not valid")
(u/not-valid plugin-id :addCommentThread "Board not valid")
(not (r/check-permission plugin-id "comment:write"))
(u/display-not-valid :addCommentThread "Plugin doesn't have 'comment:write' permission")
(u/not-valid plugin-id :addCommentThread "Plugin doesn't have 'comment:write' permission")
:else
(let [position
@ -378,10 +378,10 @@
(fn [thread]
(cond
(not (pc/comment-thread-proxy? thread))
(u/display-not-valid :removeCommentThread "Comment thread not valid")
(u/not-valid plugin-id :removeCommentThread "Comment thread not valid")
(not (r/check-permission plugin-id "comment:write"))
(u/display-not-valid :removeCommentThread "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :removeCommentThread "Plugin doesn't have 'content:write' permission")
:else
(js/Promise.
@ -400,7 +400,7 @@
(cond
(not (r/check-permission plugin-id "comment:read"))
(do
(u/display-not-valid :findCommentThreads "Plugin doesn't have 'comment:read' permission")
(u/not-valid plugin-id :findCommentThreads "Plugin doesn't have 'comment:read' permission")
(reject "Plugin doesn't have 'comment:read' permission"))
:else

View File

@ -14,10 +14,10 @@
[app.plugins.utils :as u]))
(defn ^:export centerShapes
[shapes]
[plugin-id shapes]
(cond
(not (every? shape/shape-proxy? shapes))
(u/display-not-valid :centerShapes shapes)
(u/not-valid plugin-id :centerShapes shapes)
:else
(let [shapes (->> shapes (map u/proxy->shape))]

View File

@ -44,13 +44,13 @@
(let [shape (u/locate-shape file-id page-id (obj/get value "$id"))]
(cond
(not (shape-proxy? value))
(u/display-not-valid :board "The board is not a shape proxy")
(u/not-valid plugin-id :board "The board is not a shape proxy")
(not (cfh/frame-shape? shape))
(u/display-not-valid :board "The shape is not a board")
(u/not-valid plugin-id :board "The shape is not a board")
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :board "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :board "Plugin doesn't have 'content:write' permission")
:else
(let [board-id (when value (obj/get value "$id"))
@ -78,10 +78,10 @@
(fn [self value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :position "Not valid position")
(u/not-valid plugin-id :position "Not valid position")
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :position "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :position "Plugin doesn't have 'content:write' permission")
:else
(let [guide (u/proxy->ruler-guide self)

View File

@ -31,7 +31,6 @@
[app.common.types.shape.radius :as ctsr]
[app.common.types.shape.shadow :as ctss]
[app.common.types.text :as txt]
[app.common.types.token :as cto]
[app.common.uuid :as uuid]
[app.main.data.plugins :as dp]
[app.main.data.workspace :as dw]
@ -47,7 +46,6 @@
[app.main.data.workspace.variants :as dwv]
[app.main.repo :as rp]
[app.main.store :as st]
[app.plugins.flags :refer [natural-child-ordering?]]
[app.plugins.flex :as flex]
[app.plugins.format :as format]
[app.plugins.grid :as grid]
@ -55,6 +53,7 @@
[app.plugins.register :as r]
[app.plugins.ruler-guides :as rg]
[app.plugins.text :as text]
[app.plugins.tokens :refer [applied-tokens-plugin->applied-tokens token-attr-plugin->token-attr token-attr?]]
[app.plugins.utils :as u]
[app.util.http :as http]
[app.util.object :as obj]
@ -91,7 +90,7 @@
(let [value (parser/parse-keyword value)]
(cond
(not (contains? ctsi/event-types value))
(u/display-not-valid :trigger value)
(u/not-valid plugin-id :trigger value)
:else
(st/emit! (dwi/update-interaction
@ -107,7 +106,7 @@
(fn [_ value]
(cond
(or (not (number? value)) (not (pos? value)))
(u/display-not-valid :delay value)
(u/not-valid plugin-id :delay value)
:else
(st/emit! (dwi/update-interaction
@ -127,7 +126,7 @@
(d/patch-object params))]
(cond
(not (sm/validate ctsi/schema:interaction interaction))
(u/display-not-valid :action interaction)
(u/not-valid plugin-id :action interaction)
:else
(st/emit! (dwi/update-interaction
@ -192,7 +191,8 @@
(assert (uuid? id))
(let [data (u/locate-shape file-id page-id id)]
(-> (obj/reify {:name "ShapeProxy" :on-error u/handle-error}
(-> (obj/reify {:name "ShapeProxy"
:on-error (u/handle-error plugin-id)}
:$plugin {:enumerable false :get (fn [] plugin-id)}
:$id {:enumerable false :get (fn [] id)}
:$file {:enumerable false :get (fn [] file-id)}
@ -218,10 +218,10 @@
(not (str/blank? value)))]
(cond
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :name "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission")
(not valid?)
(u/display-not-valid :name value)
(u/not-valid plugin-id :name value)
:else
(st/emit! (dw/rename-shape-or-variant file-id page-id id value)))))}
@ -233,10 +233,10 @@
(fn [self value]
(cond
(not (boolean? value))
(u/display-not-valid :blocked value)
(u/not-valid plugin-id :blocked value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :blocked "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :blocked "Plugin doesn't have 'content:write' permission")
:else
(let [id (obj/get self "$id")]
@ -249,10 +249,10 @@
(fn [self value]
(cond
(not (boolean? value))
(u/display-not-valid :hidden value)
(u/not-valid plugin-id :hidden value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :hidden "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :hidden "Plugin doesn't have 'content:write' permission")
:else
(let [id (obj/get self "$id")]
@ -265,10 +265,10 @@
(fn [self value]
(cond
(not (boolean? value))
(u/display-not-valid :visible value)
(u/not-valid plugin-id :visible value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :visible "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :visible "Plugin doesn't have 'content:write' permission")
:else
(let [id (obj/get self "$id")]
@ -281,10 +281,10 @@
(fn [self value]
(cond
(not (boolean? value))
(u/display-not-valid :proportionLock value)
(u/not-valid plugin-id :proportionLock value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :proportionLock "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :proportionLock "Plugin doesn't have 'content:write' permission")
:else
(let [id (obj/get self "$id")]
@ -299,10 +299,10 @@
value (keyword value)]
(cond
(not (contains? cts/horizontal-constraint-types value))
(u/display-not-valid :constraintsHorizontal value)
(u/not-valid plugin-id :constraintsHorizontal value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :constraintsHorizontal "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :constraintsHorizontal "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :constraints-h value))))))}
@ -316,10 +316,10 @@
value (keyword value)]
(cond
(not (contains? cts/vertical-constraint-types value))
(u/display-not-valid :constraintsVertical value)
(u/not-valid plugin-id :constraintsVertical value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :constraintsVertical "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :constraintsVertical "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :constraints-v value))))))}
@ -332,10 +332,10 @@
(let [id (obj/get self "$id")]
(cond
(or (not (sm/valid-safe-int? value)) (< value 0))
(u/display-not-valid :borderRadius value)
(u/not-valid plugin-id :borderRadius value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :borderRadius "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :borderRadius "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-all-corners % value))))))}
@ -348,10 +348,10 @@
(let [id (obj/get self "$id")]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :borderRadiusTopLeft value)
(u/not-valid plugin-id :borderRadiusTopLeft value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :borderRadiusTopLeft "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :borderRadiusTopLeft "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r1 value))))))}
@ -364,10 +364,10 @@
(let [id (obj/get self "$id")]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :borderRadiusTopRight value)
(u/not-valid plugin-id :borderRadiusTopRight value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :borderRadiusTopRight "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :borderRadiusTopRight "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r2 value))))))}
@ -380,10 +380,10 @@
(let [id (obj/get self "$id")]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :borderRadiusBottomRight value)
(u/not-valid plugin-id :borderRadiusBottomRight value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :borderRadiusBottomRight "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :borderRadiusBottomRight "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r3 value))))))}
@ -396,10 +396,10 @@
(let [id (obj/get self "$id")]
(cond
(not (sm/valid-safe-int? value))
(u/display-not-valid :borderRadiusBottomLeft value)
(u/not-valid plugin-id :borderRadiusBottomLeft value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :borderRadiusBottomLeft "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :borderRadiusBottomLeft "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r4 value))))))}
@ -412,10 +412,10 @@
(let [id (obj/get self "$id")]
(cond
(or (not (sm/valid-safe-number? value)) (< value 0) (> value 1))
(u/display-not-valid :opacity value)
(u/not-valid plugin-id :opacity value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :opacity "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :opacity "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :opacity value))))))}
@ -429,10 +429,10 @@
value (keyword value)]
(cond
(not (contains? cts/blend-modes value))
(u/display-not-valid :blendMode value)
(u/not-valid plugin-id :blendMode value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :blendMode "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :blendMode "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :blend-mode value))))))}
@ -446,10 +446,10 @@
value (mapv #(shadow-defaults (parser/parse-shadow %)) value)]
(cond
(not (sm/validate [:vector ctss/schema:shadow] value))
(u/display-not-valid :shadows value)
(u/not-valid plugin-id :shadows value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :shadows "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :shadows "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :shadow value))))))}
@ -465,10 +465,10 @@
value (blur-defaults (parser/parse-blur value))]
(cond
(not (sm/validate ctsb/schema:blur value))
(u/display-not-valid :blur value)
(u/not-valid plugin-id :blur value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :blur "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :blur "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :blur value)))))))}
@ -482,10 +482,10 @@
value (parser/parse-exports value)]
(cond
(not (sm/validate [:vector ctse/schema:export] value))
(u/display-not-valid :exports value)
(u/not-valid plugin-id :exports value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :exports "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :exports "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :exports value))))))}
@ -499,10 +499,10 @@
(let [id (obj/get self "$id")]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :x value)
(u/not-valid plugin-id :x value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :x "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :x "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dw/update-position id
@ -517,10 +517,10 @@
(let [id (obj/get self "$id")]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :y value)
(u/not-valid plugin-id :y value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :y "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :y "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dw/update-position id
@ -562,10 +562,10 @@
(fn [self value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :parentX value)
(u/not-valid plugin-id :parentX value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :parentX "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :parentX "Plugin doesn't have 'content:write' permission")
:else
(let [id (obj/get self "$id")
@ -589,10 +589,10 @@
(fn [self value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :parentY value)
(u/not-valid plugin-id :parentY value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :parentY "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :parentY "Plugin doesn't have 'content:write' permission")
:else
(let [id (obj/get self "$id")
@ -616,10 +616,10 @@
(fn [self value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :frameX value)
(u/not-valid plugin-id :frameX value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :frameX "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :frameX "Plugin doesn't have 'content:write' permission")
:else
(let [id (obj/get self "$id")
@ -643,10 +643,10 @@
(fn [self value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :frameY value)
(u/not-valid plugin-id :frameY value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :frameY "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :frameY "Plugin doesn't have 'content:write' permission")
:else
(let [id (obj/get self "$id")
@ -680,10 +680,10 @@
(fn [self value]
(cond
(not (number? value))
(u/display-not-valid :rotation value)
(u/not-valid plugin-id :rotation value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :rotation "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :rotation "Plugin doesn't have 'content:write' permission")
:else
(let [shape (u/proxy->shape self)]
@ -696,10 +696,10 @@
(fn [self value]
(cond
(not (boolean? value))
(u/display-not-valid :flipX value)
(u/not-valid plugin-id :flipX value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :flipX "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :flipX "Plugin doesn't have 'content:write' permission")
:else
(let [id (obj/get self "$id")]
@ -712,10 +712,10 @@
(fn [self value]
(cond
(not (boolean? value))
(u/display-not-valid :flipY value)
(u/not-valid plugin-id :flipY value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :flipY "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :flipY "Plugin doesn't have 'content:write' permission")
:else
(let [id (obj/get self "$id")]
@ -734,13 +734,13 @@
value (parser/parse-fills value)]
(cond
(not (sm/validate [:vector types.fills/schema:fill] value))
(u/display-not-valid :fills value)
(u/not-valid plugin-id :fills value)
(cfh/text-shape? shape)
(st/emit! (dwt/update-attrs id {:fills value}))
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :fills "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :fills "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :fills value))))))}
@ -754,10 +754,10 @@
value (parser/parse-strokes value)]
(cond
(not (sm/validate [:vector cts/schema:stroke] value))
(u/display-not-valid :strokes value)
(u/not-valid plugin-id :strokes value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :strokes "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :strokes "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :strokes value))))))}
@ -802,13 +802,13 @@
(fn [width height]
(cond
(or (not (sm/valid-safe-number? width)) (<= width 0))
(u/display-not-valid :resize width)
(u/not-valid plugin-id :resize width)
(or (not (sm/valid-safe-number? height)) (<= height 0))
(u/display-not-valid :resize height)
(u/not-valid plugin-id :resize height)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :resize "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :resize "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dw/update-dimensions [id] :width width)
@ -819,13 +819,13 @@
(let [center (when center {:x (obj/get center "x") :y (obj/get center "y")})]
(cond
(not (number? angle))
(u/display-not-valid :rotate-angle angle)
(u/not-valid plugin-id :rotate-angle angle)
(and (some? center) (or (not (number? (:x center))) (not (number? (:y center)))))
(u/display-not-valid :rotate-center center)
(u/not-valid plugin-id :rotate-center center)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :rotate "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :rotate "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dw/increase-rotation [id] angle {:center center :delta? true})))))
@ -835,7 +835,7 @@
(let [ret-v (atom nil)]
(cond
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :clone "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :clone "Plugin doesn't have 'content:write' permission")
:else
(do (st/emit! (dws/duplicate-shapes #{id} :change-selection? false :return-ref ret-v))
@ -845,7 +845,7 @@
(fn []
(cond
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :remove "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/delete-shapes #{id}))))
@ -855,7 +855,7 @@
(fn [key]
(cond
(not (string? key))
(u/display-not-valid :getPluginData key)
(u/not-valid plugin-id :getPluginData key)
:else
(let [shape (u/locate-shape file-id page-id id)]
@ -865,13 +865,13 @@
(fn [key value]
(cond
(not (string? key))
(u/display-not-valid :setPluginData-key key)
(u/not-valid plugin-id :setPluginData-key key)
(and (some? value) (not (string? value)))
(u/display-not-valid :setPluginData-value value)
(u/not-valid plugin-id :setPluginData-value value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :setPluginData "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :setPluginData "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dp/set-plugin-data file-id :shape id page-id (keyword "plugin" (str plugin-id)) key value))))
@ -885,10 +885,10 @@
(fn [namespace key]
(cond
(not (string? namespace))
(u/display-not-valid :getSharedPluginData-namespace namespace)
(u/not-valid plugin-id :getSharedPluginData-namespace namespace)
(not (string? key))
(u/display-not-valid :getSharedPluginData-key key)
(u/not-valid plugin-id :getSharedPluginData-key key)
:else
(let [shape (u/locate-shape file-id page-id id)]
@ -898,16 +898,16 @@
(fn [namespace key value]
(cond
(not (string? namespace))
(u/display-not-valid :setSharedPluginData-namespace namespace)
(u/not-valid plugin-id :setSharedPluginData-namespace namespace)
(not (string? key))
(u/display-not-valid :setSharedPluginData-key key)
(u/not-valid plugin-id :setSharedPluginData-key key)
(and (some? value) (not (string? value)))
(u/display-not-valid :setSharedPluginData-value value)
(u/not-valid plugin-id :setSharedPluginData-value value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :setSharedPluginData "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dp/set-plugin-data file-id :shape id page-id (keyword "shared" namespace) key value))))
@ -916,7 +916,7 @@
(fn [namespace]
(cond
(not (string? namespace))
(u/display-not-valid :getSharedPluginDataKeys namespace)
(u/not-valid plugin-id :getSharedPluginDataKeys namespace)
:else
(let [shape (u/locate-shape file-id page-id id)]
@ -931,12 +931,12 @@
(not (cfh/group-shape? shape))
(not (cfh/svg-raw-shape? shape))
(not (cfh/bool-shape? shape)))
(u/display-not-valid :getChildren (:type shape))
(u/not-valid plugin-id :getChildren (:type shape))
:else
(let [is-reversed? (ctl/flex-layout? shape)
reverse-fn
(if (and (natural-child-ordering? plugin-id) is-reversed?)
(if (and (u/natural-child-ordering? plugin-id) is-reversed?)
reverse identity)]
(->> (u/locate-shape file-id page-id id)
(:shapes)
@ -951,19 +951,19 @@
(not (cfh/group-shape? shape))
(not (cfh/svg-raw-shape? shape))
(not (cfh/bool-shape? shape)))
(u/display-not-valid :appendChild (:type shape))
(u/not-valid plugin-id :appendChild (:type shape))
(not (shape-proxy? child))
(u/display-not-valid :appendChild-child child)
(u/not-valid plugin-id :appendChild-child child)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :appendChild "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :appendChild "Plugin doesn't have 'content:write' permission")
:else
(let [child-id (obj/get child "$id")
is-reversed? (ctl/flex-layout? shape)
index
(if (or (not (natural-child-ordering? plugin-id)) is-reversed?)
(if (or (not (u/natural-child-ordering? plugin-id)) is-reversed?)
0
(count (:shapes shape)))]
(st/emit! (dwsh/relocate-shapes #{child-id} id index))))))
@ -976,19 +976,19 @@
(not (cfh/group-shape? shape))
(not (cfh/svg-raw-shape? shape))
(not (cfh/bool-shape? shape)))
(u/display-not-valid :insertChild (:type shape))
(u/not-valid plugin-id :insertChild (:type shape))
(not (shape-proxy? child))
(u/display-not-valid :insertChild-child child)
(u/not-valid plugin-id :insertChild-child child)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :insertChild "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :insertChild "Plugin doesn't have 'content:write' permission")
:else
(let [child-id (obj/get child "$id")
is-reversed? (ctl/flex-layout? shape)
index
(if (or (not (natural-child-ordering? plugin-id)) is-reversed?)
(if (or (not (u/natural-child-ordering? plugin-id)) is-reversed?)
(- (count (:shapes shape)) index)
index)]
(st/emit! (dwsh/relocate-shapes #{child-id} id index))))))
@ -999,10 +999,10 @@
(let [shape (u/locate-shape file-id page-id id)]
(cond
(not (cfh/frame-shape? shape))
(u/display-not-valid :addFlexLayout (:type shape))
(u/not-valid plugin-id :addFlexLayout (:type shape))
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :addFlexLayout "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :addFlexLayout "Plugin doesn't have 'content:write' permission")
:else
(do (st/emit! (dwsl/create-layout-from-id id :flex :from-frame? true :calculate-params? false))
@ -1013,10 +1013,10 @@
(let [shape (u/locate-shape file-id page-id id)]
(cond
(not (cfh/frame-shape? shape))
(u/display-not-valid :addGridLayout (:type shape))
(u/not-valid plugin-id :addGridLayout (:type shape))
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :addGridLayout "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :addGridLayout "Plugin doesn't have 'content:write' permission")
:else
(do (st/emit! (dwsl/create-layout-from-id id :grid :from-frame? true :calculate-params? false))
@ -1028,10 +1028,10 @@
(let [shape (u/locate-shape file-id page-id id)]
(cond
(not (cfh/group-shape? shape))
(u/display-not-valid :makeMask (:type shape))
(u/not-valid plugin-id :makeMask (:type shape))
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :makeMask "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :makeMask "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwg/mask-group #{id})))))
@ -1041,10 +1041,10 @@
(let [shape (u/locate-shape file-id page-id id)]
(cond
(not (cfh/mask-shape? shape))
(u/display-not-valid :removeMask (:type shape))
(u/not-valid plugin-id :removeMask (:type shape))
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :removeMask "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :removeMask "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwg/unmask-group #{id})))))
@ -1055,7 +1055,7 @@
(let [shape (u/locate-shape file-id page-id id)]
(cond
(and (not (cfh/path-shape? shape)) (not (cfh/bool-shape? shape)))
(u/display-not-valid :toD (:type shape))
(u/not-valid plugin-id :toD (:type shape))
:else
(.toString (:content shape)))))
@ -1066,13 +1066,13 @@
(let [shape (u/locate-shape file-id page-id id)]
(cond
(not (cfh/text-shape? shape))
(u/display-not-valid :getRange-shape "shape is not text")
(u/not-valid plugin-id :getRange-shape "shape is not text")
(or (not (sm/valid-safe-int? start)) (< start 0) (> start end))
(u/display-not-valid :getRange-start start)
(u/not-valid plugin-id :getRange-start start)
(not (sm/valid-safe-int? end))
(u/display-not-valid :getRange-end end)
(u/not-valid plugin-id :getRange-end end)
:else
(text/text-range-proxy plugin-id file-id page-id id start end))))
@ -1082,13 +1082,13 @@
(let [shape (u/locate-shape file-id page-id id)]
(cond
(not (lib-typography-proxy? typography))
(u/display-not-valid :applyTypography-typography typography)
(u/not-valid plugin-id :applyTypography-typography typography)
(not (cfh/text-shape? shape))
(u/display-not-valid :applyTypography-shape (:type shape))
(u/not-valid plugin-id :applyTypography-shape (:type shape))
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :applyTypography "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :applyTypography "Plugin doesn't have 'content:write' permission")
:else
(let [typography (u/proxy->library-typography typography)]
@ -1099,10 +1099,10 @@
(fn [index]
(cond
(not (sm/valid-safe-int? index))
(u/display-not-valid :setParentIndex index)
(u/not-valid plugin-id :setParentIndex index)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :setParentIndex "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :setParentIndex "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dw/set-shape-index file-id page-id id index))))
@ -1197,7 +1197,7 @@
(let [value (parser/parse-export value)]
(cond
(not (sm/validate ctse/schema:export value))
(u/display-not-valid :export value)
(u/not-valid plugin-id :export value)
:else
(let [shape (u/locate-shape file-id page-id id)
@ -1233,7 +1233,7 @@
(d/patch-object (parser/parse-interaction trigger action delay)))]
(cond
(not (sm/validate ctsi/schema:interaction interaction))
(u/display-not-valid :addInteraction interaction)
(u/not-valid plugin-id :addInteraction interaction)
:else
(let [index (-> (u/locate-shape file-id page-id id) (:interactions []) count)]
@ -1244,7 +1244,7 @@
(fn [interaction]
(cond
(not (interaction-proxy? interaction))
(u/display-not-valid :removeInteraction interaction)
(u/not-valid plugin-id :removeInteraction interaction)
:else
(st/emit! (dwi/remove-interaction {:id id} (obj/get interaction "$index")))))
@ -1255,16 +1255,16 @@
(let [shape (u/locate-shape file-id page-id id)]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :addRulerGuide "Value not a safe number")
(u/not-valid plugin-id :addRulerGuide "Value not a safe number")
(not (contains? #{"vertical" "horizontal"} orientation))
(u/display-not-valid :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'")
(u/not-valid plugin-id :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'")
(not (cfh/frame-shape? shape))
(u/display-not-valid :addRulerGuide "The shape is not a board")
(u/not-valid plugin-id :addRulerGuide "The shape is not a board")
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :addRulerGuide "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :addRulerGuide "Plugin doesn't have 'content:write' permission")
:else
(let [ruler-id (uuid/next)
@ -1285,10 +1285,10 @@
(fn [_ value]
(cond
(not (rg/ruler-guide-proxy? value))
(u/display-not-valid :removeRulerGuide "Guide not provided")
(u/not-valid plugin-id :removeRulerGuide "Guide not provided")
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :removeRulerGuide "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :removeRulerGuide "Plugin doesn't have 'content:write' permission")
:else
(let [guide (u/proxy->ruler-guide value)]
@ -1298,25 +1298,26 @@
{:this true
:get
(fn [_]
(let [tokens
(let [applied-tokens
(-> (u/locate-shape file-id page-id id)
(get :applied-tokens))]
(get :applied-tokens)
(applied-tokens-plugin->applied-tokens))]
(reduce
(fn [acc [prop name]]
(obj/set! acc (json/write-camel-key prop) name))
#js {}
tokens)))}
applied-tokens)))}
:applyToken
{:enumerable false
:schema [:tuple
[:fn token-proxy?]
[:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
[:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]]
:fn (fn [token attrs]
(let [token (u/locate-token file-id (obj/get token "$set-id") (obj/get token "$id"))
kw-attrs (into #{} (map keyword attrs))]
(if (some #(not (cto/token-attr? %)) kw-attrs)
(u/display-not-valid :applyToken attrs)
kw-attrs (into #{} (map token-attr-plugin->token-attr attrs))]
(if (some #(not (token-attr? %)) kw-attrs)
(u/not-valid plugin-id :applyToken attrs)
(st/emit!
(dwta/toggle-token {:token token
:attrs kw-attrs
@ -1338,10 +1339,10 @@
(fn [pos value]
(cond
(not (nat-int? pos))
(u/display-not-valid :pos pos)
(u/not-valid plugin-id :pos pos)
(not (string? value))
(u/display-not-valid :value value)
(u/not-valid plugin-id :value value)
:else
(let [shape (u/locate-shape file-id page-id id)
@ -1353,7 +1354,7 @@
(fn [ids]
(cond
(or (not (seq ids)) (not (every? uuid/parse* ids)))
(u/display-not-valid :ids ids)
(u/not-valid plugin-id :ids ids)
:else
(let [shape (u/locate-shape file-id page-id id)
@ -1381,21 +1382,21 @@
(fn [^js self children]
(cond
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :children "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :children "Plugin doesn't have 'content:write' permission")
(not (every? shape-proxy? children))
(u/display-not-valid :children "Every children needs to be shape proxies")
(u/not-valid plugin-id :children "Every children needs to be shape proxies")
:else
(let [shape (u/proxy->shape self)
file-id (obj/get self "$file")
page-id (obj/get self "$page")
reverse-fn (if (natural-child-ordering? plugin-id) reverse identity)
reverse-fn (if (u/natural-child-ordering? plugin-id) reverse identity)
ids (->> children reverse-fn (map #(obj/get % "$id")))]
(cond
(not= (set ids) (set (:shapes shape)))
(u/display-not-valid :children "Not all children are present in the input")
(u/not-valid plugin-id :children "Not all children are present in the input")
:else
(st/emit! (dw/reorder-children file-id page-id (:id shape) ids))))))}))
@ -1411,10 +1412,10 @@
(fn [_ value]
(cond
(not (boolean? value))
(u/display-not-valid :clipContent value)
(u/not-valid plugin-id :clipContent value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :clipContent "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :clipContent "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :show-content (not value))))))}
@ -1427,10 +1428,10 @@
(fn [_ value]
(cond
(not (boolean? value))
(u/display-not-valid :showInViewMode value)
(u/not-valid plugin-id :showInViewMode value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :showInViewMode "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :showInViewMode "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :hide-in-viewer (not value))))))}
@ -1462,10 +1463,10 @@
value (parser/parse-frame-guides value)]
(cond
(not (sm/validate [:vector ::ctg/grid] value))
(u/display-not-valid :guides value)
(u/not-valid plugin-id :guides value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :guides "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :guides "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :grids value))))))}
@ -1487,10 +1488,10 @@
value (keyword value)]
(cond
(not (contains? #{:fix :auto} value))
(u/display-not-valid :horizontalSizing value)
(u/not-valid plugin-id :horizontalSizing value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :horizontalSizing "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :horizontalSizing "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-item-h-sizing value})))))}
@ -1503,10 +1504,10 @@
value (keyword value)]
(cond
(not (contains? #{:fix :auto} value))
(u/display-not-valid :verticalSizing value)
(u/not-valid plugin-id :verticalSizing value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :verticalSizing "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :verticalSizing "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout #{id} {:layout-item-v-sizing value})))))}
@ -1530,10 +1531,10 @@
(let [segments (parser/parse-commands value)]
(cond
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :content "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :content "Plugin doesn't have 'content:write' permission")
(not (sm/validate path/schema:segments segments))
(u/display-not-valid :content segments)
(u/not-valid plugin-id :content segments)
:else
(let [selrect (path/calc-selrect segments)
@ -1556,13 +1557,13 @@
value)]
(cond
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :content "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :content "Plugin doesn't have 'content:write' permission")
(not (cfh/path-shape? data))
(u/display-not-valid :content-type type)
(u/not-valid plugin-id :content-type type)
(not (sm/validate path/schema:segments segments))
(u/display-not-valid :content segments)
(u/not-valid plugin-id :content segments)
:else
(let [selrect (path/calc-selrect segments)

View File

@ -119,10 +119,10 @@
variant (fonts/get-default-variant font)]
(cond
(not font)
(u/display-not-valid :fontId value)
(u/not-valid plugin-id :fontId value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :fontId "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :fontId "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-text-range id start end (font-data font variant))))))}
@ -141,10 +141,10 @@
variant (fonts/get-default-variant font)]
(cond
(not (string? value))
(u/display-not-valid :fontFamily value)
(u/not-valid plugin-id :fontFamily value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :fontFamily "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :fontFamily "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-text-range id start end (font-data font variant))))))}
@ -162,10 +162,10 @@
variant (fonts/get-variant font value)]
(cond
(not (string? value))
(u/display-not-valid :fontVariantId value)
(u/not-valid plugin-id :fontVariantId value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :fontVariantId "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :fontVariantId "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-text-range id start end (variant-data variant))))))}
@ -182,10 +182,10 @@
(let [value (str/trim (dm/str value))]
(cond
(or (empty? value) (not (re-matches font-size-re value)))
(u/display-not-valid :fontSize value)
(u/not-valid plugin-id :fontSize value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :fontSize "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :fontSize "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-text-range id start end {:font-size value})))))}
@ -209,10 +209,10 @@
(fonts/find-variant font {:weight weight}))]
(cond
(nil? variant)
(u/display-not-valid :fontWeight (dm/str "Font weight '" value "' not supported for the current font"))
(u/not-valid plugin-id :fontWeight (dm/str "Font weight '" value "' not supported for the current font"))
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :fontWeight "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :fontWeight "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-text-range id start end (variant-data variant))))))}
@ -235,10 +235,10 @@
(fonts/find-variant font {:style style}))]
(cond
(nil? variant)
(u/display-not-valid :fontStyle (dm/str "Font style '" value "' not supported for the current font"))
(u/not-valid plugin-id :fontStyle (dm/str "Font style '" value "' not supported for the current font"))
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :fontStyle "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :fontStyle "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-text-range id start end (variant-data variant))))))}
@ -255,10 +255,10 @@
(let [value (str/trim (dm/str value))]
(cond
(or (empty? value) (not (re-matches line-height-re value)))
(u/display-not-valid :lineHeight value)
(u/not-valid plugin-id :lineHeight value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :lineHeight "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :lineHeight "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-text-range id start end {:line-height value})))))}
@ -275,10 +275,10 @@
(let [value (str/trim (dm/str value))]
(cond
(or (empty? value) (re-matches letter-spacing-re value))
(u/display-not-valid :letterSpacing value)
(u/not-valid plugin-id :letterSpacing value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :letterSpacing "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :letterSpacing "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-text-range id start end {:letter-spacing value})))))}
@ -294,10 +294,10 @@
(fn [_ value]
(cond
(and (string? value) (not (re-matches text-transform-re value)))
(u/display-not-valid :textTransform value)
(u/not-valid plugin-id :textTransform value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :textTransform "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :textTransform "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-text-range id start end {:text-transform value}))))}
@ -313,10 +313,10 @@
(fn [_ value]
(cond
(and (string? value) (re-matches text-decoration-re value))
(u/display-not-valid :textDecoration value)
(u/not-valid plugin-id :textDecoration value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :textDecoration "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :textDecoration "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-text-range id start end {:text-decoration value}))))}
@ -332,10 +332,10 @@
(fn [_ value]
(cond
(and (string? value) (re-matches text-direction-re value))
(u/display-not-valid :direction value)
(u/not-valid plugin-id :direction value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :direction "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :direction "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-text-range id start end {:direction value}))))}
@ -351,10 +351,10 @@
(fn [_ value]
(cond
(and (string? value) (re-matches text-align-re value))
(u/display-not-valid :align value)
(u/not-valid plugin-id :align value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :align "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :align "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-text-range id start end {:text-align value}))))}
@ -371,10 +371,10 @@
(let [value (parser/parse-fills value)]
(cond
(not (sm/validate [:vector ::cts/fill] value))
(u/display-not-valid :fills value)
(u/not-valid plugin-id :fills value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :fills "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :fills "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-text-range id start end {:fills value})))))}
@ -401,10 +401,10 @@
;; editor as well
(cond
(or (not (string? value)) (empty? value))
(u/display-not-valid :characters value)
(u/not-valid plugin-id :characters value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :characters "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :characters "Plugin doesn't have 'content:write' permission")
(contains? (:workspace-editor-state @st/state) id)
(let [shape (u/proxy->shape self)
@ -428,10 +428,10 @@
value (keyword value)]
(cond
(not (contains? #{:auto-width :auto-height :fixed} value))
(u/display-not-valid :growType value)
(u/not-valid plugin-id :growType value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :growType "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :growType "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsh/update-shapes [id] #(assoc % :grow-type value))))))}
@ -445,10 +445,10 @@
variant (fonts/get-default-variant font)]
(cond
(not font)
(u/display-not-valid :fontId value)
(u/not-valid plugin-id :fontId value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :fontId "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :fontId "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-attrs id (font-data font variant))))))}
@ -462,10 +462,10 @@
variant (fonts/get-default-variant font)]
(cond
(not font)
(u/display-not-valid :fontFamily value)
(u/not-valid plugin-id :fontFamily value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :fontFamily "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :fontFamily "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-attrs id (font-data font variant))))))}
@ -479,10 +479,10 @@
variant (fonts/get-variant font value)]
(cond
(not variant)
(u/display-not-valid :fontVariantId value)
(u/not-valid plugin-id :fontVariantId value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :fontVariantId "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :fontVariantId "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-attrs id (variant-data variant))))))}
@ -495,10 +495,10 @@
value (str/trim (dm/str value))]
(cond
(or (empty? value) (not (re-matches font-size-re value)))
(u/display-not-valid :fontSize value)
(u/not-valid plugin-id :fontSize value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :fontSize "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :fontSize "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-attrs id {:font-size value})))))}
@ -517,10 +517,10 @@
(fonts/find-variant font {:weight weight}))]
(cond
(nil? variant)
(u/display-not-valid :fontWeight (dm/str "Font weight '" value "' not supported for the current font"))
(u/not-valid plugin-id :fontWeight (dm/str "Font weight '" value "' not supported for the current font"))
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :fontWeight "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :fontWeight "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-attrs id (variant-data variant))))))}
@ -539,10 +539,10 @@
(fonts/find-variant font {:style style}))]
(cond
(nil? variant)
(u/display-not-valid :fontStyle (dm/str "Font style '" value "' not supported for the current font"))
(u/not-valid plugin-id :fontStyle (dm/str "Font style '" value "' not supported for the current font"))
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :fontStyle "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :fontStyle "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-attrs id (variant-data variant))))))}
@ -555,10 +555,10 @@
value (str/trim (dm/str value))]
(cond
(or (empty? value) (not (re-matches line-height-re value)))
(u/display-not-valid :lineHeight value)
(u/not-valid plugin-id :lineHeight value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :lineHeight "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :lineHeight "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-attrs id {:line-height value})))))}
@ -571,10 +571,10 @@
value (str/trim (dm/str value))]
(cond
(or (not (string? value)) (not (re-matches letter-spacing-re value)))
(u/display-not-valid :letterSpacing value)
(u/not-valid plugin-id :letterSpacing value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :letterSpacing "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :letterSpacing "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-attrs id {:letter-spacing value})))))}
@ -586,10 +586,10 @@
(let [id (obj/get self "$id")]
(cond
(or (not (string? value)) (not (re-matches text-transform-re value)))
(u/display-not-valid :textTransform value)
(u/not-valid plugin-id :textTransform value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :textTransform "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :textTransform "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-attrs id {:text-transform value})))))}
@ -601,10 +601,10 @@
(let [id (obj/get self "$id")]
(cond
(or (not (string? value)) (not (re-matches text-decoration-re value)))
(u/display-not-valid :textDecoration value)
(u/not-valid plugin-id :textDecoration value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :textDecoration "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :textDecoration "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-attrs id {:text-decoration value})))))}
@ -616,10 +616,10 @@
(let [id (obj/get self "$id")]
(cond
(or (not (string? value)) (not (re-matches text-direction-re value)))
(u/display-not-valid :textDirection value)
(u/not-valid plugin-id :textDirection value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :textDirection "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :textDirection "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-attrs id {:text-direction value})))))}
@ -631,10 +631,10 @@
(let [id (obj/get self "$id")]
(cond
(or (not (string? value)) (not (re-matches text-align-re value)))
(u/display-not-valid :align value)
(u/not-valid plugin-id :align value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :align "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :align "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-attrs id {:text-align value})))))}
@ -646,10 +646,10 @@
(let [id (obj/get self "$id")]
(cond
(or (not (string? value)) (not (re-matches vertical-align-re value)))
(u/display-not-valid :verticalAlign value)
(u/not-valid plugin-id :verticalAlign value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :verticalAlign "Plugin doesn't have 'content:write' permission")
(u/not-valid plugin-id :verticalAlign "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwt/update-attrs id {:vertical-align value})))))}

View File

@ -19,18 +19,58 @@
[app.main.store :as st]
[app.plugins.utils :as u]
[app.util.object :as obj]
[clojure.datafy :refer [datafy]]))
[clojure.datafy :refer [datafy]]
[clojure.set :refer [map-invert]]))
;; === Token
;; Give more semantic names to the shape attributes that tokens can be applied to
(def ^:private map:token-attr->token-attr-plugin
{:r1 :border-radius-top-left
:r2 :border-radius-top-right
:r3 :border-radius-bottom-right
:r4 :border-radius-bottom-left
:p1 :padding-top-left
:p2 :padding-top-right
:p3 :padding-bottom-right
:p4 :padding-bottom-left
:m1 :margin-top-left
:m2 :margin-top-right
:m3 :margin-bottom-right
:m4 :margin-bottom-left})
(def ^:private map:token-attr-plugin->token-attr
(map-invert map:token-attr->token-attr-plugin))
(defn token-attr->token-attr-plugin
[k]
(get map:token-attr->token-attr-plugin k k))
(defn token-attr-plugin->token-attr
[k]
(get map:token-attr-plugin->token-attr k k))
(defn applied-tokens-plugin->applied-tokens
[value]
(into {}
(map (fn [[k v]] [(token-attr->token-attr-plugin k) v]))
value))
(defn token-attr?
[attr]
(cto/token-attr? (token-attr-plugin->token-attr attr)))
(defn- apply-token-to-shapes
[file-id set-id id shape-ids attrs]
[plugin-id file-id set-id id shape-ids attrs]
(let [token (u/locate-token file-id set-id id)]
(if (some #(not (cto/token-attr? %)) attrs)
(u/display-not-valid :applyToSelected attrs)
(if (some #(not (token-attr? %)) attrs)
(u/not-valid plugin-id :applyToSelected attrs)
(st/emit!
(dwta/toggle-token {:token token
:attrs attrs
:attrs (into #{} (map token-attr-plugin->token-attr) attrs)
:shape-ids shape-ids
:expand-with-children false})))))
@ -52,7 +92,7 @@
(defn token-proxy
[plugin-id file-id set-id id]
(obj/reify {:name "TokenProxy"
:on-error u/handle-error}
:on-error (u/handle-error plugin-id)}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$file-id {:enumerable false :get (constantly file-id)}
:$set-id {:enumerable false :get (constantly set-id)}
@ -146,16 +186,16 @@
{:enumerable false
:schema [:tuple
[:vector [:fn shape-proxy?]]
[:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
[:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]]
:fn (fn [shapes attrs]
(apply-token-to-shapes file-id set-id id (map #(obj/get % "$id") shapes) attrs))}
(apply-token-to-shapes plugin-id file-id set-id id (map #(obj/get % "$id") shapes) attrs))}
:applyToSelected
{:enumerable false
:schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
:schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]]
:fn (fn [attrs]
(let [selected (get-in @st/state [:workspace-local :selected])]
(apply-token-to-shapes file-id set-id id selected attrs)))}))
(apply-token-to-shapes plugin-id file-id set-id id selected attrs)))}))
;; === Token Set
@ -165,7 +205,7 @@
(defn token-set-proxy
[plugin-id file-id id]
(obj/reify {:name "TokenSetProxy"
:on-error u/handle-error}
:on-error (u/handle-error plugin-id)}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$file-id {:enumerable false :get (constantly file-id)}
:$id {:enumerable false :get (constantly id)}
@ -270,7 +310,7 @@
(if resolved-value
(do (st/emit! (dwtl/create-token id token))
(token-proxy plugin-id file-id id (:id token)))
(do (u/display-not-valid :addToken (str errors))
(do (u/not-valid plugin-id :addToken (str errors))
nil))))}
:duplicate
@ -287,7 +327,7 @@
(defn token-theme-proxy
[plugin-id file-id id]
(obj/reify {:name "TokenThemeProxy"
:on-error u/handle-error}
:on-error (u/handle-error plugin-id)}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$file-id {:enumerable false :get (constantly file-id)}
:$id {:enumerable false :get (constantly id)}
@ -394,7 +434,7 @@
(defn tokens-catalog
[plugin-id file-id]
(obj/reify {:name "TokensCatalog"
:on-error u/handle-error}
:on-error (u/handle-error plugin-id)}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$id {:enumerable false :get (constantly file-id)}

View File

@ -9,7 +9,6 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.json :as json]
[app.common.schema :as sm]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
@ -222,6 +221,16 @@
(resolve value)))))]
[ret-v ret-p]))
(defn natural-child-ordering?
[plugin-id]
(boolean
(dm/get-in @st/state [:plugins :flags plugin-id :natural-child-ordering])))
(defn throw-validation-errors?
[plugin-id]
(boolean
(dm/get-in @st/state [:plugins :flags plugin-id :throw-validation-errors])))
(defn display-not-valid
[code value]
(if (some? value)
@ -229,23 +238,25 @@
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid. Code: " code)))
nil)
(defn throw-not-valid
[code value]
(if (some? value)
(throw (js/Error. (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code)))
(throw (js/Error. (dm/str "[PENPOT PLUGIN] Value not valid. Code: " code))))
nil)
(defn not-valid
[plugin-id code value]
(if (throw-validation-errors? plugin-id)
(throw-not-valid code value)
(display-not-valid code value)))
(defn reject-not-valid
[reject code value]
(let [msg (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code)]
(.error js/console msg)
(reject msg)))
(defn coerce
"Decodes a javascript object into clj and check against schema. If schema validation fails,
displays a not-valid message with the code and hint provided and returns nil."
[attrs schema code hint]
(let [decoder (sm/decoder schema sm/json-transformer)
explainer (sm/explainer schema)
attrs (-> attrs json/->clj decoder)]
(if-let [explain (explainer attrs)]
(display-not-valid code (str hint " " (sm/humanize-explain explain)))
attrs)))
(defn mixed-value
[values]
(let [s (set values)]
@ -254,12 +265,14 @@
(defn handle-error
"Function to be used in plugin proxies methods to handle errors and print a readable
message to the console."
[cause]
(display-not-valid (ex-message cause) nil)
(if-let [explain (-> cause ex-data ::sm/explain)]
(println (sm/humanize-explain explain))
(js/console.log (ex-data cause)))
(js/console.log (.-stack cause)))
[plugin-id]
(fn [cause]
(let [message
(if-let [explain (-> cause ex-data ::sm/explain)]
(sm/humanize-explain explain)
(ex-data cause))]
(js/console.log (.-stack cause))
(not-valid plugin-id :error message))))
(defn is-main-component-proxy?
[p]

View File

@ -38,10 +38,10 @@
new-y (obj/get value "y")]
(cond
(not (sm/valid-safe-number? new-x))
(u/display-not-valid :center-x new-x)
(u/not-valid plugin-id :center-x new-x)
(not (sm/valid-safe-number? new-y))
(u/display-not-valid :center-y new-y)
(u/not-valid plugin-id :center-y new-y)
:else
(let [vb (dm/get-in @st/state [:workspace-local :vbox])
@ -63,7 +63,7 @@
(fn [value]
(cond
(not (sm/valid-safe-number? value))
(u/display-not-valid :zoom value)
(u/not-valid plugin-id :zoom value)
:else
(let [z (dm/get-in @st/state [:workspace-local :zoom])]
@ -87,7 +87,7 @@
(fn [shapes]
(cond
(not (every? ps/shape-proxy? shapes))
(u/display-not-valid :zoomIntoView "Argument should be valid shapes")
(u/not-valid plugin-id :zoomIntoView "Argument should be valid shapes")
:else
(let [ids (->> shapes

View File

@ -55,10 +55,11 @@
(defn text-editor-wasm?
[]
(let [runtime-features (get @st/state :features-runtime)
enabled-features (get @st/state :features)]
(or (contains? runtime-features "text-editor-wasm/v1")
(contains? enabled-features "text-editor-wasm/v1"))))
(or (contains? cf/flags :feature-text-editor-wasm)
(let [runtime-features (get @st/state :features-runtime)
enabled-features (get @st/state :features)]
(or (contains? runtime-features "text-editor-wasm/v1")
(contains? enabled-features "text-editor-wasm/v1")))))
(def ^:const UUID-U8-SIZE 16)
(def ^:const UUID-U32-SIZE (/ UUID-U8-SIZE 4))
@ -951,14 +952,14 @@
(= result 1)))
(def render-finish
(letfn [(do-render [ts]
(letfn [(do-render []
;; Check if context is still initialized before executing
;; to prevent errors when navigating quickly
(when wasm/context-initialized?
(perf/begin-measure "render-finish")
(h/call wasm/internal-module "_set_view_end")
(render ts)
(perf/end-measure "render-finish")))]
(perf/end-measure "render-finish")
(render (js/performance.now))))]
(fns/debounce do-render DEBOUNCE_DELAY_MS)))
(def render-pan
@ -1401,6 +1402,8 @@
(dbg/enabled? :wasm-viewbox)
(bit-or 2r00000000000000000000000000000001)
(text-editor-wasm?)
(bit-or 2r00000000000000000000000000000100)
(contains? cf/flags :render-wasm-info)
(bit-or 2r00000000000000000000000000001000)))
(defn set-canvas-size

View File

@ -66,17 +66,48 @@
(let [res (h/call wasm/internal-module "_text_editor_poll_event")]
res)))
(defn text-editor-insert-text
(defn text-editor-encode-text-pre
[text]
(when wasm/context-initialized?
(when (and (not (empty? text))
wasm/context-initialized?)
(let [encoder (js/TextEncoder.)
buf (.encode encoder text)
heapu8 (mem/get-heap-u8)
size (mem/size buf)
offset (mem/alloc size)]
(mem/write-buffer offset heapu8 buf)
(h/call wasm/internal-module "_text_editor_insert_text")
(mem/free))))
(mem/write-buffer offset heapu8 buf))))
(defn text-editor-encode-text-post
[text]
(when (and (not (empty? text))
wasm/context-initialized?)
(mem/free)))
(defn text-editor-composition-start
[]
(when wasm/context-initialized?
(h/call wasm/internal-module "_text_editor_composition_start")))
(defn text-editor-composition-update
[text]
(when wasm/context-initialized?
(text-editor-encode-text-pre text)
(h/call wasm/internal-module "_text_editor_composition_update")
(text-editor-encode-text-post text)))
(defn text-editor-composition-end
[text]
(when wasm/context-initialized?
(text-editor-encode-text-pre text)
(h/call wasm/internal-module "_text_editor_composition_end")
(text-editor-encode-text-post text)))
(defn text-editor-insert-text
[text]
(when wasm/context-initialized?
(text-editor-encode-text-pre text)
(h/call wasm/internal-module "_text_editor_insert_text")
(text-editor-encode-text-post text)))
(defn text-editor-delete-backward
([]

View File

@ -79,6 +79,7 @@ body {
:border-end-end-radius
:box-shadow
:filter
:backdrop-filter
:opacity
:overflow
:blend-mode

View File

@ -38,6 +38,7 @@
:border-color :border-color
:box-shadow :shadows
:filter :blur
:backdrop-filter :blur
:gap :size-array
:row-gap :size-array
:column-gap :size-array

View File

@ -256,7 +256,14 @@
(defn- get-filter
[shape]
(when-not (cgc/svg-markup? shape)
(get-in shape [:blur :value])))
(when (= :layer-blur (get-in shape [:blur :type]))
(get-in shape [:blur :value]))))
(defn- get-backdrop-filter
[shape]
(when-not (cgc/svg-markup? shape)
(when (= :background-blur (get-in shape [:blur :type]))
(get-in shape [:blur :value]))))
(defn- get-display
[shape]
@ -546,6 +553,7 @@
:opacity (get-opacity shape)
:box-shadow (get-box-shadow shape)
:filter (get-filter shape)
:backdrop-filter (get-backdrop-filter shape)
:overflow (get-overflow shape)
;; Display

View File

@ -141,7 +141,7 @@
events [(dwta/apply-token {:shape-ids [(cthi/id :frame1)]
:attributes #{:r1 :r2 :r3 :r4}
:token (toht/get-token file "test-token-2")
:on-update-shape dwta/update-shape-radius-all})]
:on-update-shape dwta/update-shape-radius})]
step2 (fn [_]
(let [events2 [(dwl/sync-file (:id file) (:id file))]]
@ -249,11 +249,11 @@
events [(dwta/apply-token {:shape-ids [(cthi/id :c-frame1)]
:attributes #{:r1 :r2 :r3 :r4}
:token (toht/get-token file "test-token-2")
:on-update-shape dwta/update-shape-radius-all})
:on-update-shape dwta/update-shape-radius})
(dwta/apply-token {:shape-ids [(cthi/id :frame1)]
:attributes #{:r1 :r2 :r3 :r4}
:token (toht/get-token file "test-token-3")
:on-update-shape dwta/update-shape-radius-all})]
:on-update-shape dwta/update-shape-radius})]
step2 (fn [_]
(let [events2 [(dwl/sync-file (:id file) (:id file))]]
@ -293,7 +293,7 @@
(dwta/apply-token {:shape-ids [(cthi/id :frame1)]
:attributes #{:r1 :r2 :r3 :r4}
:token (toht/get-token file "test-token-3")
:on-update-shape dwta/update-shape-radius-all})]
:on-update-shape dwta/update-shape-radius})]
step2 (fn [_]
(let [events2 [(dwl/sync-file (:id file) (:id file))]]

View File

@ -64,7 +64,7 @@
events [(dwta/apply-token {:shape-ids [(:id rect-1)]
:attributes #{:r1 :r2 :r3 :r4}
:token (toht/get-token file "borderRadius.md")
:on-update-shape dwta/update-shape-radius-all})]]
:on-update-shape dwta/update-shape-radius})]]
(tohs/run-store-async
store done events
(fn [new-state]
@ -89,11 +89,11 @@
events [(dwta/apply-token {:shape-ids [(:id rect-1)]
:attributes #{:r1 :r2 :r3 :r4}
:token (toht/get-token file "borderRadius.sm")
:on-update-shape dwta/update-shape-radius-all})
:on-update-shape dwta/update-shape-radius})
(dwta/apply-token {:shape-ids [(:id rect-1)]
:attributes #{:r1 :r2 :r3 :r4}
:token (toht/get-token file "borderRadius.md")
:on-update-shape dwta/update-shape-radius-all})]]
:on-update-shape dwta/update-shape-radius})]]
(tohs/run-store-async
store done events
(fn [new-state]
@ -117,14 +117,14 @@
(dwta/apply-token {:attributes #{:r1 :r2 :r3 :r4}
:token (toht/get-token file "borderRadius.sm")
:shape-ids [(:id rect-1)]
:on-update-shape dwta/update-shape-radius-all})
:on-update-shape dwta/update-shape-radius})
;; Apply single `:r1` attribute to same shape
;; while removing other attributes from the border-radius set
;; but keep `:r4` for testing purposes
(dwta/apply-token {:attributes #{:r1 :r2 :r3}
:token (toht/get-token file "borderRadius.md")
:shape-ids [(:id rect-1)]
:on-update-shape dwta/update-shape-radius-all})]]
:on-update-shape dwta/update-shape-radius})]]
(tohs/run-store-async
store done events
(fn [new-state]
@ -153,7 +153,7 @@
(dwta/apply-token {:shape-ids [(:id rect-2)]
:attributes #{:r1 :r2 :r3 :r4}
:token (toht/get-token file "borderRadius.sm")
:on-update-shape dwta/update-shape-radius-all})]]
:on-update-shape dwta/update-shape-radius})]]
(tohs/run-store-async
store done events
(fn [new-state]
@ -762,7 +762,7 @@
rect-2 (cths/get-shape file :rect-2)
events [(dwta/toggle-token {:shape-ids [(:id rect-1) (:id rect-2)]
:token-type-props {:attributes #{:r1 :r2 :r3 :r4}
:on-update-shape dwta/update-shape-radius-all}
:on-update-shape dwta/update-shape-radius}
:token (toht/get-token file "borderRadius.md")})]]
(tohs/run-store-async
store done events

View File

@ -3833,9 +3833,12 @@ msgstr "Invitation sent successfully"
msgid "notifications.invitation-link-copied"
msgstr "Invitation link copied"
msgid "notifications.mcp.active-tab-switching.text"
msgid "notifications.mcp.active-in-another-tab"
msgstr "MCP is active in another tab. Switch here?"
msgid "notifications.mcp.active-in-this-tab"
msgstr "MCP is now active in this tab."
#: src/app/main/ui/settings/delete_account.cljs:24
msgid "notifications.profile-deletion-not-allowed"
msgstr "You can't delete your profile. Reassign your teams before proceed."
@ -6106,6 +6109,14 @@ msgstr "Remove blur"
msgid "workspace.options.blur-options.title"
msgstr "Blur"
#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
msgid "workspace.options.blur-options.layer-blur"
msgstr "Layer blur"
#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
msgid "workspace.options.blur-options.background-blur"
msgstr "Background blur"
#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:91
msgid "workspace.options.blur-options.title.group"
msgstr "Group blur"

View File

@ -3790,9 +3790,12 @@ msgstr "Invitación enviada con éxito"
msgid "notifications.invitation-link-copied"
msgstr "Enlace de invitacion copiado"
msgid "notifications.mcp.active-tab-switching.text"
msgid "notifications.mcp.active-in-another-tab"
msgstr "MCP está activo en otra pestaña. ¿Cambiar a esta?"
msgid "notifications.mcp.active-in-this-tab"
msgstr "MCP está ahora activo en esta pestaña."
#: src/app/main/ui/settings/delete_account.cljs:24
msgid "notifications.profile-deletion-not-allowed"
msgstr "No puedes borrar tu perfil. Reasigna tus equipos antes de seguir."
@ -6059,6 +6062,14 @@ msgstr "Eliminar desenfoque"
msgid "workspace.options.blur-options.title"
msgstr "Desenfoque"
#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
msgid "workspace.options.blur-options.layer-blur"
msgstr "Desenfoque de capa"
#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs
msgid "workspace.options.blur-options.background-blur"
msgstr "Desenfoque de fondo"
#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:91
msgid "workspace.options.blur-options.title.group"
msgstr "Desenfoque del grupo"

View File

@ -55,6 +55,8 @@ function connectToMcpServer(baseUrl?: string, token?: string): void {
try {
let wsUrl = baseUrl || PENPOT_MCP_WEBSOCKET_URL;
let wsError: unknown | undefined;
if (token) {
wsUrl += `?userToken=${encodeURIComponent(token)}`;
}
@ -79,14 +81,18 @@ function connectToMcpServer(baseUrl?: string, token?: string): void {
};
ws.onclose = (event: CloseEvent) => {
console.log("Disconnected from MCP server");
const message = event.reason || undefined;
updateConnectionStatus("disconnected", "Disconnected", false, message);
// If we've send the error update we don't send the disconnect as well
if (!wsError) {
console.log("Disconnected from MCP server");
const message = event.reason || undefined;
updateConnectionStatus("disconnected", "Disconnected", false, message);
}
ws = null;
};
ws.onerror = (error) => {
console.error("WebSocket error:", error);
wsError = error;
// note: WebSocket error events typically don't contain detailed error messages
updateConnectionStatus("error", "Connection error", false);
};

File diff suppressed because it is too large Load Diff

View File

@ -282,7 +282,7 @@ Variants are a system for grouping related component versions along named proper
- check with `isVariantContainer()`
- property `variants: Variants`.
* `Variants`: Defines the combinations of property values for which component variants can exist and manages the concrete component variants.
- `properties: string[]` (ordered list of property names); `addProperty()`, `renameProperty(pos, name)`, `currentValues(property)`
- `properties: string[]` (ordered list of property names); `addProperty(): void`, `renameProperty(pos, name)`, `currentValues(property)`
- `variantComponents(): LibraryVariantComponent[]`
* `LibraryVariantComponent` (extends `LibraryComponent`): full library component with metadata, for which `isVariant()` returns true.
- `variantProps: { [property: string]: string }` (this component's value for each property)
@ -292,11 +292,11 @@ Variants are a system for grouping related component versions along named proper
Properties are often addressed positionally: `pos` parameter in various methods = index in `Variants.properties`.
**Creating a variant group**:
- `component.transformInVariant(): null`: Converts a standard component into a variant group, creating a `VariantContainer` and a second duplicate variant.
Both start with a default property `Property 1` with values `Value 1` / `Value 2`; there is no name-based auto-parsing.
- `board.combineAsVariants(ids: string[]): null`: Combines the board (a main component instance) with other main components (referenced via IDs) into a new variant group.
All components end up inside a single new `VariantContainer` on the canvas.
- In both cases, look for the created `VariantContainer` on the page, and then edit properties using `variants.renameProperty(pos, name)`, `variants.addProperty()`, and `comp.setVariantProperty(pos, value)`.
- `penpot.createVariantFromComponents(mainInstances: Board[]): VariantContainer`: Combines several main component instances into a new variant group.
All components end up inside a single new container on the canvas.
NOTE: The returned instance `variantContainer` is not usable but has an usable id; use `penpot.findShapeById(variantContainer.id)` to get the actual instance you can work with.
The container's `Variants` instance is initialised with one property `Property 1`, with the property values set to the respective component's name.
- After creation, edit properties using `variants.renameProperty(pos, name)`, `variants.addProperty()`, and `comp.setVariantProperty(pos, value)`.
**Adding a variant to an existing group**:
Use `variantContainer.appendChild(mainInstance)` to move a component's main instance into the container, then set its position manually and assign property values via `setVariantProperty`.
@ -342,7 +342,7 @@ Applying tokens:
(if properties is undefined, use a default property based on the token type - not usually recommended).
`TokenProperty` is a union type; possible values are:
- "all": applies the token to all properties it can control
- TokenBorderRadiusProps: "r1", "r2", "r3", "r4"
- TokenBorderRadiusProps: "borderRadiusTopLeft", "borderRadiusTopRight", "borderRadiusBottomRight", "borderRadiusBottomLeft"
- TokenShadowProps: "shadow"
- TokenColorProps: "fill", "strokeColor"
- TokenDimensionProps: "x", "y", "strokeWidth"
@ -353,7 +353,7 @@ Applying tokens:
- TokenNumberProps: "rotation"
- TokenOpacityProps: "opacity"
- TokenSizingProps: "width", "height", "layoutItemMinW", "layoutItemMaxW", "layoutItemMinH", "layoutItemMaxH"
- TokenSpacingProps: "rowGap", "columnGap", "p1", "p2", "p3", "p4", "m1", "m2", "m3", "m4"
- TokenSpacingProps: "rowGap", "columnGap", "paddingLeft", "paddingTop", "paddingRight", "paddingBottom", "marginLeft", "marginTop", "marginRight", "marginBottom"
- TokenBorderWidthProps: "strokeWidth"
- TokenTextCaseProps: "textCase"
- TokenTextDecorationProps: "textDecoration"

View File

@ -1,6 +1,12 @@
## 1.5.0 (Unreleased)
- **plugin-types**: Added a flags subcontexts with the flag `naturalChildrenOrdering`
- **plugins-runtime**: Added `version` field that returns the current version
- **plugin-types**: Fix penpot.openPage() to navigate in same tab by default
- **plugin-types**: Added `createVariantFromComponents`
- **plugin-types**: Change return type of `combineAsVariants`
- **plugin-types**: Added `textBounds` property for text shapes
- **plugin-types**: Added flag `throwValidationErrors` to enable exceptions on validation
## 1.4.2 (2026-01-21)

View File

@ -264,7 +264,7 @@ export class AppComponent {
type: 'apply-token',
setId: this.currentSetId,
tokenId,
// properties: ['strokeColor'] // Uncomment to choose attribute to apply
// properties: ['borderRadiusTopRight'] // Uncomment to choose attribute to apply
}); // (incompatible attributes will have no effect)
}
}

View File

@ -768,6 +768,11 @@ export interface CommonLayout {
* Represents the context of Penpot, providing access to various Penpot functionalities and data.
*/
export interface Context {
/**
* Returns the current penpot version.
*/
readonly version: string;
/**
* The root shape in the current Penpot context. Requires `content:read` permission.
*
@ -1710,6 +1715,13 @@ export interface Flags {
* Defaults to false
*/
naturalChildOrdering: boolean;
/**
* If `true` the validation errors will throw an exception instead of displaying an
* error in the debugger console.
* Defaults to false
*/
throwValidationErrors: boolean;
}
/**
@ -5231,7 +5243,11 @@ export interface TokenTheme {
/**
* The properties that a BorderRadius token can be applied to.
*/
type TokenBorderRadiusProps = 'r1' | 'r2' | 'r3' | 'r4';
type TokenBorderRadiusProps =
| 'borderRadiusTopLeft'
| 'borderRadiusTopRight'
| 'borderRadiusBottomRight'
| 'borderRadiusBottomLeft';
/**
* The properties that a Shadow token can be applied to.
@ -5307,16 +5323,16 @@ type TokenSpacingProps =
| 'columnGap'
// Spacing / Padding
| 'p1'
| 'p2'
| 'p3'
| 'p4'
| 'paddingLeft'
| 'paddingTop'
| 'paddingRight'
| 'paddingBottom'
// Spacing / Margin
| 'm1'
| 'm2'
| 'm3'
| 'm4';
| 'marginLeft'
| 'marginTop'
| 'marginRight'
| 'marginBottom';
/**
* The properties that a BorderWidth token can be applied to.

View File

@ -165,6 +165,10 @@ export function createApi(
// Penpot State API
get version(): string {
return plugin.context.version;
},
get root(): Shape | null {
checkPermission('content:read');
return plugin.context.root;

View File

@ -61,11 +61,13 @@ pub fn wasm_error(_attr: TokenStream, item: TokenStream) -> TokenStream {
let _: &dyn std::error::Error = &__e;
let __msg = __e.to_string();
crate::mem::set_error_code(__e.into());
crate::mem::free_bytes().expect("Failed to free bytes");
panic!("WASM error: {}",__msg);
}
},
Err(__payload) => {
crate::mem::set_error_code(0x02); // critical, same as Error::Critical
crate::mem::free_bytes().expect("Failed to free bytes");
std::panic::resume_unwind(__payload);
}
}

View File

@ -3,7 +3,6 @@ mod emscripten;
mod error;
mod math;
mod mem;
mod options;
mod performance;
mod render;
mod shapes;
@ -30,6 +29,9 @@ use uuid::Uuid;
pub(crate) static mut STATE: Option<Box<State>> = None;
// FIXME: These with_state* macros should be using our CriticalError instead of expect.
// But to do that, we need to not use them at domain-level (i.e. in business logic), just
// in the context of the wasm call.
#[macro_export]
macro_rules! with_state_mut {
($state:ident, $block:block) => {{
@ -102,7 +104,7 @@ macro_rules! with_state_mut_current_shape {
#[no_mangle]
#[wasm_error]
pub extern "C" fn init(width: i32, height: i32) -> Result<()> {
let state_box = Box::new(State::new(width, height));
let state_box = Box::new(State::try_new(width, height)?);
unsafe {
STATE = Some(state_box);
}
@ -138,7 +140,7 @@ pub extern "C" fn set_render_options(debug: u32, dpr: f32) -> Result<()> {
with_state_mut!(state, {
let render_state = state.render_state_mut();
render_state.set_debug_flags(debug);
render_state.set_dpr(dpr);
render_state.set_dpr(dpr)?;
});
Ok(())
}
@ -162,7 +164,7 @@ pub extern "C" fn render(_: i32) -> Result<()> {
state.rebuild_touched_tiles();
state
.start_render_loop(performance::get_time())
.expect("Error rendering");
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?;
});
Ok(())
}
@ -174,7 +176,7 @@ pub extern "C" fn render_sync() -> Result<()> {
state.rebuild_tiles();
state
.render_sync(performance::get_time())
.expect("Error rendering");
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?;
});
Ok(())
}
@ -236,24 +238,12 @@ pub extern "C" fn render_preview() -> Result<()> {
#[no_mangle]
#[wasm_error]
pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> {
let result = std::panic::catch_unwind(|| {
with_state_mut!(state, {
state
.process_animation_frame(timestamp)
.expect("Error processing animation frame");
});
});
let result = with_state_mut!(state, { state.process_animation_frame(timestamp) });
match result {
Ok(_) => {}
Err(err) => {
match err.downcast_ref::<String>() {
Some(message) => println!("process_animation_frame error: {}", message),
None => println!("process_animation_frame error: {:?}", err),
}
std::panic::resume_unwind(err);
}
if let Err(err) = result {
eprintln!("process_animation_frame error: {}", err);
}
Ok(())
}
@ -270,7 +260,7 @@ pub extern "C" fn reset_canvas() -> Result<()> {
#[wasm_error]
pub extern "C" fn resize_viewbox(width: i32, height: i32) -> Result<()> {
with_state_mut!(state, {
state.resize(width, height);
state.resize(width, height)?;
});
Ok(())
}
@ -305,43 +295,33 @@ pub extern "C" fn set_view_start() -> Result<()> {
Ok(())
}
/// Finishes a view interaction (zoom or pan). Rebuilds the tile index
/// and invalidates the tile texture cache so the subsequent render
/// re-draws all tiles at full quality (fast_mode is off at this point).
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_view_end() -> Result<()> {
with_state_mut!(state, {
let _end_start = performance::begin_timed_log!("set_view_end");
performance::begin_measure!("set_view_end");
state.render_state.options.set_fast_mode(false);
state.render_state.cancel_animation_frame();
// Update tile_viewbox first so that get_tiles_for_shape uses the correct interest area
// This is critical because we limit tiles to the interest area for optimization
let scale = state.render_state.get_scale();
state
.render_state
.tile_viewbox
.update(state.render_state.viewbox, scale);
// We rebuild the tile index on both pan and zoom because `get_tiles_for_shape`
// clips each shape to the current `TileViewbox::interest_rect` (viewport-dependent).
let _rebuild_start = performance::begin_timed_log!("rebuild_tiles");
performance::begin_measure!("set_view_end::rebuild_tiles");
if state.render_state.options.is_profile_rebuild_tiles() {
state.rebuild_tiles();
} else {
// Rebuild tile index + invalidate tile texture cache.
// Cache canvas is preserved so render_from_cache can still
// show a scaled preview during zoom.
state.rebuild_tiles_shallow();
}
performance::end_measure!("set_view_end::rebuild_tiles");
performance::end_timed_log!("rebuild_tiles", _rebuild_start);
state.render_state.sync_cached_viewbox();
performance::end_measure!("set_view_end");
performance::end_timed_log!("set_view_end", _end_start);
#[cfg(feature = "profile-macros")]
{
let total_time = performance::get_time() - unsafe { VIEW_INTERACTION_START };
performance::console_log!("[PERF] view_interaction: {}ms", total_time);
}
});
Ok(())
}
@ -362,8 +342,8 @@ pub extern "C" fn set_focus_mode() -> Result<()> {
let entries: Vec<Uuid> = bytes
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
.map(|data| Uuid::try_from(data).unwrap())
.collect();
.map(|data| Uuid::try_from(data).map_err(|e| Error::RecoverableError(e.to_string())))
.collect::<Result<Vec<Uuid>>>()?;
with_state_mut!(state, {
state.set_focus_mode(entries);
@ -637,8 +617,8 @@ pub extern "C" fn set_children() -> Result<()> {
let entries: Vec<Uuid> = bytes
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
.map(|data| Uuid::try_from(data).unwrap())
.collect();
.map(|data| Uuid::try_from(data).map_err(|e| Error::CriticalError(e.to_string())))
.collect::<Result<Vec<Uuid>>>()?;
set_children_set(entries)?;
@ -761,10 +741,15 @@ pub extern "C" fn get_selection_rect() -> Result<*mut u8> {
pub extern "C" fn set_structure_modifiers() -> Result<()> {
let bytes = mem::bytes();
let entries: Vec<_> = bytes
let entries: Vec<StructureEntry> = bytes
.chunks(44)
.map(|data| StructureEntry::from_bytes(data.try_into().unwrap()))
.collect();
.map(|chunk| {
let data = chunk
.try_into()
.map_err(|_| Error::CriticalError("Invalid StructureEntry bytes".to_string()))?;
Ok(StructureEntry::from_bytes(data))
})
.collect::<Result<Vec<_>>>()?;
with_state_mut!(state, {
let mut structure = HashMap::new();
@ -783,7 +768,9 @@ pub extern "C" fn set_structure_modifiers() -> Result<()> {
structure.entry(entry.parent).or_insert_with(Vec::new);
structure
.get_mut(&entry.parent)
.expect("Parent not found for entry")
.ok_or(Error::CriticalError(
"Parent not found for entry".to_string(),
))?
.push(entry);
}
}
@ -814,10 +801,10 @@ pub extern "C" fn clean_modifiers() -> Result<()> {
pub extern "C" fn set_modifiers() -> Result<()> {
let bytes = mem::bytes();
let entries: Vec<_> = bytes
let entries: Vec<TransformEntry> = bytes
.chunks(size_of::<<TransformEntry as SerializableResult>::BytesType>())
.map(|data| TransformEntry::try_from(data).unwrap())
.collect();
.map(|data| TransformEntry::try_from(data).map_err(|e| Error::CriticalError(e.to_string())))
.collect::<Result<Vec<_>>>()?;
let mut modifiers = HashMap::new();
let mut ids = Vec::<Uuid>::new();
@ -828,7 +815,7 @@ pub extern "C" fn set_modifiers() -> Result<()> {
with_state_mut!(state, {
state.set_modifiers(modifiers);
state.rebuild_modifier_tiles(ids);
state.rebuild_modifier_tiles(ids)?;
});
Ok(())
}
@ -838,8 +825,10 @@ pub extern "C" fn set_modifiers() -> Result<()> {
pub extern "C" fn start_temp_objects() -> Result<()> {
unsafe {
#[allow(static_mut_refs)]
let mut state = STATE.take().expect("Got an invalid state pointer");
state = Box::new(state.start_temp_objects());
let mut state = STATE.take().ok_or(Error::CriticalError(
"Got an invalid state pointer".to_string(),
))?;
state = Box::new(state.start_temp_objects()?);
STATE = Some(state);
}
Ok(())
@ -850,8 +839,10 @@ pub extern "C" fn start_temp_objects() -> Result<()> {
pub extern "C" fn end_temp_objects() -> Result<()> {
unsafe {
#[allow(static_mut_refs)]
let mut state = STATE.take().expect("Got an invalid state pointer");
state = Box::new(state.end_temp_objects());
let mut state = STATE.take().ok_or(Error::CriticalError(
"Got an invalid state pointer".to_string(),
))?;
state = Box::new(state.end_temp_objects()?);
STATE = Some(state);
}
Ok(())

View File

@ -1,4 +0,0 @@
pub const DEBUG_VISIBLE: u32 = 0x01;
pub const PROFILE_REBUILD_TILES: u32 = 0x02;
pub const FAST_MODE: u32 = 0x04;
pub const INFO_TEXT: u32 = 0x08;

View File

@ -21,6 +21,7 @@ use gpu_state::GpuState;
use options::RenderOptions;
pub use surfaces::{SurfaceId, Surfaces};
use crate::error::{Error, Result};
use crate::performance;
use crate::shapes::{
all_with_ancestors, radius_to_sigma, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor,
@ -281,6 +282,9 @@ pub(crate) struct RenderState {
pub current_tile: Option<tiles::Tile>,
pub sampling_options: skia::SamplingOptions,
pub render_area: Rect,
// render_area expanded by surface margins — used for visibility checks so that
// shapes in the margin zone are rendered (needed for background blur sampling).
pub render_area_with_margins: Rect,
pub tile_viewbox: tiles::TileViewbox,
pub tiles: tiles::TileHashMap,
pub pending_tiles: PendingTiles,
@ -323,19 +327,19 @@ pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
}
impl RenderState {
pub fn new(width: i32, height: i32) -> RenderState {
pub fn try_new(width: i32, height: i32) -> Result<RenderState> {
// This needs to be done once per WebGL context.
let mut gpu_state = GpuState::new();
let mut gpu_state = GpuState::try_new()?;
let sampling_options =
skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest);
let fonts = FontStore::new();
let surfaces = Surfaces::new(
let fonts = FontStore::try_new()?;
let surfaces = Surfaces::try_new(
&mut gpu_state,
(width, height),
sampling_options,
tiles::get_tile_dimensions(),
);
)?;
// This is used multiple times everywhere so instead of creating new instances every
// time we reuse this one.
@ -343,7 +347,7 @@ impl RenderState {
let viewbox = Viewbox::new(width as f32, height as f32);
let tiles = tiles::TileHashMap::new();
RenderState {
Ok(RenderState {
gpu_state: gpu_state.clone(),
options: RenderOptions::default(),
surfaces,
@ -358,6 +362,7 @@ impl RenderState {
current_tile: None,
sampling_options,
render_area: Rect::new_empty(),
render_area_with_margins: Rect::new_empty(),
tiles,
tile_viewbox: tiles::TileViewbox::new_with_interest(
viewbox,
@ -373,7 +378,7 @@ impl RenderState {
touched_ids: HashSet::default(),
ignore_nested_blurs: false,
preview_mode: false,
}
})
}
/// Combines every visible layer blur currently active (ancestors + shape)
@ -426,18 +431,116 @@ impl RenderState {
shape.frame_clip_layer_blur()
}
/// Renders background blur effect directly to the Current surface.
/// Must be called BEFORE any save_layer for the shape's own opacity/blend,
/// so that the backdrop blur is independent of the shape's visual properties.
fn render_background_blur(&mut self, shape: &Shape) {
if self.options.is_fast_mode() {
return;
}
if matches!(shape.shape_type, Type::Text(_)) || matches!(shape.shape_type, Type::SVGRaw(_))
{
return;
}
let blur = match shape
.blur
.filter(|b| !b.hidden && b.blur_type == BlurType::BackgroundBlur)
{
Some(blur) => blur,
None => return,
};
let scale = self.get_scale();
let scaled_sigma = radius_to_sigma(blur.value * scale);
// Cap sigma so the blur kernel (≈3σ) stays within the tile margin.
// This prevents visible seams at tile boundaries when zoomed in.
let margin = self.surfaces.margins().width as f32;
let max_sigma = margin / 3.0;
let capped_sigma = scaled_sigma.min(max_sigma);
let blur_filter = match skia::image_filters::blur(
(capped_sigma, capped_sigma),
skia::TileMode::Clamp,
None,
None,
) {
Some(filter) => filter,
None => return,
};
let snapshot = self.surfaces.snapshot(SurfaceId::Current);
let translation = self
.surfaces
.get_render_context_translation(self.render_area, scale);
let center = shape.center();
let mut matrix = shape.transform;
matrix.post_translate(center);
matrix.pre_translate(-center);
let canvas = self.surfaces.canvas(SurfaceId::Current);
canvas.save();
// Current has no render context transform (identity canvas).
// Apply scale + translate + shape transform so the clip maps
// from shape-local coords to device pixels correctly.
canvas.scale((scale, scale));
canvas.translate(translation);
canvas.concat(&matrix);
// Clip to shape's path based on shape type
match &shape.shape_type {
Type::Rect(data) if data.corners.is_some() => {
let rrect = RRect::new_rect_radii(shape.selrect, data.corners.as_ref().unwrap());
canvas.clip_rrect(rrect, skia::ClipOp::Intersect, true);
}
Type::Frame(data) if data.corners.is_some() => {
let rrect = RRect::new_rect_radii(shape.selrect, data.corners.as_ref().unwrap());
canvas.clip_rrect(rrect, skia::ClipOp::Intersect, true);
}
Type::Rect(_) | Type::Frame(_) => {
canvas.clip_rect(shape.selrect, skia::ClipOp::Intersect, true);
}
Type::Circle => {
let mut pb = skia::PathBuilder::new();
pb.add_oval(shape.selrect, None, None);
canvas.clip_path(&pb.detach(), skia::ClipOp::Intersect, true);
}
_ => {
if let Some(path) = shape.get_skia_path() {
canvas.clip_path(&path, skia::ClipOp::Intersect, true);
} else {
canvas.clip_rect(shape.selrect, skia::ClipOp::Intersect, true);
}
}
}
// Reset matrix so snapshot draws pixel-for-pixel on the surface.
// Clips survive reset_matrix (stored in device coords).
canvas.reset_matrix();
// Use Src blend to replace content within the clip with the
// blurred version (not SrcOver which would double-render).
let mut paint = skia::Paint::default();
paint.set_image_filter(blur_filter);
paint.set_blend_mode(skia::BlendMode::Src);
canvas.draw_image(&snapshot, (0, 0), Some(&paint));
canvas.restore();
}
/// Runs `f` with `ignore_nested_blurs` temporarily forced to `true`.
/// Certain off-screen passes (e.g. shadow masks) must render shapes without
/// inheriting ancestor blur. This helper guarantees the flag is restored.
fn with_nested_blurs_suppressed<F, R>(&mut self, f: F) -> R
fn with_nested_blurs_suppressed<F, R>(&mut self, f: F) -> Result<R>
where
F: FnOnce(&mut RenderState) -> R,
F: FnOnce(&mut RenderState) -> Result<R>,
{
let previous = self.ignore_nested_blurs;
self.ignore_nested_blurs = true;
let result = f(self);
let result = f(self)?;
self.ignore_nested_blurs = previous;
result
Ok(result)
}
pub fn fonts(&self) -> &FontStore {
@ -448,12 +551,7 @@ impl RenderState {
&mut self.fonts
}
pub fn add_image(
&mut self,
id: Uuid,
is_thumbnail: bool,
image_data: &[u8],
) -> Result<(), String> {
pub fn add_image(&mut self, id: Uuid, is_thumbnail: bool, image_data: &[u8]) -> Result<()> {
self.images.add(id, is_thumbnail, image_data)
}
@ -465,7 +563,7 @@ impl RenderState {
texture_id: u32,
width: i32,
height: i32,
) -> Result<(), String> {
) -> Result<()> {
self.images
.add_image_from_gl_texture(id, is_thumbnail, texture_id, width, height)
}
@ -478,15 +576,16 @@ impl RenderState {
self.options.flags = debug;
}
pub fn set_dpr(&mut self, dpr: f32) {
pub fn set_dpr(&mut self, dpr: f32) -> Result<()> {
if Some(dpr) != self.options.dpr {
self.options.dpr = Some(dpr);
self.resize(
self.viewbox.width.floor() as i32,
self.viewbox.height.floor() as i32,
);
)?;
self.fonts.set_scale_debug_font(dpr);
}
Ok(())
}
pub fn set_background_color(&mut self, color: skia::Color) {
@ -497,13 +596,15 @@ impl RenderState {
self.preview_mode = enabled;
}
pub fn resize(&mut self, width: i32, height: i32) {
pub fn resize(&mut self, width: i32, height: i32) -> Result<()> {
let dpr_width = (width as f32 * self.options.dpr()).floor() as i32;
let dpr_height = (height as f32 * self.options.dpr()).floor() as i32;
self.surfaces
.resize(&mut self.gpu_state, dpr_width, dpr_height);
.resize(&mut self.gpu_state, dpr_width, dpr_height)?;
self.viewbox.set_wh(width as f32, height as f32);
self.tile_viewbox.update(self.viewbox, self.get_scale());
Ok(())
}
pub fn flush_and_submit(&mut self) {
@ -525,19 +626,23 @@ impl RenderState {
self.surfaces.canvas(surface_id).restore();
}
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) {
let tile_rect = self.get_current_aligned_tile_bounds();
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) -> Result<()> {
let tile_rect = self.get_current_aligned_tile_bounds()?;
self.surfaces.cache_current_tile_texture(
&self.tile_viewbox,
&self.current_tile.unwrap(),
&self
.current_tile
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
&tile_rect,
);
self.surfaces.draw_cached_tile_surface(
self.current_tile.unwrap(),
self.current_tile
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
rect,
self.background_color,
);
Ok(())
}
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>) {
@ -646,7 +751,7 @@ impl RenderState {
offset: Option<(f32, f32)>,
parent_shadows: Option<Vec<skia_safe::Paint>>,
outset: Option<f32>,
) {
) -> Result<()> {
let surface_ids = fills_surface_id as u32
| strokes_surface_id as u32
| innershadows_surface_id as u32
@ -711,7 +816,7 @@ impl RenderState {
antialias,
SurfaceId::Current,
None,
);
)?;
// Pass strokes in natural order; stroke merging handles top-most ordering internally.
let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect();
@ -722,7 +827,7 @@ impl RenderState {
Some(SurfaceId::Current),
antialias,
outset,
);
)?;
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
s.canvas().restore();
@ -738,7 +843,7 @@ impl RenderState {
s.canvas().restore();
});
}
return;
return Ok(());
}
// set clipping
@ -788,6 +893,22 @@ impl RenderState {
// We don't want to change the value in the global state
let mut shape: Cow<Shape> = Cow::Borrowed(shape);
// Remove background blur from the shape so it doesn't get processed
// as a layer blur. The actual rendering is done before the save_layer
// in render_background_blur() so it's independent of shape opacity.
if !fast_mode
&& apply_to_current_surface
&& fills_surface_id == SurfaceId::Fills
&& !matches!(shape.shape_type, Type::Text(_))
&& !matches!(shape.shape_type, Type::SVGRaw(_))
&& shape
.blur
.is_some_and(|b| !b.hidden && b.blur_type == BlurType::BackgroundBlur)
{
shape.to_mut().set_blur(None);
}
let frame_has_blur = Self::frame_clip_layer_blur(&shape).is_some();
let shape_has_blur = shape.blur.is_some();
@ -899,7 +1020,7 @@ impl RenderState {
None,
text_fill_inset,
None,
);
)?;
for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list
.iter_mut()
@ -916,7 +1037,7 @@ impl RenderState {
text_stroke_blur_outset,
None,
*layer_opacity,
);
)?;
}
} else {
let mut drop_shadows = shape.drop_shadow_paints();
@ -959,7 +1080,7 @@ impl RenderState {
blur_filter.as_ref(),
None,
None,
);
)?;
}
} else {
shadows::render_text_shadows(
@ -970,7 +1091,7 @@ impl RenderState {
text_drop_shadows_surface_id.into(),
&parent_shadows,
&blur_filter,
);
)?;
}
} else {
// 1. Text drop shadows
@ -986,7 +1107,7 @@ impl RenderState {
blur_filter.as_ref(),
None,
None,
);
)?;
}
}
@ -1001,7 +1122,7 @@ impl RenderState {
blur_filter.as_ref(),
text_fill_inset,
None,
);
)?;
// 3. Stroke drop shadows
shadows::render_text_shadows(
@ -1012,7 +1133,7 @@ impl RenderState {
text_drop_shadows_surface_id.into(),
&drop_shadows,
&blur_filter,
);
)?;
// 4. Stroke fills
for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list
@ -1030,7 +1151,7 @@ impl RenderState {
text_stroke_blur_outset,
None,
*layer_opacity,
);
)?;
}
// 5. Stroke inner shadows
@ -1042,7 +1163,7 @@ impl RenderState {
Some(innershadows_surface_id),
&inner_shadows,
&blur_filter,
);
)?;
// 6. Fill Inner shadows
if !shape.has_visible_strokes() {
@ -1057,7 +1178,7 @@ impl RenderState {
blur_filter.as_ref(),
None,
None,
);
)?;
}
}
}
@ -1105,7 +1226,7 @@ impl RenderState {
antialias,
fills_surface_id,
outset,
);
)?;
}
} else {
fills::render(
@ -1115,7 +1236,7 @@ impl RenderState {
antialias,
fills_surface_id,
outset,
);
)?;
}
// Skip stroke rendering for clipped frames - they are drawn in render_shape_exit
@ -1131,7 +1252,7 @@ impl RenderState {
Some(strokes_surface_id),
antialias,
outset,
);
)?;
if !fast_mode {
for stroke in &visible_strokes {
shadows::render_stroke_inner_shadows(
@ -1140,7 +1261,7 @@ impl RenderState {
stroke,
antialias,
innershadows_surface_id,
);
)?;
}
}
}
@ -1177,13 +1298,23 @@ impl RenderState {
s.canvas().restore();
});
}
Ok(())
}
pub fn update_render_context(&mut self, tile: tiles::Tile) {
self.current_tile = Some(tile);
self.render_area = tiles::get_tile_rect(tile, self.get_scale());
self.surfaces
.update_render_context(self.render_area, self.get_scale());
let scale = self.get_scale();
self.render_area = tiles::get_tile_rect(tile, scale);
let margins = self.surfaces.margins();
let margin_w = margins.width as f32 / scale;
let margin_h = margins.height as f32 / scale;
self.render_area_with_margins = skia::Rect::from_ltrb(
self.render_area.left - margin_w,
self.render_area.top - margin_h,
self.render_area.right + margin_w,
self.render_area.bottom + margin_h,
);
self.surfaces.update_render_context(self.render_area, scale);
}
pub fn cancel_animation_frame(&mut self) {
@ -1246,7 +1377,7 @@ impl RenderState {
/// Render a preview of the shapes during loading.
/// This rebuilds tiles for touched shapes and renders synchronously.
pub fn render_preview(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<(), String> {
pub fn render_preview(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<()> {
let _start = performance::begin_timed_log!("render_preview");
performance::begin_measure!("render_preview");
@ -1276,7 +1407,7 @@ impl RenderState {
tree: ShapesPoolRef,
timestamp: i32,
sync_render: bool,
) -> Result<(), String> {
) -> Result<()> {
let _start = performance::begin_timed_log!("start_render_loop");
let scale = self.get_scale();
@ -1303,7 +1434,7 @@ impl RenderState {
|| viewbox_cache_size.height > cached_viewbox_cache_size.height
{
self.surfaces
.resize_cache(viewbox_cache_size, VIEWPORT_INTEREST_AREA_THRESHOLD);
.resize_cache(viewbox_cache_size, VIEWPORT_INTEREST_AREA_THRESHOLD)?;
}
// FIXME - review debug
@ -1348,7 +1479,7 @@ impl RenderState {
base_object: Option<&Uuid>,
tree: ShapesPoolRef,
timestamp: i32,
) -> Result<(), String> {
) -> Result<()> {
performance::begin_measure!("process_animation_frame");
if self.render_in_progress {
if tree.len() != 0 {
@ -1372,7 +1503,7 @@ impl RenderState {
base_object: Option<&Uuid>,
tree: ShapesPoolRef,
timestamp: i32,
) -> Result<(), String> {
) -> Result<()> {
if tree.len() != 0 {
self.render_shape_tree_partial(base_object, tree, timestamp, false)?;
}
@ -1462,7 +1593,7 @@ impl RenderState {
element: &Shape,
visited_mask: bool,
clip_bounds: Option<ClipStack>,
) {
) -> Result<()> {
if visited_mask {
// Because masked groups needs two rendering passes (first drawing
// the content and then drawing the mask), we need to do an
@ -1533,7 +1664,7 @@ impl RenderState {
None,
None,
None,
);
)?;
}
// Only restore if we created a layer (optimization for simple shapes)
@ -1545,19 +1676,22 @@ impl RenderState {
}
self.focus_mode.exit(&element.id);
Ok(())
}
pub fn get_current_tile_bounds(&mut self) -> Rect {
let tiles::Tile(tile_x, tile_y) = self.current_tile.unwrap();
pub fn get_current_tile_bounds(&mut self) -> Result<Rect> {
let tiles::Tile(tile_x, tile_y) = self
.current_tile
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
let scale = self.get_scale();
let offset_x = self.viewbox.area.left * scale;
let offset_y = self.viewbox.area.top * scale;
Rect::from_xywh(
Ok(Rect::from_xywh(
(tile_x as f32 * tiles::TILE_SIZE) - offset_x,
(tile_y as f32 * tiles::TILE_SIZE) - offset_y,
tiles::TILE_SIZE,
tiles::TILE_SIZE,
)
))
}
pub fn get_rect_bounds(&mut self, rect: skia::Rect) -> Rect {
@ -1605,8 +1739,11 @@ impl RenderState {
// lower multiple of `TILE_SIZE`. This ensures the tile bounds are aligned
// with the global tile grid, which is useful for rendering tiles in a
/// consistent and predictable layout.
pub fn get_current_aligned_tile_bounds(&mut self) -> Rect {
self.get_aligned_tile_bounds(self.current_tile.unwrap())
pub fn get_current_aligned_tile_bounds(&mut self) -> Result<Rect> {
Ok(self.get_aligned_tile_bounds(
self.current_tile
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
))
}
/// Renders a drop shadow effect for the given shape.
@ -1623,7 +1760,7 @@ impl RenderState {
scale: f32,
translation: (f32, f32),
extra_layer_blur: Option<Blur>,
) {
) -> Result<()> {
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
transformed_shadow.to_mut().offset = (0.0, 0.0);
transformed_shadow.to_mut().color = skia::Color::BLACK;
@ -1678,15 +1815,15 @@ impl RenderState {
plain_shape_mut.clip_content = false;
let Some(drop_filter) = transformed_shadow.get_drop_shadow_filter() else {
return;
return Ok(());
};
let mut bounds = drop_filter.compute_fast_bounds(shape_bounds);
// Account for the shadow offset so the temporary surface fully contains the shifted blur.
bounds.offset(world_offset);
// Early cull if the shadow bounds are outside the render area.
if !bounds.intersects(self.render_area) {
return;
if !bounds.intersects(self.render_area_with_margins) {
return Ok(());
}
// blur=0 at high zoom: draw directly on DropShadows with geometric spread (no filter).
@ -1708,11 +1845,11 @@ impl RenderState {
Some(shadow.offset),
None,
Some(shadow.spread),
);
});
)
})?;
self.surfaces.canvas(SurfaceId::DropShadows).restore();
return;
return Ok(());
}
// Create filter with blur only (no offset, no spread - handled geometrically)
@ -1750,11 +1887,11 @@ impl RenderState {
Some(shadow.offset), // Offset is geometric
None,
Some(shadow.spread),
);
});
)
})?;
self.surfaces.canvas(SurfaceId::DropShadows).restore();
return;
return Ok(());
}
// Adaptive downscale for large blur values (lossless GPU optimization).
@ -1791,12 +1928,13 @@ impl RenderState {
Some(shadow.offset), // Offset is geometric
None,
Some(shadow.spread),
);
});
)
})?;
state.surfaces.canvas(temp_surface).restore();
Ok(())
},
);
)?;
if let Some((mut surface, filter_scale)) = filter_result {
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
@ -1831,6 +1969,7 @@ impl RenderState {
}
drop_canvas.restore();
}
Ok(())
}
/// Renders element drop shadows to DropShadows surface and composites to Current.
@ -1845,7 +1984,7 @@ impl RenderState {
scale: f32,
translation: (f32, f32),
node_render_state: &NodeRenderState,
) {
) -> Result<()> {
let element_extrect = extrect.get_or_insert_with(|| element.extrect(tree, scale));
let inherited_layer_blur = match element.shape_type {
Type::Frame(_) | Type::Group(_) => element.blur,
@ -1867,7 +2006,7 @@ impl RenderState {
scale,
translation,
None,
);
)?;
if !matches!(element.shape_type, Type::Bool(_)) {
let shadow_children = if element.is_recursive() {
@ -1896,7 +2035,7 @@ impl RenderState {
scale,
translation,
inherited_layer_blur,
);
)?;
} else {
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
@ -1932,8 +2071,8 @@ impl RenderState {
None,
Some(vec![new_shadow_paint.clone()]),
None,
);
});
)
})?;
self.surfaces.canvas(SurfaceId::DropShadows).restore();
}
}
@ -1990,6 +2129,7 @@ impl RenderState {
self.surfaces
.canvas(SurfaceId::DropShadows)
.clear(skia::Color::TRANSPARENT);
Ok(())
}
pub fn render_shape_tree_partial_uncached(
@ -1997,7 +2137,7 @@ impl RenderState {
tree: ShapesPoolRef,
timestamp: i32,
allow_stop: bool,
) -> Result<(bool, bool), String> {
) -> Result<(bool, bool)> {
let mut iteration = 0;
let mut is_empty = true;
@ -2025,7 +2165,7 @@ impl RenderState {
if visited_children {
if !node_render_state.flattened {
self.render_shape_exit(element, visited_mask, clip_bounds);
self.render_shape_exit(element, visited_mask, clip_bounds)?;
}
continue;
}
@ -2051,11 +2191,11 @@ impl RenderState {
let is_visible = if is_container || has_effects {
let element_extrect =
extrect.get_or_insert_with(|| transformed_element.extrect(tree, scale));
element_extrect.intersects(self.render_area)
element_extrect.intersects(self.render_area_with_margins)
&& !transformed_element.visually_insignificant(scale, tree)
} else {
let selrect = transformed_element.selrect();
selrect.intersects(self.render_area)
selrect.intersects(self.render_area_with_margins)
&& !transformed_element.visually_insignificant(scale, tree)
};
@ -2099,7 +2239,19 @@ impl RenderState {
scale,
translation,
&node_render_state,
);
)?;
}
// Render background blur BEFORE save_layer so it modifies
// the backdrop independently of the shape's opacity.
if !node_render_state.is_root() && self.focus_mode.is_active() {
self.render_background_blur(element);
}
// Render background blur BEFORE save_layer so it modifies
// the backdrop independently of the shape's opacity.
if !node_render_state.is_root() && self.focus_mode.is_active() {
self.render_background_blur(element);
}
self.render_shape_enter(element, mask);
@ -2129,7 +2281,7 @@ impl RenderState {
scale,
translation,
&node_render_state,
);
)?;
}
self.render_shape(
@ -2143,7 +2295,7 @@ impl RenderState {
None,
None,
None,
);
)?;
self.surfaces
.canvas(SurfaceId::DropShadows)
@ -2240,14 +2392,14 @@ impl RenderState {
tree: ShapesPoolRef,
timestamp: i32,
allow_stop: bool,
) -> Result<(), String> {
) -> Result<()> {
let mut should_stop = false;
let root_ids = {
if let Some(shape_id) = base_object {
vec![*shape_id]
} else {
let Some(root) = tree.get(&Uuid::nil()) else {
return Err(String::from("Root shape not found"));
return Err(Error::CriticalError("Root shape not found".to_string()));
};
root.children_ids(false)
}
@ -2257,7 +2409,7 @@ impl RenderState {
if let Some(current_tile) = self.current_tile {
if self.surfaces.has_cached_tile_surface(current_tile) {
performance::begin_measure!("render_shape_tree::cached");
let tile_rect = self.get_current_tile_bounds();
let tile_rect = self.get_current_tile_bounds()?;
self.surfaces.draw_cached_tile_surface(
current_tile,
tile_rect,
@ -2287,9 +2439,9 @@ impl RenderState {
return Ok(());
}
performance::end_measure!("render_shape_tree::uncached");
let tile_rect = self.get_current_tile_bounds();
let tile_rect = self.get_current_tile_bounds()?;
if !is_empty {
self.apply_render_to_final_canvas(tile_rect);
self.apply_render_to_final_canvas(tile_rect)?;
if self.options.is_debug_visible() {
debug::render_workspace_current_tile(
@ -2320,10 +2472,22 @@ impl RenderState {
if !self.surfaces.has_cached_tile_surface(next_tile) {
if let Some(ids) = self.tiles.get_shapes_at(next_tile) {
// Check if any shape on this tile has a background blur.
// If so, we need ALL root shapes rendered (not just those
// assigned to this tile) because the blur snapshots Current
// which must contain the shapes behind it.
let tile_has_bg_blur = ids.iter().any(|id| {
tree.get(id).is_some_and(|s| {
s.blur.is_some_and(|b| {
!b.hidden && b.blur_type == BlurType::BackgroundBlur
})
})
});
// We only need first level shapes, in the same order as the parent node
let mut valid_ids = Vec::with_capacity(ids.len());
for root_id in root_ids.iter() {
if ids.contains(root_id) {
if tile_has_bg_blur || ids.contains(root_id) {
valid_ids.push(*root_id);
}
}
@ -2511,24 +2675,20 @@ impl RenderState {
self.surfaces.remove_cached_tile_surface(tile);
}
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
performance::begin_measure!("rebuild_tiles_shallow");
// Check if zoom changed - if so, we need full cache invalidation
// because tiles are rendered at specific zoom levels
/// Rebuild the tile index (shape→tile mapping) for all top-level shapes.
/// This does NOT invalidate the tile texture cache — cached tile images
/// survive so that fast-mode renders during pan still show shadows/blur.
pub fn rebuild_tile_index(&mut self, tree: ShapesPoolRef) {
let zoom_changed = self.zoom_changed();
let mut tiles_to_invalidate = HashSet::<tiles::Tile>::new();
let mut nodes = vec![Uuid::nil()];
while let Some(shape_id) = nodes.pop() {
if let Some(shape) = tree.get(&shape_id) {
if shape_id != Uuid::nil() {
if zoom_changed {
// Zoom changed: use full update that tracks all affected tiles
tiles_to_invalidate.extend(self.update_shape_tiles(shape, tree));
let _ = self.update_shape_tiles(shape, tree);
} else {
// Pan only: use incremental update that preserves valid cached tiles
self.update_shape_tiles_incremental(shape, tree);
let _ = self.update_shape_tiles_incremental(shape, tree);
}
} else {
// We only need to rebuild tiles from the first level.
@ -2538,9 +2698,17 @@ impl RenderState {
}
}
}
}
// Invalidate changed tiles - old content stays visible until new tiles render
self.surfaces.remove_cached_tiles(self.background_color);
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
performance::begin_measure!("rebuild_tiles_shallow");
self.rebuild_tile_index(tree);
// Invalidate the tile texture cache so all tiles are re-rendered, but
// preserve the cache canvas so render_from_cache can still show a scaled
// preview of old content while new tiles load progressively.
self.surfaces.invalidate_tile_cache();
performance::end_measure!("rebuild_tiles_shallow");
}
@ -2615,7 +2783,11 @@ impl RenderState {
///
/// This is useful when you have a pre-computed set of shape IDs that need to be refreshed,
/// regardless of their relationship to other shapes (e.g., ancestors, descendants, or any other collection).
pub fn update_tiles_shapes(&mut self, shape_ids: &[Uuid], tree: ShapesPoolMutRef<'_>) {
pub fn update_tiles_shapes(
&mut self,
shape_ids: &[Uuid],
tree: ShapesPoolMutRef<'_>,
) -> Result<()> {
performance::begin_measure!("invalidate_and_update_tiles");
let mut all_tiles = HashSet::<tiles::Tile>::new();
for shape_id in shape_ids {
@ -2627,6 +2799,7 @@ impl RenderState {
self.remove_cached_tile(tile);
}
performance::end_measure!("invalidate_and_update_tiles");
Ok(())
}
/// Rebuilds tiles for shapes with modifiers and processes their ancestors
@ -2635,9 +2808,14 @@ impl RenderState {
/// Additionally, it processes all ancestors of modified shapes to ensure their
/// extended rectangles are properly recalculated and their tiles are updated.
/// This is crucial for frames and groups that contain transformed children.
pub fn rebuild_modifier_tiles(&mut self, tree: ShapesPoolMutRef<'_>, ids: Vec<Uuid>) {
pub fn rebuild_modifier_tiles(
&mut self,
tree: ShapesPoolMutRef<'_>,
ids: Vec<Uuid>,
) -> Result<()> {
let ancestors = all_with_ancestors(&ids, tree, false);
self.update_tiles_shapes(&ancestors, tree);
self.update_tiles_shapes(&ancestors, tree)?;
Ok(())
}
pub fn get_scale(&self) -> f32 {
@ -2652,10 +2830,6 @@ impl RenderState {
(self.viewbox.zoom - self.cached_viewbox.zoom).abs() > f32::EPSILON
}
pub fn sync_cached_viewbox(&mut self) {
self.cached_viewbox = self.viewbox;
}
pub fn mark_touched(&mut self, uuid: Uuid) {
self.touched_ids.insert(uuid);
}

View File

@ -41,6 +41,10 @@ pub fn render_debug_cache_surface(render_state: &mut RenderState) {
}
pub fn render_wasm_label(render_state: &mut RenderState) {
if !render_state.options.show_wasm_info() {
return;
}
let canvas = render_state.surfaces.canvas(SurfaceId::Target);
let skia::ISize { width, height } = canvas.base_layer_size();
let mut paint = skia::Paint::default();
@ -57,7 +61,7 @@ pub fn render_wasm_label(render_state: &mut RenderState) {
let debug_font = render_state.fonts.debug_font();
canvas.draw_str(str, p, debug_font, &paint);
if render_state.options.show_info_text() {
if render_state.options.is_text_editor_v3() {
str = "TEXT EDITOR / V3";
let (scalar, _) = render_state.fonts.debug_font().measure_str(str, None);
@ -179,9 +183,12 @@ pub fn render_debug_shape(
#[cfg(target_arch = "wasm32")]
#[allow(dead_code)]
pub fn console_debug_surface(render_state: &mut RenderState, id: SurfaceId) {
let base64_image = render_state.surfaces.base64_snapshot(id);
let base64_image = render_state
.surfaces
.base64_snapshot(id)
.expect("Failed to get base64 image");
run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')"))
run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')"));
}
#[allow(dead_code)]
@ -194,7 +201,10 @@ pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId,
rect.bottom as i32,
);
let base64_image = render_state.surfaces.base64_snapshot_rect(id, int_rect);
let base64_image = render_state
.surfaces
.base64_snapshot_rect(id, int_rect)
.expect("Failed to get base64 image");
if let Some(base64_image) = base64_image {
run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')"))

View File

@ -1,6 +1,7 @@
use skia_safe::{self as skia, Paint, RRect};
use super::{filters, RenderState, SurfaceId};
use crate::error::Result;
use crate::render::get_source_rect;
use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, StrokeKind, Type};
@ -20,12 +21,11 @@ fn draw_image_fill(
antialias: bool,
surface_id: SurfaceId,
) {
let image = render_state.images.get(&image_fill.id());
if image.is_none() {
let Some(image) = render_state.images.get(&image_fill.id()) else {
return;
}
};
let size = image.unwrap().dimensions();
let size = image.dimensions();
let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id);
let container = &shape.selrect;
let path_transform = shape.to_path_transform();
@ -85,15 +85,13 @@ fn draw_image_fill(
}
// Draw the image with the calculated destination rectangle
if let Some(image) = image {
canvas.draw_image_rect_with_sampling_options(
image,
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
dest_rect,
render_state.sampling_options,
paint,
);
}
canvas.draw_image_rect_with_sampling_options(
image,
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
dest_rect,
render_state.sampling_options,
paint,
);
// Restore the canvas to remove the clipping
canvas.restore();
@ -109,9 +107,9 @@ pub fn render(
antialias: bool,
surface_id: SurfaceId,
outset: Option<f32>,
) {
) -> Result<()> {
if fills.is_empty() {
return;
return Ok(());
}
let scale = render_state.get_scale().max(1e-6);
@ -134,9 +132,9 @@ pub fn render(
surface_id,
outset,
inset,
);
)?;
}
return;
return Ok(());
}
let mut paint = merge_fills(fills, shape.selrect);
@ -152,15 +150,17 @@ pub fn render(
let mut filtered_paint = paint.clone();
filtered_paint.set_image_filter(image_filter.clone());
draw_fill_to_surface(state, shape, temp_surface, &filtered_paint, outset, inset);
Ok(())
},
) {
return;
)? {
return Ok(());
} else {
paint.set_image_filter(image_filter);
}
}
draw_fill_to_surface(render_state, shape, surface_id, &paint, outset, inset);
Ok(())
}
/// Draws a single paint (with a merged shader) to the appropriate surface
@ -203,7 +203,7 @@ fn render_single_fill(
surface_id: SurfaceId,
outset: Option<f32>,
inset: Option<f32>,
) {
) -> Result<()> {
let mut paint = fill.to_paint(&shape.selrect, antialias);
if let Some(image_filter) = shape.image_filter(1.) {
let bounds = image_filter.compute_fast_bounds(shape.selrect);
@ -224,9 +224,10 @@ fn render_single_fill(
outset,
inset,
);
Ok(())
},
) {
return;
)? {
return Ok(());
} else {
paint.set_image_filter(image_filter);
}
@ -242,6 +243,7 @@ fn render_single_fill(
outset,
inset,
);
Ok(())
}
#[allow(clippy::too_many_arguments)]

View File

@ -1,6 +1,7 @@
use skia_safe::{self as skia, ImageFilter, Rect};
use super::{RenderState, SurfaceId};
use crate::error::Result;
/// Composes two image filters, returning a combined filter if both are present,
/// or the individual filter if only one is present, or None if neither is present.
@ -36,12 +37,12 @@ pub fn render_with_filter_surface<F>(
bounds: Rect,
target_surface: SurfaceId,
draw_fn: F,
) -> bool
) -> Result<bool>
where
F: FnOnce(&mut RenderState, SurfaceId),
F: FnOnce(&mut RenderState, SurfaceId) -> Result<()>,
{
if let Some((mut surface, scale)) =
render_into_filter_surface(render_state, bounds, 1.0, draw_fn)
render_into_filter_surface(render_state, bounds, 1.0, draw_fn)?
{
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
@ -58,9 +59,9 @@ where
surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
canvas.restore();
}
true
Ok(true)
} else {
false
Ok(false)
}
}
@ -81,12 +82,12 @@ pub fn render_into_filter_surface<F>(
bounds: Rect,
extra_downscale: f32,
draw_fn: F,
) -> Option<(skia::Surface, f32)>
) -> Result<Option<(skia::Surface, f32)>>
where
F: FnOnce(&mut RenderState, SurfaceId),
F: FnOnce(&mut RenderState, SurfaceId) -> Result<()>,
{
if !bounds.is_finite() || bounds.width() <= 0.0 || bounds.height() <= 0.0 {
return None;
return Ok(None);
}
let filter_id = SurfaceId::Filter;
@ -125,10 +126,10 @@ where
canvas.translate((-bounds.left, -bounds.top));
}
draw_fn(render_state, filter_id);
draw_fn(render_state, filter_id)?;
render_state.surfaces.canvas(filter_id).restore();
let filter_surface = render_state.surfaces.surface_clone(filter_id);
Some((filter_surface, scale))
Ok(Some((filter_surface, scale)))
}

View File

@ -1,6 +1,7 @@
use skia_safe::{self as skia, textlayout, Font, FontMgr};
use std::collections::HashSet;
use crate::error::{Error, Result};
use crate::shapes::{FontFamily, FontStyle};
use crate::uuid::Uuid;
@ -26,7 +27,7 @@ pub struct FontStore {
}
impl FontStore {
pub fn new() -> Self {
pub fn try_new() -> Result<Self> {
let font_mgr = FontMgr::new();
let font_provider = load_default_provider(&font_mgr);
let mut font_collection = skia::textlayout::FontCollection::new();
@ -34,17 +35,19 @@ impl FontStore {
let debug_typeface = font_provider
.match_family_style(default_font().as_str(), skia::FontStyle::default())
.unwrap();
.ok_or(Error::CriticalError(
"Failed to match default font".to_string(),
))?;
let debug_font = skia::Font::new(debug_typeface, 10.0);
Self {
Ok(Self {
font_mgr,
font_provider,
font_collection,
debug_font,
fallback_fonts: HashSet::new(),
}
})
}
pub fn set_scale_debug_font(&mut self, dpr: f32) {
@ -70,7 +73,7 @@ impl FontStore {
font_data: &[u8],
is_emoji: bool,
is_fallback: bool,
) -> Result<(), String> {
) -> Result<()> {
if self.has_family(&family, is_emoji) {
return Ok(());
}
@ -78,7 +81,9 @@ impl FontStore {
let typeface = self
.font_mgr
.new_from_data(font_data, None)
.ok_or("Failed to create typeface")?;
.ok_or(Error::CriticalError(
"Failed to create typeface".to_string(),
))?;
let alias = format!("{}", family);
let font_name = if is_emoji {

View File

@ -1,3 +1,4 @@
use crate::error::{Error, Result};
use skia_safe::gpu::{self, gl::FramebufferInfo, gl::TextureInfo, DirectContext};
use skia_safe::{self as skia, ISize};
@ -8,24 +9,30 @@ pub struct GpuState {
}
impl GpuState {
pub fn new() -> Self {
let interface = gpu::gl::Interface::new_native().unwrap();
let context = gpu::direct_contexts::make_gl(interface, None).unwrap();
pub fn try_new() -> Result<Self> {
let interface = gpu::gl::Interface::new_native().ok_or(Error::CriticalError(
"Failed to create GL interface".to_string(),
))?;
let context = gpu::direct_contexts::make_gl(interface, None).ok_or(
Error::CriticalError("Failed to create GL context".to_string()),
)?;
let framebuffer_info = {
let mut fboid: gl::types::GLint = 0;
unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) };
FramebufferInfo {
fboid: fboid.try_into().unwrap(),
fboid: fboid.try_into().map_err(|_| {
Error::CriticalError("Failed to convert GL framebuffer ID to u32".to_string())
})?,
format: gpu::gl::Format::RGBA8.into(),
protected: gpu::Protected::No,
}
};
GpuState {
Ok(GpuState {
context,
framebuffer_info,
}
})
}
fn create_webgl_texture(&mut self, width: i32, height: i32) -> gl::types::GLuint {
@ -56,7 +63,11 @@ impl GpuState {
texture_id
}
pub fn create_surface_with_isize(&mut self, label: String, size: ISize) -> skia::Surface {
pub fn create_surface_with_isize(
&mut self,
label: String,
size: ISize,
) -> Result<skia::Surface> {
self.create_surface_with_dimensions(label, size.width, size.height)
}
@ -65,7 +76,7 @@ impl GpuState {
label: String,
width: i32,
height: i32,
) -> skia::Surface {
) -> Result<skia::Surface> {
let backend_texture = unsafe {
let texture_id = self.create_webgl_texture(width, height);
let texture_info = TextureInfo {
@ -77,7 +88,7 @@ impl GpuState {
gpu::backend_textures::make_gl((width, height), gpu::Mipmapped::No, texture_info, label)
};
gpu::surfaces::wrap_backend_texture(
let surface = gpu::surfaces::wrap_backend_texture(
&mut self.context,
&backend_texture,
gpu::SurfaceOrigin::BottomLeft,
@ -86,15 +97,19 @@ impl GpuState {
None,
None,
)
.unwrap()
.ok_or(Error::CriticalError(
"Failed to create Skia surface".to_string(),
))?;
Ok(surface)
}
/// Create a Skia surface that will be used for rendering.
pub fn create_target_surface(&mut self, width: i32, height: i32) -> skia::Surface {
pub fn create_target_surface(&mut self, width: i32, height: i32) -> Result<skia::Surface> {
let backend_render_target =
gpu::backend_render_targets::make_gl((width, height), 1, 8, self.framebuffer_info);
gpu::surfaces::wrap_backend_render_target(
let surface = gpu::surfaces::wrap_backend_render_target(
&mut self.context,
&backend_render_target,
gpu::SurfaceOrigin::BottomLeft,
@ -102,6 +117,10 @@ impl GpuState {
None,
None,
)
.unwrap()
.ok_or(Error::CriticalError(
"Failed to create Skia surface".to_string(),
))?;
Ok(surface)
}
}

View File

@ -2,6 +2,7 @@ use crate::math::Rect as MathRect;
use crate::shapes::ImageFill;
use crate::uuid::Uuid;
use crate::error::{Error, Result};
use skia_safe::gpu::{surfaces, Budgeted, DirectContext};
use skia_safe::{self as skia, Codec, ISize};
use std::collections::HashMap;
@ -70,7 +71,7 @@ fn create_image_from_gl_texture(
texture_id: u32,
width: i32,
height: i32,
) -> Result<Image, String> {
) -> Result<Image> {
use skia_safe::gpu;
use skia_safe::gpu::gl::TextureInfo;
@ -99,7 +100,9 @@ fn create_image_from_gl_texture(
skia::AlphaType::Premul,
None,
)
.ok_or("Failed to create Skia image from GL texture")?;
.ok_or(crate::error::Error::CriticalError(
"Failed to create Skia image from GL texture".to_string(),
))?;
Ok(image)
}
@ -147,11 +150,16 @@ impl ImageStore {
}
}
pub fn add(&mut self, id: Uuid, is_thumbnail: bool, image_data: &[u8]) -> Result<(), String> {
pub fn add(
&mut self,
id: Uuid,
is_thumbnail: bool,
image_data: &[u8],
) -> crate::error::Result<()> {
let key = (id, is_thumbnail);
if self.images.contains_key(&key) {
return Err("Image already exists".to_string());
return Err(Error::RecoverableError("Image already exists".to_string()));
}
let raw_data = image_data.to_vec();
@ -174,11 +182,11 @@ impl ImageStore {
texture_id: u32,
width: i32,
height: i32,
) -> Result<(), String> {
) -> Result<()> {
let key = (id, is_thumbnail);
if self.images.contains_key(&key) {
return Err("Image already exists".to_string());
return Err(Error::RecoverableError("Image already exists".to_string()));
}
// Create a Skia image from the existing GL texture

View File

@ -1,38 +1,43 @@
use crate::options;
// Render options flags
const DEBUG_VISIBLE: u32 = 0x01;
const PROFILE_REBUILD_TILES: u32 = 0x02;
const TEXT_EDITOR_V3: u32 = 0x04;
const SHOW_WASM_INFO: u32 = 0x08;
#[derive(Debug, Copy, Clone, PartialEq, Default)]
pub struct RenderOptions {
pub flags: u32,
pub dpr: Option<f32>,
fast_mode: bool,
}
impl RenderOptions {
pub fn is_debug_visible(&self) -> bool {
self.flags & options::DEBUG_VISIBLE == options::DEBUG_VISIBLE
self.flags & DEBUG_VISIBLE == DEBUG_VISIBLE
}
pub fn is_profile_rebuild_tiles(&self) -> bool {
self.flags & options::PROFILE_REBUILD_TILES == options::PROFILE_REBUILD_TILES
self.flags & PROFILE_REBUILD_TILES == PROFILE_REBUILD_TILES
}
/// Use fast mode to enable / disable expensive operations
pub fn is_fast_mode(&self) -> bool {
self.flags & options::FAST_MODE == options::FAST_MODE
self.fast_mode
}
pub fn set_fast_mode(&mut self, enabled: bool) {
if enabled {
self.flags |= options::FAST_MODE;
} else {
self.flags &= !options::FAST_MODE;
}
self.fast_mode = enabled;
}
pub fn dpr(&self) -> f32 {
self.dpr.unwrap_or(1.0)
}
pub fn show_info_text(&self) -> bool {
self.flags & options::INFO_TEXT == options::INFO_TEXT
pub fn is_text_editor_v3(&self) -> bool {
self.flags & TEXT_EDITOR_V3 == TEXT_EDITOR_V3
}
pub fn show_wasm_info(&self) -> bool {
self.flags & SHOW_WASM_INFO == SHOW_WASM_INFO
}
}

View File

@ -3,6 +3,7 @@ use crate::render::strokes;
use crate::shapes::{ParagraphBuilderGroup, Shadow, Shape, Stroke, Type};
use skia_safe::{canvas::SaveLayerRec, Paint, Path};
use crate::error::Result;
use crate::render::text;
// Fill Shadows
@ -36,7 +37,7 @@ pub fn render_stroke_inner_shadows(
stroke: &Stroke,
antialias: bool,
surface_id: SurfaceId,
) {
) -> Result<()> {
if !shape.has_fills() {
for shadow in shape.inner_shadows_visible() {
let filter = shadow.get_inner_shadow_filter();
@ -48,9 +49,10 @@ pub fn render_stroke_inner_shadows(
filter.as_ref(),
antialias,
None, // Inner shadows don't use spread
)
)?;
}
}
Ok(())
}
// Render text paths (unused)
@ -133,9 +135,9 @@ pub fn render_text_shadows(
surface_id: Option<SurfaceId>,
shadows: &[Paint],
blur_filter: &Option<skia_safe::ImageFilter>,
) {
) -> Result<()> {
if stroke_paragraphs_group.is_empty() {
return;
return Ok(());
}
let canvas = render_state
@ -156,7 +158,7 @@ pub fn render_text_shadows(
blur_filter.as_ref(),
None,
None,
);
)?;
for stroke_paragraphs in stroke_paragraphs_group.iter_mut() {
text::render(
@ -169,9 +171,10 @@ pub fn render_text_shadows(
blur_filter.as_ref(),
None,
None,
);
)?;
}
canvas.restore();
}
Ok(())
}

View File

@ -6,6 +6,7 @@ use crate::shapes::{
use skia_safe::{self as skia, ImageFilter, RRect};
use super::{filters, RenderState, SurfaceId};
use crate::error::{Error, Result};
use crate::render::filters::compose_filters;
use crate::render::{get_dest_rect, get_source_rect};
@ -294,16 +295,16 @@ fn handle_stroke_caps(
blur: Option<&ImageFilter>,
_antialias: bool,
) {
let mut points = path.points().to_vec();
// Curves can have duplicated points, so let's remove consecutive duplicated points
points.dedup();
let c_points = points.len();
// Closed shapes don't have caps
if c_points >= 2 && is_open {
let first_point = points.first().unwrap();
let last_point = points.last().unwrap();
if !is_open {
return;
}
// Curves can have duplicated points, so let's remove consecutive duplicated points
let mut points = path.points().to_vec();
points.dedup();
if let [first_point, .., last_point] = points.as_slice() {
let mut paint_stroke = paint.clone();
if let Some(filter) = blur {
@ -328,7 +329,7 @@ fn handle_stroke_caps(
stroke.width,
&mut paint_stroke,
last_point,
&points[c_points - 2],
&points[points.len() - 2],
);
}
}
@ -456,14 +457,13 @@ fn draw_image_stroke_in_container(
image_fill: &ImageFill,
antialias: bool,
surface_id: SurfaceId,
) {
) -> Result<()> {
let scale = render_state.get_scale();
let image = render_state.images.get(&image_fill.id());
if image.is_none() {
return;
}
let Some(image) = render_state.images.get(&image_fill.id()) else {
return Ok(());
};
let size = image.unwrap().dimensions();
let size = image.dimensions();
let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id);
let container = &shape.selrect;
let path_transform = shape.to_path_transform();
@ -509,7 +509,10 @@ fn draw_image_stroke_in_container(
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
if let Some(p) = shape_type.path() {
canvas.save();
let path = p.to_skia_path().make_transform(&path_transform.unwrap());
let path = p.to_skia_path().make_transform(
&path_transform.ok_or(Error::CriticalError("No path transform".to_string()))?,
);
let stroke_kind = stroke.render_kind(p.is_open());
match stroke_kind {
StrokeKind::Inner => {
@ -561,7 +564,7 @@ fn draw_image_stroke_in_container(
canvas.clip_rect(dest_rect, skia::ClipOp::Intersect, antialias);
canvas.draw_image_rect_with_sampling_options(
image.unwrap(),
image,
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
dest_rect,
render_state.sampling_options,
@ -571,7 +574,9 @@ fn draw_image_stroke_in_container(
// Clear outer stroke for paths if necessary. When adding an outer stroke we need to empty the stroke added too in the inner area.
if let Type::Path(p) = &shape.shape_type {
if stroke.render_kind(p.is_open()) == StrokeKind::Outer {
let path = p.to_skia_path().make_transform(&path_transform.unwrap());
let path = p.to_skia_path().make_transform(
&path_transform.ok_or(Error::CriticalError("No path transform".to_string()))?,
);
let mut clear_paint = skia::Paint::default();
clear_paint.set_blend_mode(skia::BlendMode::Clear);
clear_paint.set_anti_alias(antialias);
@ -581,6 +586,7 @@ fn draw_image_stroke_in_container(
// Restore canvas state
canvas.restore();
Ok(())
}
/// Renders all strokes for a shape. Merges strokes that share the same
@ -593,9 +599,9 @@ pub fn render(
surface_id: Option<SurfaceId>,
antialias: bool,
outset: Option<f32>,
) {
) -> Result<()> {
if strokes.is_empty() {
return;
return Ok(());
}
let has_image_fills = strokes.iter().any(|s| matches!(s.fill, Fill::Image(_)));
@ -655,13 +661,14 @@ pub fn render(
true,
true,
outset,
);
)?;
}
state.surfaces.canvas(temp_surface).restore();
Ok(())
},
) {
return;
)? {
return Ok(());
}
}
@ -675,9 +682,9 @@ pub fn render(
None,
antialias,
outset,
);
)?;
}
return;
return Ok(());
}
render_merged(
@ -688,7 +695,7 @@ pub fn render(
antialias,
false,
outset,
);
)
}
fn strokes_share_geometry(strokes: &[&Stroke]) -> bool {
@ -709,7 +716,7 @@ fn render_merged(
antialias: bool,
bypass_filter: bool,
outset: Option<f32>,
) {
) -> Result<()> {
let representative = *strokes
.last()
.expect("render_merged expects at least one stroke");
@ -761,14 +768,15 @@ fn render_merged(
antialias,
true,
outset,
);
)?;
state.surfaces.apply_mut(temp_surface as u32, |surface| {
surface.canvas().restore();
});
Ok(())
},
) {
return;
)? {
return Ok(());
}
}
}
@ -844,6 +852,7 @@ fn render_merged(
}
_ => unreachable!("This shape should not have strokes"),
}
Ok(())
}
/// Renders a single stroke. Used by the shadow module which needs per-stroke
@ -857,7 +866,7 @@ pub fn render_single(
shadow: Option<&ImageFilter>,
antialias: bool,
outset: Option<f32>,
) {
) -> Result<()> {
render_single_internal(
render_state,
shape,
@ -868,7 +877,7 @@ pub fn render_single(
false,
false,
outset,
);
)
}
#[allow(clippy::too_many_arguments)]
@ -882,7 +891,7 @@ fn render_single_internal(
bypass_filter: bool,
skip_blur: bool,
outset: Option<f32>,
) {
) -> Result<()> {
if !bypass_filter {
if let Some(image_filter) = shape.image_filter(1.) {
let mut content_bounds = shape.selrect;
@ -916,10 +925,10 @@ fn render_single_internal(
true,
true,
outset,
);
)
},
) {
return;
)? {
return Ok(());
}
}
}
@ -949,7 +958,7 @@ fn render_single_internal(
image_fill,
antialias,
target_surface,
);
)?;
}
} else {
match &shape.shape_type {
@ -1014,6 +1023,7 @@ fn render_single_internal(
_ => unreachable!("This shape should not have strokes"),
}
}
Ok(())
}
// Render text paths (unused)

View File

@ -1,3 +1,4 @@
use crate::error::{Error, Result};
use crate::performance;
use crate::shapes::Shape;
@ -61,38 +62,39 @@ pub struct Surfaces {
#[allow(dead_code)]
impl Surfaces {
pub fn new(
pub fn try_new(
gpu_state: &mut GpuState,
(width, height): (i32, i32),
sampling_options: skia::SamplingOptions,
tile_dims: skia::ISize,
) -> Self {
) -> Result<Self> {
let extra_tile_dims = skia::ISize::new(
tile_dims.width * TILE_SIZE_MULTIPLIER,
tile_dims.height * TILE_SIZE_MULTIPLIER,
);
let margins = skia::ISize::new(extra_tile_dims.width / 4, extra_tile_dims.height / 4);
let target = gpu_state.create_target_surface(width, height);
let filter = gpu_state.create_surface_with_isize("filter".to_string(), extra_tile_dims);
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height);
let current = gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims);
let target = gpu_state.create_target_surface(width, height)?;
let filter = gpu_state.create_surface_with_isize("filter".to_string(), extra_tile_dims)?;
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height)?;
let current =
gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims)?;
let drop_shadows =
gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims);
gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims)?;
let inner_shadows =
gpu_state.create_surface_with_isize("inner_shadows".to_string(), extra_tile_dims);
let text_drop_shadows =
gpu_state.create_surface_with_isize("text_drop_shadows".to_string(), extra_tile_dims);
gpu_state.create_surface_with_isize("inner_shadows".to_string(), extra_tile_dims)?;
let text_drop_shadows = gpu_state
.create_surface_with_isize("text_drop_shadows".to_string(), extra_tile_dims)?;
let shape_fills =
gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims);
gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims)?;
let shape_strokes =
gpu_state.create_surface_with_isize("shape_strokes".to_string(), extra_tile_dims);
gpu_state.create_surface_with_isize("shape_strokes".to_string(), extra_tile_dims)?;
let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height);
let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height);
let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height)?;
let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height)?;
let tiles = TileTextureCache::new();
Surfaces {
Ok(Surfaces {
target,
filter,
cache,
@ -108,15 +110,25 @@ impl Surfaces {
sampling_options,
margins,
dirty_surfaces: 0,
}
})
}
pub fn clear_tiles(&mut self) {
self.tiles.clear();
}
pub fn resize(&mut self, gpu_state: &mut GpuState, new_width: i32, new_height: i32) {
self.reset_from_target(gpu_state.create_target_surface(new_width, new_height));
pub fn margins(&self) -> skia::ISize {
self.margins
}
pub fn resize(
&mut self,
gpu_state: &mut GpuState,
new_width: i32,
new_height: i32,
) -> Result<()> {
self.reset_from_target(gpu_state.create_target_surface(new_width, new_height)?)?;
Ok(())
}
pub fn snapshot(&mut self, id: SurfaceId) -> skia::Image {
@ -128,26 +140,33 @@ impl Surfaces {
(self.filter.width(), self.filter.height())
}
pub fn base64_snapshot(&mut self, id: SurfaceId) -> String {
pub fn base64_snapshot(&mut self, id: SurfaceId) -> Result<String> {
let surface = self.get_mut(id);
let image = surface.image_snapshot();
let mut context = surface.direct_context();
let encoded_image = image
.encode(context.as_mut(), skia::EncodedImageFormat::PNG, None)
.unwrap();
general_purpose::STANDARD.encode(encoded_image.as_bytes())
.ok_or(Error::CriticalError("Failed to encode image".to_string()))?;
Ok(general_purpose::STANDARD.encode(encoded_image.as_bytes()))
}
pub fn base64_snapshot_rect(&mut self, id: SurfaceId, irect: skia::IRect) -> Option<String> {
pub fn base64_snapshot_rect(
&mut self,
id: SurfaceId,
irect: skia::IRect,
) -> Result<Option<String>> {
let surface = self.get_mut(id);
if let Some(image) = surface.image_snapshot_with_bounds(irect) {
let mut context = surface.direct_context();
let encoded_image = image
.encode(context.as_mut(), skia::EncodedImageFormat::PNG, None)
.unwrap();
return Some(general_purpose::STANDARD.encode(encoded_image.as_bytes()));
.ok_or(Error::CriticalError("Failed to encode image".to_string()))?;
Ok(Some(
general_purpose::STANDARD.encode(encoded_image.as_bytes()),
))
} else {
Ok(None)
}
None
}
/// Returns a mutable reference to the canvas and automatically marks
@ -337,22 +356,41 @@ impl Surfaces {
}
}
fn reset_from_target(&mut self, target: skia::Surface) {
fn reset_from_target(&mut self, target: skia::Surface) -> Result<()> {
let dim = (target.width(), target.height());
self.target = target;
self.filter = self.target.new_surface_with_dimensions(dim).unwrap();
self.debug = self.target.new_surface_with_dimensions(dim).unwrap();
self.ui = self.target.new_surface_with_dimensions(dim).unwrap();
self.filter = self
.target
.new_surface_with_dimensions(dim)
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
self.debug = self
.target
.new_surface_with_dimensions(dim)
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
self.ui = self
.target
.new_surface_with_dimensions(dim)
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
// The rest are tile size surfaces
Ok(())
}
pub fn resize_cache(&mut self, cache_dims: skia::ISize, interest_area_threshold: i32) {
self.cache = self.target.new_surface_with_dimensions(cache_dims).unwrap();
pub fn resize_cache(
&mut self,
cache_dims: skia::ISize,
interest_area_threshold: i32,
) -> Result<()> {
self.cache = self
.target
.new_surface_with_dimensions(cache_dims)
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
self.cache.canvas().reset_matrix();
self.cache.canvas().translate((
(interest_area_threshold as f32 * TILE_SIZE),
(interest_area_threshold as f32 * TILE_SIZE),
));
Ok(())
}
pub fn draw_rect_to(
@ -570,11 +608,22 @@ impl Surfaces {
);
}
/// Full cache reset: clears both the tile texture cache and the cache canvas.
/// Used by `rebuild_tiles` (full rebuild). For shallow rebuilds that preserve
/// the cache canvas for scaled previews, use `invalidate_tile_cache` instead.
pub fn remove_cached_tiles(&mut self, color: skia::Color) {
self.tiles.clear();
self.cache.canvas().clear(color);
}
/// Invalidate the tile texture cache without clearing the cache canvas.
/// This forces all tiles to be re-rendered, but preserves the cache canvas
/// so that `render_from_cache` can still show a scaled preview of the old
/// content while new tiles are being rendered.
pub fn invalidate_tile_cache(&mut self) {
self.tiles.clear();
}
pub fn gc(&mut self) {
self.tiles.gc();
}

View File

@ -1,5 +1,6 @@
use super::{filters, RenderState, Shape, SurfaceId};
use crate::{
error::Result,
math::Rect,
shapes::{
calculate_position_data, calculate_text_layout_data, merge_fills, set_paint_fill,
@ -66,7 +67,7 @@ pub fn stroke_paragraph_builder_group_from_text(
}
let stroke_paragraphs: Vec<ParagraphBuilder> = (0..stroke_paragraphs_map.len())
.map(|i| stroke_paragraphs_map.remove(&i).unwrap())
.filter_map(|i| stroke_paragraphs_map.remove(&i))
.collect();
paragraph_group.push(stroke_paragraphs);
@ -195,7 +196,7 @@ pub fn render_with_bounds_outset(
stroke_bounds_outset: f32,
fill_inset: Option<f32>,
layer_opacity: Option<f32>,
) {
) -> Result<()> {
if let Some(render_state) = render_state {
let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
@ -225,9 +226,10 @@ pub fn render_with_bounds_outset(
fill_inset,
layer_opacity,
);
Ok(())
},
) {
return;
)? {
return Ok(());
}
}
}
@ -242,7 +244,7 @@ pub fn render_with_bounds_outset(
fill_inset,
layer_opacity,
);
return;
return Ok(());
}
if let Some(canvas) = canvas {
@ -256,6 +258,7 @@ pub fn render_with_bounds_outset(
layer_opacity,
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
@ -269,7 +272,7 @@ pub fn render(
blur: Option<&ImageFilter>,
fill_inset: Option<f32>,
layer_opacity: Option<f32>,
) {
) -> Result<()> {
render_with_bounds_outset(
render_state,
canvas,
@ -281,7 +284,7 @@ pub fn render(
0.0,
fill_inset,
layer_opacity,
);
)
}
fn render_text_on_canvas(

View File

@ -1241,6 +1241,7 @@ impl Shape {
let sigma = radius_to_sigma(blur.value * scale);
skia::image_filters::blur((sigma, sigma), None, None, None)
}
BlurType::BackgroundBlur => None,
})
}
@ -1253,6 +1254,7 @@ impl Shape {
let sigma = radius_to_sigma(blur.value * scale);
skia::MaskFilter::blur(skia::BlurStyle::Normal, sigma, Some(true))
}
BlurType::BackgroundBlur => None,
})
}

View File

@ -15,6 +15,7 @@ pub fn radius_to_sigma(radius: f32) -> f32 {
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BlurType {
LayerBlur,
BackgroundBlur,
}
#[derive(Debug, Clone, Copy, PartialEq)]

View File

@ -9,6 +9,7 @@ pub mod grid_layout;
use crate::math::{self as math, bools, identitish, is_close_to, Bounds, Matrix, Point};
use common::GetBounds;
use crate::error::Result;
use crate::shapes::{
ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, TransformEntry,
TransformEntrySource, Type,
@ -24,9 +25,9 @@ fn propagate_children(
parent_bounds_after: &Bounds,
transform: Matrix,
bounds: &HashMap<Uuid, Bounds>,
) -> VecDeque<Modifier> {
) -> Result<VecDeque<Modifier>> {
if identitish(&transform) {
return VecDeque::new();
return Ok(VecDeque::new());
}
let mut result = VecDeque::new();
@ -74,12 +75,12 @@ fn propagate_children(
constraint_v,
transform,
child.ignore_constraints,
);
)?;
result.push_back(Modifier::transform_propagate(*child_id, transform));
}
result
Ok(result)
}
fn calculate_group_bounds(
@ -172,9 +173,9 @@ fn propagate_transform(
entries: &mut VecDeque<Modifier>,
bounds: &mut HashMap<Uuid, Bounds>,
modifiers: &mut HashMap<Uuid, Matrix>,
) {
) -> Result<()> {
let Some(shape) = state.shapes.get(&entry.id) else {
return;
return Ok(());
};
let shapes = &state.shapes;
@ -249,7 +250,7 @@ fn propagate_transform(
&shape_bounds_after,
transform,
bounds,
);
)?;
entries.append(&mut children);
}
@ -275,6 +276,7 @@ fn propagate_transform(
entries.push_back(Modifier::reflow(parent.id, false));
}
}
Ok(())
}
fn propagate_reflow(
@ -338,34 +340,35 @@ fn reflow_shape(
reflown: &mut HashSet<Uuid>,
entries: &mut VecDeque<Modifier>,
bounds: &mut HashMap<Uuid, Bounds>,
) {
) -> Result<()> {
let Some(shape) = state.shapes.get(id) else {
return;
return Ok(());
};
let shapes = &state.shapes;
let Type::Frame(frame_data) = &shape.shape_type else {
return;
return Ok(());
};
if let Some(Layout::FlexLayout(layout_data, flex_data)) = &frame_data.layout {
let mut children =
flex_layout::reflow_flex_layout(shape, layout_data, flex_data, shapes, bounds);
flex_layout::reflow_flex_layout(shape, layout_data, flex_data, shapes, bounds)?;
entries.append(&mut children);
} else if let Some(Layout::GridLayout(layout_data, grid_data)) = &frame_data.layout {
let mut children =
grid_layout::reflow_grid_layout(shape, layout_data, grid_data, shapes, bounds);
grid_layout::reflow_grid_layout(shape, layout_data, grid_data, shapes, bounds)?;
entries.append(&mut children);
}
reflown.insert(*id);
Ok(())
}
pub fn propagate_modifiers(
state: &State,
modifiers: &[TransformEntry],
pixel_precision: bool,
) -> Vec<TransformEntry> {
) -> Result<Vec<TransformEntry>> {
let mut entries: VecDeque<_> = modifiers
.iter()
.map(|entry| {
@ -399,7 +402,7 @@ pub fn propagate_modifiers(
&mut entries,
&mut bounds,
&mut modifiers,
),
)?,
Modifier::Reflow(id, force_reflow) => {
if force_reflow {
reflown.remove(&id);
@ -437,16 +440,16 @@ pub fn propagate_modifiers(
if reflown.contains(id) {
continue;
}
reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp);
reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp)?;
}
layout_reflows = HashSet::new();
}
#[allow(dead_code)]
modifiers
// #[allow(dead_code)]
Ok(modifiers
.iter()
.map(|(key, val)| TransformEntry::from_input(*key, *val))
.collect()
.collect())
}
#[cfg(test)]
@ -494,7 +497,8 @@ mod tests {
&bounds_after,
transform,
&HashMap::new(),
);
)
.unwrap();
assert_eq!(result.len(), 1);
}

View File

@ -1,3 +1,4 @@
use crate::error::{Error, Result};
use crate::math::{is_move_only_matrix, Bounds, Matrix};
use crate::shapes::{ConstraintH, ConstraintV};
@ -105,14 +106,14 @@ pub fn propagate_shape_constraints(
constraint_v: ConstraintV,
transform: Matrix,
ignore_constrainst: bool,
) -> Matrix {
) -> Result<Matrix> {
// if the constrains are scale & scale or the transform has only moves we
// can propagate as is
if (ignore_constrainst
|| constraint_h == ConstraintH::Scale && constraint_v == ConstraintV::Scale)
|| is_move_only_matrix(&transform)
{
return transform;
return Ok(transform);
}
let mut transform = transform;
@ -133,7 +134,9 @@ pub fn propagate_shape_constraints(
parent_transform.post_translate(center);
parent_transform.pre_translate(-center);
let parent_transform_inv = &parent_transform.invert().unwrap();
let parent_transform_inv = &parent_transform.invert().ok_or(Error::CriticalError(
"Failed to invert parent transform".to_string(),
))?;
let origin = parent_transform_inv.map_point(child_bounds_after.nw);
let mut scale = Matrix::scale((scale_width, scale_height));
@ -160,5 +163,5 @@ pub fn propagate_shape_constraints(
transform.post_concat(&Matrix::translate(th + tv));
}
transform
Ok(transform)
}

View File

@ -1,4 +1,6 @@
#![allow(dead_code)]
use crate::error::{Error, Result};
use crate::math::{self as math, Bounds, Matrix, Point, Vector, VectorExt};
use crate::shapes::{
AlignContent, AlignItems, AlignSelf, FlexData, JustifyContent, LayoutData, LayoutItem,
@ -588,7 +590,7 @@ pub fn reflow_flex_layout(
flex_data: &FlexData,
shapes: ShapesPoolRef,
bounds: &mut HashMap<Uuid, Bounds>,
) -> VecDeque<Modifier> {
) -> Result<VecDeque<Modifier>> {
let mut result = VecDeque::new();
let layout_bounds = &bounds.find(shape);
let layout_axis = LayoutAxis::new(shape, layout_bounds, layout_data, flex_data);
@ -724,7 +726,9 @@ pub fn reflow_flex_layout(
let parent_transform = layout_bounds.transform_matrix().unwrap_or_default();
let parent_transform_inv = &parent_transform.invert().unwrap();
let parent_transform_inv = &parent_transform.invert().ok_or(Error::CriticalError(
"Failed to invert parent transform".to_string(),
))?;
let origin = parent_transform_inv.map_point(layout_bounds.nw);
let mut scale = Matrix::scale((scale_width, scale_height));
@ -737,5 +741,5 @@ pub fn reflow_flex_layout(
result.push_back(Modifier::parent(shape.id, scale));
bounds.insert(shape.id, layout_bounds_after);
}
result
Ok(result)
}

View File

@ -1,3 +1,4 @@
use crate::error::{Error, Result};
use crate::math::{self as math, intersect_rays, Bounds, Matrix, Point, Ray, Vector, VectorExt};
use crate::shapes::{
AlignContent, AlignItems, AlignSelf, Frame, GridCell, GridData, GridTrack, GridTrackType,
@ -6,6 +7,7 @@ use crate::shapes::{
};
use crate::state::ShapesPoolRef;
use crate::uuid::Uuid;
use std::collections::{HashMap, HashSet, VecDeque};
use super::common::GetBounds;
@ -704,7 +706,7 @@ pub fn reflow_grid_layout(
grid_data: &GridData,
shapes: ShapesPoolRef,
bounds: &mut HashMap<Uuid, Bounds>,
) -> VecDeque<Modifier> {
) -> Result<VecDeque<Modifier>> {
let mut result = VecDeque::new();
let layout_bounds = bounds.find(shape);
let children: HashSet<Uuid> = shape.children_ids_iter(true).copied().collect();
@ -825,7 +827,9 @@ pub fn reflow_grid_layout(
let parent_transform = layout_bounds.transform_matrix().unwrap_or_default();
let parent_transform_inv = &parent_transform.invert().unwrap();
let parent_transform_inv = &parent_transform.invert().ok_or(Error::CriticalError(
"Failed to invert parent transform".to_string(),
))?;
let origin = parent_transform_inv.map_point(layout_bounds.nw);
let mut scale = Matrix::scale((scale_width, scale_height));
@ -839,5 +843,5 @@ pub fn reflow_grid_layout(
bounds.insert(shape.id, layout_bounds_after);
}
result
Ok(result)
}

View File

@ -1139,7 +1139,11 @@ impl TextSpan {
fn process_ignored_chars(text: &str, browser: u8) -> String {
text.chars()
.filter_map(|c| {
if c < '\u{0020}' || c == '\u{2028}' || c == '\u{2029}' {
// Preserve line breaks: \n (U+000A), \r (U+000D), and Unicode separators
if c == '\n' || c == '\r' || c == '\u{2028}' || c == '\u{2029}' {
return Some(c);
}
if c < '\u{0020}' {
if browser == Browser::Firefox as u8 {
None
} else {

View File

@ -6,6 +6,7 @@ mod text_editor;
pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef};
pub use text_editor::*;
use crate::error::{Error, Result};
use crate::render::RenderState;
use crate::shapes::Shape;
use crate::tiles;
@ -28,41 +29,44 @@ pub(crate) struct State {
}
impl State {
pub fn new(width: i32, height: i32) -> Self {
State {
render_state: RenderState::new(width, height),
pub fn try_new(width: i32, height: i32) -> Result<Self> {
Ok(State {
render_state: RenderState::try_new(width, height)?,
text_editor_state: TextEditorState::new(),
current_id: None,
current_browser: 0,
shapes: ShapesPool::new(),
// TODO: Maybe this can be moved to a different object
saved_shapes: None,
}
})
}
// Creates a new temporary shapes pool.
// Will panic if a previous temporary pool exists.
pub fn start_temp_objects(mut self) -> Self {
pub fn start_temp_objects(mut self) -> Result<Self> {
if self.saved_shapes.is_some() {
panic!("Tried to start a temp objects while the previous have not been restored");
return Err(Error::CriticalError(
"Tried to start a temp objects while the previous have not been restored"
.to_string(),
));
}
self.saved_shapes = Some(self.shapes);
self.shapes = ShapesPool::new();
self
Ok(self)
}
// Disposes of the temporary shapes pool restoring the normal pool
// Will panic if a there is no temporary pool.
pub fn end_temp_objects(mut self) -> Self {
self.shapes = self
.saved_shapes
.expect("Tried to end temp objects but not content to be restored is present");
pub fn end_temp_objects(mut self) -> Result<Self> {
self.shapes = self.saved_shapes.ok_or(Error::CriticalError(
"Tried to end temp objects but not content to be restored is present".to_string(),
))?;
self.saved_shapes = None;
self
Ok(self)
}
pub fn resize(&mut self, width: i32, height: i32) {
self.render_state.resize(width, height);
pub fn resize(&mut self, width: i32, height: i32) -> Result<()> {
self.render_state.resize(width, height)
}
pub fn render_state_mut(&mut self) -> &mut RenderState {
@ -87,38 +91,34 @@ impl State {
self.render_state.render_from_cache(&self.shapes);
}
pub fn render_sync(&mut self, timestamp: i32) -> Result<(), String> {
pub fn render_sync(&mut self, timestamp: i32) -> Result<()> {
self.render_state
.start_render_loop(None, &self.shapes, timestamp, true)?;
Ok(())
.start_render_loop(None, &self.shapes, timestamp, true)
}
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<(), String> {
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<()> {
self.render_state
.start_render_loop(Some(id), &self.shapes, timestamp, true)?;
Ok(())
.start_render_loop(Some(id), &self.shapes, timestamp, true)
}
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> {
// If zoom changed, we MUST rebuild the tile index before using it.
// Otherwise, the index will have tiles from the old zoom level, causing visible
// tiles to appear empty. This can happen if start_render_loop() is called before
// set_view_end() finishes rebuilding the index, or if set_view_end() hasn't been
// called yet.
let zoom_changed = self.render_state.zoom_changed();
if zoom_changed {
self.rebuild_tiles_shallow();
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<()> {
// If zoom changed (e.g. interrupted zoom render followed by pan), the
// tile index may be stale for the new viewport position. Rebuild the
// index so shapes are mapped to the correct tiles. We use
// rebuild_tile_index (NOT rebuild_tiles_shallow) to preserve the tile
// texture cache — otherwise cached tiles with shadows/blur would be
// cleared and re-rendered in fast mode without effects.
if self.render_state.zoom_changed() {
self.render_state.rebuild_tile_index(&self.shapes);
}
self.render_state
.start_render_loop(None, &self.shapes, timestamp, false)?;
Ok(())
.start_render_loop(None, &self.shapes, timestamp, false)
}
pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<(), String> {
pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<()> {
self.render_state
.process_animation_frame(None, &self.shapes, timestamp)?;
Ok(())
.process_animation_frame(None, &self.shapes, timestamp)
}
pub fn clear_focus_mode(&mut self) {
@ -227,10 +227,10 @@ impl State {
let _ = self.render_state.render_preview(&self.shapes, timestamp);
}
pub fn rebuild_modifier_tiles(&mut self, ids: Vec<Uuid>) {
pub fn rebuild_modifier_tiles(&mut self, ids: Vec<Uuid>) -> Result<()> {
// Index-based storage is safe
self.render_state
.rebuild_modifier_tiles(&mut self.shapes, ids);
.rebuild_modifier_tiles(&mut self.shapes, ids)
}
pub fn font_collection(&self) -> &FontCollection {

View File

@ -103,9 +103,68 @@ pub struct TextEditorTheme {
pub cursor_color: Color,
}
pub struct TextComposition {
pub previous: String,
pub current: String,
pub is_composing: bool,
}
impl TextComposition {
pub fn new() -> Self {
Self {
previous: String::new(),
current: String::new(),
is_composing: false,
}
}
pub fn start(&mut self) -> bool {
if self.is_composing {
return false;
}
self.is_composing = true;
self.previous = String::new();
self.current = String::new();
true
}
pub fn update(&mut self, text: &str) -> bool {
if !self.is_composing {
self.is_composing = true;
}
self.previous = self.current.clone();
self.current = text.to_owned();
true
}
pub fn end(&mut self) -> bool {
if !self.is_composing {
return false;
}
self.is_composing = false;
true
}
pub fn get_selection(&self, selection: &TextSelection) -> TextSelection {
if self.previous.is_empty() {
return *selection;
}
let focus = selection.focus;
let previous_len = self.previous.chars().count();
let anchor = TextPositionWithAffinity::new_without_affinity(
focus.paragraph,
focus.offset + previous_len,
);
TextSelection { anchor, focus }
}
}
pub struct TextEditorState {
pub theme: TextEditorTheme,
pub selection: TextSelection,
pub composition: TextComposition,
pub is_active: bool,
// This property indicates that we've started
// selecting something with the pointer.
@ -125,6 +184,7 @@ impl TextEditorState {
cursor_color: CURSOR_COLOR,
},
selection: TextSelection::new(),
composition: TextComposition::new(),
is_active: false,
is_pointer_selection_active: false,
active_shape_id: None,

View File

@ -3,7 +3,6 @@ use crate::uuid::Uuid;
use crate::view::Viewbox;
use skia_safe as skia;
use std::collections::{HashMap, HashSet};
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
pub struct Tile(pub i32, pub i32);
@ -178,13 +177,10 @@ impl TileHashMap {
}
pub fn add_shape_at(&mut self, tile: Tile, shape_id: Uuid) {
self.grid.entry(tile).or_default();
self.index.entry(shape_id).or_default();
let tile_set = self.grid.get_mut(&tile).unwrap();
let tile_set = self.grid.entry(tile).or_default();
tile_set.insert(shape_id);
let index_set = self.index.get_mut(&shape_id).unwrap();
let index_set = self.index.entry(shape_id).or_default();
index_set.insert(tile);
}

View File

@ -7,7 +7,8 @@ use crate::{with_current_shape_mut, STATE};
#[repr(u8)]
#[allow(dead_code)]
pub enum RawBlurType {
LayerBlur = 0, // odd naming to comply with cljs value
LayerBlur = 0,
BackgroundBlur = 1,
}
impl From<u8> for RawBlurType {
@ -20,6 +21,7 @@ impl From<RawBlurType> for BlurType {
fn from(value: RawBlurType) -> Self {
match value {
RawBlurType::LayerBlur => BlurType::LayerBlur,
RawBlurType::BackgroundBlur => BlurType::BackgroundBlur,
}
}
}

View File

@ -1,11 +1,10 @@
use crate::error::{Error, Result};
use crate::mem;
use macros::wasm_error;
// use crate::mem::SerializableResult;
use crate::error::Error;
use crate::uuid::Uuid;
use crate::with_state_mut;
use crate::STATE;
use crate::{shapes::ImageFill, utils::uuid_from_u32_quartet};
use macros::wasm_error;
const FLAG_KEEP_ASPECT_RATIO: u8 = 1 << 0;
const IMAGE_IDS_SIZE: usize = 32;
@ -50,6 +49,7 @@ pub struct ShapeImageIds {
impl From<[u8; IMAGE_IDS_SIZE]> for ShapeImageIds {
fn from(bytes: [u8; IMAGE_IDS_SIZE]) -> Self {
// FIXME: this should probably be a try_from instead
let shape_id = Uuid::try_from(&bytes[0..16]).unwrap();
let image_id = Uuid::try_from(&bytes[16..32]).unwrap();
ShapeImageIds { shape_id, image_id }
@ -57,9 +57,9 @@ impl From<[u8; IMAGE_IDS_SIZE]> for ShapeImageIds {
}
impl TryFrom<Vec<u8>> for ShapeImageIds {
type Error = &'static str;
type Error = Error;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
fn try_from(value: Vec<u8>) -> Result<Self> {
let mut arr = [0u8; IMAGE_IDS_SIZE];
arr.copy_from_slice(&value);
Ok(ShapeImageIds::from(arr))
@ -68,13 +68,16 @@ impl TryFrom<Vec<u8>> for ShapeImageIds {
#[no_mangle]
#[wasm_error]
pub extern "C" fn store_image() -> crate::error::Result<()> {
pub extern "C" fn store_image() -> Result<()> {
let bytes = mem::bytes();
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap();
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec())?;
// Read is_thumbnail flag (4 bytes as u32)
let is_thumbnail_bytes = &bytes[IMAGE_IDS_SIZE..IMAGE_HEADER_SIZE];
let is_thumbnail_value = u32::from_le_bytes(is_thumbnail_bytes.try_into().unwrap());
let is_thumbnail_value =
u32::from_le_bytes(is_thumbnail_bytes.try_into().map_err(|_| {
Error::CriticalError("Invalid bytes for is_thumbnail flag".to_string())
})?);
let is_thumbnail = is_thumbnail_value != 0;
let image_bytes = &bytes[IMAGE_HEADER_SIZE..];
@ -104,9 +107,10 @@ pub extern "C" fn store_image() -> crate::error::Result<()> {
/// - bytes 44-47: height (i32)
#[no_mangle]
#[wasm_error]
pub extern "C" fn store_image_from_texture() -> crate::error::Result<()> {
pub extern "C" fn store_image_from_texture() -> Result<()> {
let bytes = mem::bytes();
// FIXME: where does this 48 come from?
if bytes.len() < 48 {
// FIXME: Review if this should be an critical or a recoverable error.
eprintln!("store_image_from_texture: insufficient data");
@ -116,23 +120,41 @@ pub extern "C" fn store_image_from_texture() -> crate::error::Result<()> {
));
}
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap();
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec())
.map_err(|_| Error::CriticalError("Invalid image ids".to_string()))?;
// FIXME: read bytes in a safe way
// Read is_thumbnail flag (4 bytes as u32)
let is_thumbnail_bytes = &bytes[IMAGE_IDS_SIZE..IMAGE_HEADER_SIZE];
let is_thumbnail_value = u32::from_le_bytes(is_thumbnail_bytes.try_into().unwrap());
let is_thumbnail_value =
u32::from_le_bytes(is_thumbnail_bytes.try_into().map_err(|_| {
Error::CriticalError("Invalid bytes for is_thumbnail flag".to_string())
})?);
let is_thumbnail = is_thumbnail_value != 0;
// Read GL texture ID (4 bytes as u32)
let texture_id_bytes = &bytes[36..40];
let texture_id = u32::from_le_bytes(texture_id_bytes.try_into().unwrap());
let texture_id = u32::from_le_bytes(
texture_id_bytes
.try_into()
.map_err(|_| Error::CriticalError("Invalid bytes for texture id".to_string()))?,
);
// Read width and height (8 bytes as two i32s)
let width_bytes = &bytes[40..44];
let width = i32::from_le_bytes(width_bytes.try_into().unwrap());
let width = i32::from_le_bytes(
width_bytes
.try_into()
.map_err(|_| Error::CriticalError("Invalid bytes for width".to_string()))?,
);
let height_bytes = &bytes[44..48];
let height = i32::from_le_bytes(height_bytes.try_into().unwrap());
let height = i32::from_le_bytes(
height_bytes
.try_into()
.map_err(|_| Error::CriticalError("Invalid bytes for height".to_string()))?,
);
with_state_mut!(state, {
if let Err(msg) = state.render_state_mut().add_image_from_gl_texture(
@ -142,6 +164,7 @@ pub extern "C" fn store_image_from_texture() -> crate::error::Result<()> {
width,
height,
) {
// FIXME: Review if we should return a RecoverableError
eprintln!("store_image_from_texture error: {}", msg);
}
state.touch_shape(ids.shape_id);

View File

@ -8,7 +8,7 @@ use crate::{uuid_from_u32_quartet, with_current_shape_mut, with_state, with_stat
use super::align;
#[allow(unused_imports)]
use crate::error::Result;
use crate::error::{Error, Result};
#[derive(Debug)]
#[repr(C, align(1))]
@ -177,9 +177,13 @@ pub extern "C" fn set_grid_columns() -> Result<()> {
let entries: Vec<GridTrack> = bytes
.chunks(size_of::<RawGridTrack>())
.map(|data| data.try_into().unwrap())
.map(|data: [u8; size_of::<RawGridTrack>()]| RawGridTrack::from(data).into())
.collect();
.map(|data| {
let track_bytes: [u8; size_of::<RawGridTrack>()] = data
.try_into()
.map_err(|_| Error::CriticalError("Invalid bytes for grid track".to_string()))?;
Ok(RawGridTrack::from(track_bytes).into())
})
.collect::<Result<Vec<_>>>()?;
with_current_shape_mut!(state, |shape: &mut Shape| {
shape.set_grid_columns(entries);
@ -196,9 +200,13 @@ pub extern "C" fn set_grid_rows() -> Result<()> {
let entries: Vec<GridTrack> = bytes
.chunks(size_of::<RawGridTrack>())
.map(|data| data.try_into().unwrap())
.map(|data: [u8; size_of::<RawGridTrack>()]| RawGridTrack::from(data).into())
.collect();
.map(|data| {
let track_bytes: [u8; size_of::<RawGridTrack>()] = data
.try_into()
.map_err(|_| Error::CriticalError("Invalid bytes for grid track".to_string()))?;
Ok(RawGridTrack::from(track_bytes).into())
})
.collect::<Result<Vec<_>>>()?;
with_current_shape_mut!(state, |shape: &mut Shape| {
shape.set_grid_rows(entries);
@ -215,9 +223,13 @@ pub extern "C" fn set_grid_cells() -> Result<()> {
let cells: Vec<RawGridCell> = bytes
.chunks(size_of::<RawGridCell>())
.map(|data| data.try_into().expect("Invalid grid cell data"))
.map(|data: [u8; size_of::<RawGridCell>()]| RawGridCell::from(data))
.collect();
.map(|data| {
let cell_bytes: [u8; size_of::<RawGridCell>()] = data
.try_into()
.map_err(|_| Error::CriticalError("Invalid bytes for grid cell".to_string()))?;
Ok(RawGridCell::from(cell_bytes))
})
.collect::<Result<Vec<_>>>()?;
with_current_shape_mut!(state, |shape: &mut Shape| {
shape.set_grid_cells(cells.into_iter().map(|raw| raw.into()).collect());

View File

@ -4,6 +4,7 @@ use mem::SerializableResult;
use std::mem::size_of;
use std::sync::{Mutex, OnceLock};
use crate::error::{Error, Result};
use crate::shapes::{Path, Segment, ToPath};
use crate::{mem, with_current_shape, with_current_shape_mut, STATE};
@ -41,12 +42,12 @@ impl From<[u8; size_of::<RawSegmentData>()]> for RawSegmentData {
}
impl TryFrom<&[u8]> for RawSegmentData {
type Error = String;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
type Error = Error;
fn try_from(bytes: &[u8]) -> Result<Self> {
let data: [u8; RAW_SEGMENT_DATA_SIZE] = bytes
.get(0..RAW_SEGMENT_DATA_SIZE)
.and_then(|slice| slice.try_into().ok())
.ok_or("Invalid path data".to_string())?;
.ok_or(Error::CriticalError("Invalid path data".to_string()))?;
Ok(RawSegmentData::from(data))
}
}
@ -154,10 +155,14 @@ fn get_path_upload_buffer() -> &'static Mutex<Vec<u8>> {
}
#[no_mangle]
pub extern "C" fn start_shape_path_buffer() {
#[wasm_error]
pub extern "C" fn start_shape_path_buffer() -> Result<()> {
let buffer = get_path_upload_buffer();
let mut buffer = buffer.lock().unwrap();
let mut buffer = buffer
.lock()
.map_err(|_| Error::CriticalError("Failed to lock path buffer".to_string()))?;
buffer.clear();
Ok(())
}
#[no_mangle]
@ -165,32 +170,40 @@ pub extern "C" fn start_shape_path_buffer() {
pub extern "C" fn set_shape_path_chunk_buffer() -> Result<()> {
let bytes = mem::bytes();
let buffer = get_path_upload_buffer();
let mut buffer = buffer.lock().unwrap();
let mut buffer = buffer
.lock()
.map_err(|_| Error::CriticalError("Failed to lock path buffer".to_string()))?;
buffer.extend_from_slice(&bytes);
mem::free_bytes()?;
Ok(())
}
#[no_mangle]
pub extern "C" fn set_shape_path_buffer() {
#[wasm_error]
pub extern "C" fn set_shape_path_buffer() -> Result<()> {
let buffer = get_path_upload_buffer();
let mut buffer = buffer
.lock()
.map_err(|_| Error::CriticalError("Failed to lock path buffer".to_string()))?;
let chunk_size = size_of::<RawSegmentData>();
if !buffer.len().is_multiple_of(chunk_size) {
// FIXME
println!("Warning: buffer length is not a multiple of chunk size!");
}
let mut segments = Vec::new();
for (i, chunk) in buffer.chunks(chunk_size).enumerate() {
match RawSegmentData::try_from(chunk) {
Ok(seg) => segments.push(Segment::from(seg)),
Err(e) => println!("Error at segment {}: {}", i, e),
}
}
with_current_shape_mut!(state, |shape: &mut Shape| {
let buffer = get_path_upload_buffer();
let mut buffer = buffer.lock().unwrap();
let chunk_size = size_of::<RawSegmentData>();
if !buffer.len().is_multiple_of(chunk_size) {
// FIXME
println!("Warning: buffer length is not a multiple of chunk size!");
}
let mut segments = Vec::new();
for (i, chunk) in buffer.chunks(chunk_size).enumerate() {
match RawSegmentData::try_from(chunk) {
Ok(seg) => segments.push(Segment::from(seg)),
Err(e) => println!("Error at segment {}: {}", i, e),
}
}
shape.set_path_segments(segments);
buffer.clear();
});
buffer.clear();
Ok(())
}
#[no_mangle]

View File

@ -6,6 +6,10 @@ use crate::wasm::blend::RawBlendMode;
use crate::wasm::layouts::constraints::{RawConstraintH, RawConstraintV};
use crate::{with_state_mut, STATE};
#[allow(unused_imports)]
use crate::error::{Error, Result};
use macros::wasm_error;
use super::RawShapeType;
const FLAG_CLIP_CONTENT: u8 = 0b0000_0001;
@ -106,14 +110,18 @@ impl From<[u8; RAW_BASE_PROPS_SIZE]> for RawBasePropsData {
}
#[no_mangle]
pub extern "C" fn set_shape_base_props() {
#[wasm_error]
pub extern "C" fn set_shape_base_props() -> Result<()> {
let bytes = mem::bytes();
if bytes.len() < RAW_BASE_PROPS_SIZE {
return;
return Ok(());
}
let data: [u8; RAW_BASE_PROPS_SIZE] = bytes[..RAW_BASE_PROPS_SIZE].try_into().unwrap();
// FIXME: this should just be a try_from
let data: [u8; RAW_BASE_PROPS_SIZE] = bytes[..RAW_BASE_PROPS_SIZE]
.try_into()
.map_err(|_| Error::CriticalError("Invalid bytes for base props".to_string()))?;
let raw = RawBasePropsData::from(data);
let id = raw.id();
@ -151,6 +159,7 @@ pub extern "C" fn set_shape_base_props() {
shape.set_corners((raw.corner_r1, raw.corner_r2, raw.corner_r3, raw.corner_r4));
}
});
Ok(())
}
#[cfg(test)]

View File

@ -292,9 +292,10 @@ pub extern "C" fn clear_shape_text() {
#[wasm_error]
pub extern "C" fn set_shape_text_content() -> crate::error::Result<()> {
let bytes = mem::bytes();
with_current_shape_mut!(state, |shape: &mut Shape| {
let raw_text_data = RawParagraph::try_from(&bytes).unwrap();
let raw_text_data = RawParagraph::try_from(&bytes)
.map_err(|_| Error::CriticalError("Invalid text data".to_string()))?;
with_current_shape_mut!(state, |shape: &mut Shape| {
shape.add_paragraph(raw_text_data.into()).map_err(|_| {
Error::RecoverableError(format!(
"Error with set_shape_text_content on {:?}",

View File

@ -293,6 +293,135 @@ pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
// TEXT OPERATIONS
// ============================================================================
#[no_mangle]
#[wasm_error]
pub extern "C" fn text_editor_composition_start() -> Result<()> {
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return Ok(());
}
state.text_editor_state.composition.start();
});
Ok(())
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn text_editor_composition_end() -> Result<()> {
let bytes = crate::mem::bytes();
let text = match String::from_utf8(bytes) {
Ok(text) => text,
Err(_) => return Ok(()),
};
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return Ok(());
}
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return Ok(());
};
let Some(shape) = state.shapes.get_mut(&shape_id) else {
return Ok(());
};
let Type::Text(text_content) = &mut shape.shape_type else {
return Ok(());
};
state.text_editor_state.composition.update(&text);
let selection = state
.text_editor_state
.composition
.get_selection(&state.text_editor_state.selection);
text_helpers::delete_selection_range(text_content, &selection);
let cursor = state.text_editor_state.selection.focus;
if let Some(new_cursor) =
text_helpers::insert_text_with_newlines(text_content, &cursor, &text)
{
state.text_editor_state.selection.set_caret(new_cursor);
}
text_content.layout.paragraphs.clear();
text_content.layout.paragraph_builders.clear();
state.text_editor_state.reset_blink();
state
.text_editor_state
.push_event(crate::state::TextEditorEvent::ContentChanged);
state
.text_editor_state
.push_event(crate::state::TextEditorEvent::NeedsLayout);
state.render_state.mark_touched(shape_id);
state.text_editor_state.composition.end();
});
crate::mem::free_bytes()?;
Ok(())
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn text_editor_composition_update() -> Result<()> {
let bytes = crate::mem::bytes();
let text = match String::from_utf8(bytes) {
Ok(text) => text,
Err(_) => return Ok(()),
};
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return Ok(());
}
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return Ok(());
};
let Some(shape) = state.shapes.get_mut(&shape_id) else {
return Ok(());
};
let Type::Text(text_content) = &mut shape.shape_type else {
return Ok(());
};
state.text_editor_state.composition.update(&text);
let selection = state
.text_editor_state
.composition
.get_selection(&state.text_editor_state.selection);
text_helpers::delete_selection_range(text_content, &selection);
let cursor = state.text_editor_state.selection.focus;
text_helpers::insert_text_with_newlines(text_content, &cursor, &text);
text_content.layout.paragraphs.clear();
text_content.layout.paragraph_builders.clear();
state.text_editor_state.reset_blink();
state
.text_editor_state
.push_event(crate::state::TextEditorEvent::ContentChanged);
state
.text_editor_state
.push_event(crate::state::TextEditorEvent::NeedsLayout);
state.render_state.mark_touched(shape_id);
});
crate::mem::free_bytes()?;
Ok(())
}
// FIXME: Review if all the return Ok(()) should be Err instead.
#[no_mangle]
#[wasm_error]

View File

@ -1,4 +1,6 @@
use macros::ToJs;
#[allow(unused_imports)]
use crate::error::{Error, Result};
use macros::{wasm_error, ToJs};
use skia_safe as skia;
@ -39,11 +41,11 @@ impl From<[u8; RAW_TRANSFORM_ENTRY_SIZE]> for RawTransformEntry {
}
impl TryFrom<&[u8]> for RawTransformEntry {
type Error = String;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
type Error = Error;
fn try_from(bytes: &[u8]) -> Result<Self> {
let bytes: [u8; RAW_TRANSFORM_ENTRY_SIZE] = bytes
.try_into()
.map_err(|_| "Invalid transform entry bytes".to_string())?;
.map_err(|_| Error::CriticalError("Invalid transform entry bytes".to_string()))?;
Ok(RawTransformEntry::from(bytes))
}
}
@ -73,16 +75,17 @@ impl From<RawTransformEntry> for TransformEntry {
}
#[no_mangle]
pub extern "C" fn propagate_modifiers(pixel_precision: bool) -> *mut u8 {
#[wasm_error]
pub extern "C" fn propagate_modifiers(pixel_precision: bool) -> Result<*mut u8> {
let bytes = mem::bytes();
let entries: Vec<TransformEntry> = bytes
.chunks(RAW_TRANSFORM_ENTRY_SIZE)
.map(|data| RawTransformEntry::try_from(data).unwrap().into())
.collect();
.map(|data| RawTransformEntry::try_from(data).map(|entry| entry.into()))
.collect::<Result<Vec<_>>>()?;
with_state!(state, {
let result = shapes::propagate_modifiers(state, &entries, pixel_precision);
mem::write_vec(result)
let result = shapes::propagate_modifiers(state, &entries, pixel_precision)?;
Ok(mem::write_vec(result))
})
}