diff --git a/CHANGES.md b/CHANGES.md index 702384e36e..86c0362128 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) diff --git a/backend/src/app/rpc/commands/access_token.clj b/backend/src/app/rpc/commands/access_token.clj index 393f824599..eedb119d06 100644 --- a/backend/src/app/rpc/commands/access_token.clj +++ b/backend/src/app/rpc/commands/access_token.clj @@ -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))) diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index 91f1558531..d2ec5636fd 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -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)) diff --git a/frontend/playwright/data/render-wasm/get-file-background-blur.json b/frontend/playwright/data/render-wasm/get-file-background-blur.json new file mode 100644 index 0000000000..34bbba7ad8 --- /dev/null +++ b/frontend/playwright/data/render-wasm/get-file-background-blur.json @@ -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" + } + } +} \ No newline at end of file diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index b74341c965..270678de25 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -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) { diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js index 4d0597b9aa..21fb267806 100644 --- a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js +++ b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js @@ -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(); }); \ No newline at end of file diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-background-blur-on-shapes-overlapping-other-shapes-1.png b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-background-blur-on-shapes-overlapping-other-shapes-1.png new file mode 100644 index 0000000000..69f126eeaf Binary files /dev/null and b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-background-blur-on-shapes-overlapping-other-shapes-1.png differ diff --git a/frontend/playwright/ui/specs/design-tab.spec.js b/frontend/playwright/ui/specs/design-tab.spec.js index 489579b844..8fe67b9d3a 100644 --- a/frontend/playwright/ui/specs/design-tab.spec.js +++ b/frontend/playwright/ui/specs/design-tab.spec.js @@ -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, }) => { diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 7bf4afecc7..68dfb71e51 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -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] diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index 279666db33..8c1b8021a0 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -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 diff --git a/frontend/src/app/main/data/workspace/mcp.cljs b/frontend/src/app/main/data/workspace/mcp.cljs index 63e7de7516..a9198750c7 100644 --- a/frontend/src/app/main/data/workspace/mcp.cljs +++ b/frontend/src/app/main/data/workspace/mcp.cljs @@ -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 diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index 9bfc7ac8a2..743fecd850 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -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}] diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 989e85eb38..022dd11d21 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -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 diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index b1903ec5c5..6dc5618f66 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -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?) diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index 686cd866d0..3ee7758284 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -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}]}} diff --git a/frontend/src/app/main/data/workspace/wasm_text.cljs b/frontend/src/app/main/data/workspace/wasm_text.cljs index c43cee567f..c175b46bdf 100644 --- a/frontend/src/app/main/data/workspace/wasm_text.cljs +++ b/frontend/src/app/main/data/workspace/wasm_text.cljs @@ -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." diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs index 9fe9ff3fbf..d536429f50 100644 --- a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs +++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs @@ -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) diff --git a/frontend/src/app/main/ui/inspect/attributes/blur.cljs b/frontend/src/app/main/ui/inspect/attributes/blur.cljs index 21a21a6b4b..553178483d 100644 --- a/frontend/src/app/main/ui/inspect/attributes/blur.cljs +++ b/frontend/src/app/main/ui/inspect/attributes/blur.cljs @@ -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)]]]]))]]))) diff --git a/frontend/src/app/main/ui/settings/integrations.cljs b/frontend/src/app/main/ui/settings/integrations.cljs index f4b820e0b3..a54b43d2b9 100644 --- a/frontend/src/app/main/ui/settings/integrations.cljs +++ b/frontend/src/app/main/ui/settings/integrations.cljs @@ -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)) diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index 9dcbd0672e..6e0adbebea 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index aa320694c7..223e30ce50 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -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)}]])) diff --git a/frontend/src/app/main/ui/workspace/main_menu.scss b/frontend/src/app/main/ui/workspace/main_menu.scss index d69b09e61a..1b12e2cdbf 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.scss +++ b/frontend/src/app/main/ui/workspace/main_menu.scss @@ -134,6 +134,10 @@ &.active { --menu-indicator-color: var(--color-accent-primary); } + + &.failed { + --menu-indicator-color: var(--color-foreground-error); + } } .item-arrow { diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs index 576854c163..bfc2e07947 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs index 52a5570614..f1beba6eb0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs @@ -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") diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss index e15a50c83e..c80e57e1ec 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss @@ -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; diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs index 621f8f2f4e..df3c5070fe 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs @@ -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 diff --git a/frontend/src/app/plugins/api.cljs b/frontend/src/app/plugins/api.cljs index 4c982db774..5af6cb05be 100644 --- a/frontend/src/app/plugins/api.cljs +++ b/frontend/src/app/plugins/api.cljs @@ -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") diff --git a/frontend/src/app/plugins/comments.cljs b/frontend/src/app/plugins/comments.cljs index f3cfdcf954..236074142d 100644 --- a/frontend/src/app/plugins/comments.cljs +++ b/frontend/src/app/plugins/comments.cljs @@ -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. diff --git a/frontend/src/app/plugins/file.cljs b/frontend/src/app/plugins/file.cljs index 54fcb2f8c3..15c0bf7188 100644 --- a/frontend/src/app/plugins/file.cljs +++ b/frontend/src/app/plugins/file.cljs @@ -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)] diff --git a/frontend/src/app/plugins/flags.cljs b/frontend/src/app/plugins/flags.cljs index a9f1a6dce7..c28623ac62 100644 --- a/frontend/src/app/plugins/flags.cljs +++ b/frontend/src/app/plugins/flags.cljs @@ -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))))})) diff --git a/frontend/src/app/plugins/flex.cljs b/frontend/src/app/plugins/flex.cljs index a1c7ef754c..ff6de68488 100644 --- a/frontend/src/app/plugins/flex.cljs +++ b/frontend/src/app/plugins/flex.cljs @@ -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}))))})) diff --git a/frontend/src/app/plugins/fonts.cljs b/frontend/src/app/plugins/fonts.cljs index 77602816f6..13996fabc2 100644 --- a/frontend/src/app/plugins/fonts.cljs +++ b/frontend/src/app/plugins/fonts.cljs @@ -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)))))) diff --git a/frontend/src/app/plugins/grid.cljs b/frontend/src/app/plugins/grid.cljs index f57873ec31..351b673baf 100644 --- a/frontend/src/app/plugins/grid.cljs +++ b/frontend/src/app/plugins/grid.cljs @@ -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})))))}))) diff --git a/frontend/src/app/plugins/history.cljs b/frontend/src/app/plugins/history.cljs index 191dcc0d7e..25756c47da 100644 --- a/frontend/src/app/plugins/history.cljs +++ b/frontend/src/app/plugins/history.cljs @@ -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)))))) diff --git a/frontend/src/app/plugins/library.cljs b/frontend/src/app/plugins/library.cljs index c3dcd9ef28..1022a1a65c 100644 --- a/frontend/src/app/plugins/library.cljs +++ b/frontend/src/app/plugins/library.cljs @@ -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 diff --git a/frontend/src/app/plugins/local_storage.cljs b/frontend/src/app/plugins/local_storage.cljs index cac6529be1..80d9b66b6b 100644 --- a/frontend/src/app/plugins/local_storage.cljs +++ b/frontend/src/app/plugins/local_storage.cljs @@ -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)))) diff --git a/frontend/src/app/plugins/page.cljs b/frontend/src/app/plugins/page.cljs index c7a946d348..7bc5726a17 100644 --- a/frontend/src/app/plugins/page.cljs +++ b/frontend/src/app/plugins/page.cljs @@ -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 diff --git a/frontend/src/app/plugins/public_utils.cljs b/frontend/src/app/plugins/public_utils.cljs index d3ed6a46e2..0bfe911ed5 100644 --- a/frontend/src/app/plugins/public_utils.cljs +++ b/frontend/src/app/plugins/public_utils.cljs @@ -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))] diff --git a/frontend/src/app/plugins/ruler_guides.cljs b/frontend/src/app/plugins/ruler_guides.cljs index c42e148e56..9cf7535660 100644 --- a/frontend/src/app/plugins/ruler_guides.cljs +++ b/frontend/src/app/plugins/ruler_guides.cljs @@ -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) diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index 29e560bf35..6808ba8635 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -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) diff --git a/frontend/src/app/plugins/text.cljs b/frontend/src/app/plugins/text.cljs index 36828324b2..08dc64db12 100644 --- a/frontend/src/app/plugins/text.cljs +++ b/frontend/src/app/plugins/text.cljs @@ -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})))))} diff --git a/frontend/src/app/plugins/tokens.cljs b/frontend/src/app/plugins/tokens.cljs index 268652e334..ec1ebf6b71 100644 --- a/frontend/src/app/plugins/tokens.cljs +++ b/frontend/src/app/plugins/tokens.cljs @@ -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)} diff --git a/frontend/src/app/plugins/utils.cljs b/frontend/src/app/plugins/utils.cljs index 0e94c1543d..cbfc512323 100644 --- a/frontend/src/app/plugins/utils.cljs +++ b/frontend/src/app/plugins/utils.cljs @@ -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] diff --git a/frontend/src/app/plugins/viewport.cljs b/frontend/src/app/plugins/viewport.cljs index a581e3ba60..8b5b73ac57 100644 --- a/frontend/src/app/plugins/viewport.cljs +++ b/frontend/src/app/plugins/viewport.cljs @@ -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 diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index e4d097cc06..fb80c7be45 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -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 diff --git a/frontend/src/app/render_wasm/text_editor.cljs b/frontend/src/app/render_wasm/text_editor.cljs index e8e540b8f1..70c225ced2 100644 --- a/frontend/src/app/render_wasm/text_editor.cljs +++ b/frontend/src/app/render_wasm/text_editor.cljs @@ -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 ([] diff --git a/frontend/src/app/util/code_gen/style_css.cljs b/frontend/src/app/util/code_gen/style_css.cljs index e72fc9a9ee..8354a73682 100644 --- a/frontend/src/app/util/code_gen/style_css.cljs +++ b/frontend/src/app/util/code_gen/style_css.cljs @@ -79,6 +79,7 @@ body { :border-end-end-radius :box-shadow :filter + :backdrop-filter :opacity :overflow :blend-mode diff --git a/frontend/src/app/util/code_gen/style_css_formats.cljs b/frontend/src/app/util/code_gen/style_css_formats.cljs index 74c074d2bc..547e022dc6 100644 --- a/frontend/src/app/util/code_gen/style_css_formats.cljs +++ b/frontend/src/app/util/code_gen/style_css_formats.cljs @@ -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 diff --git a/frontend/src/app/util/code_gen/style_css_values.cljs b/frontend/src/app/util/code_gen/style_css_values.cljs index cba1991599..83b6bd7a98 100644 --- a/frontend/src/app/util/code_gen/style_css_values.cljs +++ b/frontend/src/app/util/code_gen/style_css_values.cljs @@ -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 diff --git a/frontend/test/frontend_tests/logic/components_and_tokens.cljs b/frontend/test/frontend_tests/logic/components_and_tokens.cljs index 689ac76b63..9e830c9a76 100644 --- a/frontend/test/frontend_tests/logic/components_and_tokens.cljs +++ b/frontend/test/frontend_tests/logic/components_and_tokens.cljs @@ -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))]] diff --git a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs index cb6f2e39d8..e468e420bb 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs @@ -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 diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 4a7eb227f9..fdcb1cac5f 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -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" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 50678fd0c8..5f630941fe 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -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" diff --git a/mcp/packages/plugin/src/main.ts b/mcp/packages/plugin/src/main.ts index 1d6c7edf3b..da87fa025f 100644 --- a/mcp/packages/plugin/src/main.ts +++ b/mcp/packages/plugin/src/main.ts @@ -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); }; diff --git a/mcp/packages/server/data/api_types.yml b/mcp/packages/server/data/api_types.yml index 943cc62021..901037c249 100644 --- a/mcp/packages/server/data/api_types.yml +++ b/mcp/packages/server/data/api_types.yml @@ -84,6 +84,7 @@ Penpot: distributeHorizontal(shapes: Shape[]): void; distributeVertical(shapes: Shape[]): void; flatten(shapes: Shape[]): Path[]; + createVariantFromComponents(shapes: Board[]): VariantContainer; } ``` @@ -823,6 +824,24 @@ Penpot: to flatten Returns Path[] + createVariantFromComponents: |- + ``` + createVariantFromComponents(shapes: Board[]): VariantContainer + ``` + + Combine several standard Components into a VariantComponent. Similar to doing it + with the contextual menu on the Penpot interface. + All the shapes passed as arguments should be main instances. + + Parameters + + * shapes: Board[] + + A list of main instances of the components to combine. + + Returns VariantContainer + + The variant container created ActiveUser: overview: |- Interface ActiveUser @@ -1071,10 +1090,10 @@ Board: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -1090,14 +1109,14 @@ Board: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -1114,7 +1133,7 @@ Board: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -1193,12 +1212,20 @@ Board: ``` The horizontal sizing behavior of the board. + It can be one of the following values: + + * 'fix': The containers has its own intrinsic fixed size. + * 'auto': The container fits the content. verticalSizing: |- ``` verticalSizing?: "auto" | "fix" ``` The vertical sizing behavior of the board. + It can be one of the following values: + + * 'fix': The containers has its own intrinsic fixed size. + * 'auto': The container fits the content. fills: |- ``` fills: Fill[] @@ -1469,10 +1496,10 @@ Board: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -1488,14 +1515,14 @@ Board: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -1881,7 +1908,7 @@ Board: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -1894,7 +1921,9 @@ Board: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -2180,10 +2209,10 @@ VariantContainer: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -2199,14 +2228,14 @@ VariantContainer: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -2223,7 +2252,7 @@ VariantContainer: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -2250,7 +2279,7 @@ VariantContainer: * Board + VariantContainer - Referenced by: ContextTypesUtils + Referenced by: Board, Boolean, Context, ContextTypesUtils, Ellipse, Group, Image, Path, Penpot, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer members: Properties: type: |- @@ -2301,12 +2330,20 @@ VariantContainer: ``` The horizontal sizing behavior of the board. + It can be one of the following values: + + * 'fix': The containers has its own intrinsic fixed size. + * 'auto': The container fits the content. verticalSizing: |- ``` verticalSizing?: "auto" | "fix" ``` The vertical sizing behavior of the board. + It can be one of the following values: + + * 'fix': The containers has its own intrinsic fixed size. + * 'auto': The container fits the content. fills: |- ``` fills: Fill[] @@ -2581,10 +2618,10 @@ VariantContainer: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -2600,14 +2637,14 @@ VariantContainer: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -2993,7 +3030,7 @@ VariantContainer: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -3006,7 +3043,9 @@ VariantContainer: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -3279,10 +3318,10 @@ Boolean: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -3298,14 +3337,14 @@ Boolean: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -3322,7 +3361,7 @@ Boolean: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -3642,10 +3681,10 @@ Boolean: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -3661,14 +3700,14 @@ Boolean: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -4005,7 +4044,7 @@ Boolean: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -4018,7 +4057,9 @@ Boolean: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -4575,8 +4616,8 @@ CommonLayout: leftPadding: number; horizontalSizing: "fill" | "auto" - | "fit-content"; - verticalSizing: "fill" | "auto" | "fit-content"; + | "fix"; + verticalSizing: "fill" | "auto" | "fix"; remove(): void; } ``` @@ -4706,26 +4747,26 @@ CommonLayout: The `leftPadding` property specifies the padding at the left of the container. horizontalSizing: |- ``` - horizontalSizing: "fill" | "auto" | "fit-content" + horizontalSizing: "fill" | "auto" | "fix" ``` The `horizontalSizing` property specifies the horizontal sizing behavior of the container. It can be one of the following values: - * 'fit-content': The container fits the content. - * 'fill': The container fills the available space. - * 'auto': The container size is determined automatically. + * 'fix': The containers has its own intrinsic fixed size. + * 'fill': The container fills the available space. Only can be set if it's inside another layout. + * 'auto': The container fits the content. verticalSizing: |- ``` - verticalSizing: "fill" | "auto" | "fit-content" + verticalSizing: "fill" | "auto" | "fix" ``` The `verticalSizing` property specifies the vertical sizing behavior of the container. It can be one of the following values: - * 'fit-content': The container fits the content. - * 'fill': The container fills the available space. - * 'auto': The container size is determined automatically. + * 'fix': The containers has its own intrinsic fixed size. + * 'fill': The container fills the available space. Only can be set if it's inside another layout. + * 'auto': The container fits the content. Methods: remove: |- ``` @@ -4808,6 +4849,7 @@ Context: distributeHorizontal(shapes: Shape[]): void; distributeVertical(shapes: Shape[]): void; flatten(shapes: Shape[]): Path[]; + createVariantFromComponents(shapes: Board[]): VariantContainer; } ``` members: @@ -5449,6 +5491,24 @@ Context: to flatten Returns Path[] + createVariantFromComponents: |- + ``` + createVariantFromComponents(shapes: Board[]): VariantContainer + ``` + + Combine several standard Components into a VariantComponent. Similar to doing it + with the contextual menu on the Penpot interface. + All the shapes passed as arguments should be main instances. + + Parameters + + * shapes: Board[] + + A list of main instances of the components to combine. + + Returns VariantContainer + + The variant container created ContextGeometryUtils: overview: |- Interface ContextGeometryUtils @@ -5859,10 +5919,10 @@ Ellipse: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -5878,14 +5938,14 @@ Ellipse: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -5902,7 +5962,7 @@ Ellipse: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -6192,10 +6252,10 @@ Ellipse: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -6211,14 +6271,14 @@ Ellipse: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -6500,7 +6560,7 @@ Ellipse: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -6513,7 +6573,9 @@ Ellipse: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -7244,8 +7306,8 @@ FlexLayout: leftPadding: number; horizontalSizing: "fill" | "auto" - | "fit-content"; - verticalSizing: "fill" | "auto" | "fit-content"; + | "fix"; + verticalSizing: "fill" | "auto" | "fix"; remove(): void; dir: "row" | "row-reverse" | "column" | "column-reverse"; wrap?: "wrap" | "nowrap"; @@ -7379,26 +7441,26 @@ FlexLayout: The `leftPadding` property specifies the padding at the left of the container. horizontalSizing: |- ``` - horizontalSizing: "fill" | "auto" | "fit-content" + horizontalSizing: "fill" | "auto" | "fix" ``` The `horizontalSizing` property specifies the horizontal sizing behavior of the container. It can be one of the following values: - * 'fit-content': The container fits the content. - * 'fill': The container fills the available space. - * 'auto': The container size is determined automatically. + * 'fix': The containers has its own intrinsic fixed size. + * 'fill': The container fills the available space. Only can be set if it's inside another layout. + * 'auto': The container fits the content. verticalSizing: |- ``` - verticalSizing: "fill" | "auto" | "fit-content" + verticalSizing: "fill" | "auto" | "fix" ``` The `verticalSizing` property specifies the vertical sizing behavior of the container. It can be one of the following values: - * 'fit-content': The container fits the content. - * 'fill': The container fills the available space. - * 'auto': The container size is determined automatically. + * 'fix': The containers has its own intrinsic fixed size. + * 'fill': The container fills the available space. Only can be set if it's inside another layout. + * 'auto': The container fits the content. dir: |- ``` dir: "row" | "row-reverse" | "column" | "column-reverse" @@ -7802,8 +7864,8 @@ GridLayout: leftPadding: number; horizontalSizing: "fill" | "auto" - | "fit-content"; - verticalSizing: "fill" | "auto" | "fit-content"; + | "fix"; + verticalSizing: "fill" | "auto" | "fix"; remove(): void; dir: "row" | "column"; rows: Track[]; @@ -7946,26 +8008,26 @@ GridLayout: The `leftPadding` property specifies the padding at the left of the container. horizontalSizing: |- ``` - horizontalSizing: "fill" | "auto" | "fit-content" + horizontalSizing: "fill" | "auto" | "fix" ``` The `horizontalSizing` property specifies the horizontal sizing behavior of the container. It can be one of the following values: - * 'fit-content': The container fits the content. - * 'fill': The container fills the available space. - * 'auto': The container size is determined automatically. + * 'fix': The containers has its own intrinsic fixed size. + * 'fill': The container fills the available space. Only can be set if it's inside another layout. + * 'auto': The container fits the content. verticalSizing: |- ``` - verticalSizing: "fill" | "auto" | "fit-content" + verticalSizing: "fill" | "auto" | "fix" ``` The `verticalSizing` property specifies the vertical sizing behavior of the container. It can be one of the following values: - * 'fit-content': The container fits the content. - * 'fill': The container fills the available space. - * 'auto': The container size is determined automatically. + * 'fix': The containers has its own intrinsic fixed size. + * 'fill': The container fills the available space. Only can be set if it's inside another layout. + * 'auto': The container fits the content. dir: |- ``` dir: "row" | "column" @@ -8288,10 +8350,10 @@ Group: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -8307,14 +8369,14 @@ Group: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -8331,7 +8393,7 @@ Group: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -8627,10 +8689,10 @@ Group: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -8646,14 +8708,14 @@ Group: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -9001,7 +9063,7 @@ Group: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -9014,7 +9076,9 @@ Group: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -9532,10 +9596,10 @@ Image: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -9551,14 +9615,14 @@ Image: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -9575,7 +9639,7 @@ Image: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -9865,10 +9929,10 @@ Image: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -9884,14 +9948,14 @@ Image: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -10173,7 +10237,7 @@ Image: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -10186,7 +10250,9 @@ Image: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -12997,10 +13063,10 @@ Path: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -13016,14 +13082,14 @@ Path: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -13040,7 +13106,7 @@ Path: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -13354,10 +13420,10 @@ Path: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -13373,14 +13439,14 @@ Path: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -13676,7 +13742,7 @@ Path: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -13689,7 +13755,9 @@ Path: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -14324,10 +14392,10 @@ Rectangle: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -14343,14 +14411,14 @@ Rectangle: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -14367,7 +14435,7 @@ Rectangle: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -14659,10 +14727,10 @@ Rectangle: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -14678,14 +14746,14 @@ Rectangle: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -14967,7 +15035,7 @@ Rectangle: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -14980,7 +15048,9 @@ Rectangle: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -15360,10 +15430,10 @@ ShapeBase: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -15379,14 +15449,14 @@ ShapeBase: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -15403,7 +15473,7 @@ ShapeBase: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -15694,10 +15764,10 @@ ShapeBase: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -15713,14 +15783,14 @@ ShapeBase: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -16002,7 +16072,7 @@ ShapeBase: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -16015,7 +16085,9 @@ ShapeBase: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -16426,10 +16498,10 @@ SvgRaw: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -16445,14 +16517,14 @@ SvgRaw: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -16469,7 +16541,7 @@ SvgRaw: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -16754,10 +16826,10 @@ SvgRaw: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -16773,14 +16845,14 @@ SvgRaw: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -17066,7 +17138,7 @@ SvgRaw: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -17079,7 +17151,9 @@ SvgRaw: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -17345,10 +17419,10 @@ Text: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -17364,14 +17438,14 @@ Text: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -17388,7 +17462,7 @@ Text: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -17691,10 +17765,10 @@ Text: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -17710,14 +17784,14 @@ Text: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -18107,7 +18181,7 @@ Text: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -18120,7 +18194,9 @@ Text: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -22618,7 +22694,11 @@ TokenBorderRadiusProps: ================================= ``` - TokenBorderRadiusProps: "r1" | "r2" | "r3" | "r4" + TokenBorderRadiusProps: + | "borderRadiusTopLeft" + | "borderRadiusTopRight" + | "borderRadiusBottomRight" + | "borderRadiusBottomLeft" ``` The properties that a BorderRadius token can be applied to. @@ -22770,14 +22850,14 @@ TokenSpacingProps: TokenSpacingProps: | "rowGap" | "columnGap" - | "p1" - | "p2" - | "p3" - | "p4" - | "m1" - | "m2" - | "m3" - | "m4" + | "paddingLeft" + | "paddingTop" + | "paddingRight" + | "paddingBottom" + | "marginLeft" + | "marginTop" + | "marginRight" + | "marginBottom" ``` The properties that a Spacing token can be applied to. diff --git a/mcp/packages/server/data/initial_instructions.md b/mcp/packages/server/data/initial_instructions.md index c212bb408b..c85441a977 100644 --- a/mcp/packages/server/data/initial_instructions.md +++ b/mcp/packages/server/data/initial_instructions.md @@ -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" diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index a5d5faff84..203e5e0e81 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -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) diff --git a/plugins/apps/poc-tokens-plugin/src/app/app.component.ts b/plugins/apps/poc-tokens-plugin/src/app/app.component.ts index 0b5584fbe8..8b60e40d8d 100644 --- a/plugins/apps/poc-tokens-plugin/src/app/app.component.ts +++ b/plugins/apps/poc-tokens-plugin/src/app/app.component.ts @@ -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) } } diff --git a/plugins/libs/plugin-types/index.d.ts b/plugins/libs/plugin-types/index.d.ts index 381de47fc7..1ebface05c 100644 --- a/plugins/libs/plugin-types/index.d.ts +++ b/plugins/libs/plugin-types/index.d.ts @@ -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. diff --git a/plugins/libs/plugins-runtime/src/lib/api/index.ts b/plugins/libs/plugins-runtime/src/lib/api/index.ts index 2f8e9e58d9..00d64e58ed 100644 --- a/plugins/libs/plugins-runtime/src/lib/api/index.ts +++ b/plugins/libs/plugins-runtime/src/lib/api/index.ts @@ -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; diff --git a/render-wasm/macros/src/lib.rs b/render-wasm/macros/src/lib.rs index 2d3536d3d1..876af71254 100644 --- a/render-wasm/macros/src/lib.rs +++ b/render-wasm/macros/src/lib.rs @@ -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); } } diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 6e519cc249..0366009ca7 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -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> = 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::() { - 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 = bytes .chunks(size_of::<::BytesType>()) - .map(|data| Uuid::try_from(data).unwrap()) - .collect(); + .map(|data| Uuid::try_from(data).map_err(|e| Error::RecoverableError(e.to_string()))) + .collect::>>()?; with_state_mut!(state, { state.set_focus_mode(entries); @@ -637,8 +617,8 @@ pub extern "C" fn set_children() -> Result<()> { let entries: Vec = bytes .chunks(size_of::<::BytesType>()) - .map(|data| Uuid::try_from(data).unwrap()) - .collect(); + .map(|data| Uuid::try_from(data).map_err(|e| Error::CriticalError(e.to_string()))) + .collect::>>()?; 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 = 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::>>()?; 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 = bytes .chunks(size_of::<::BytesType>()) - .map(|data| TransformEntry::try_from(data).unwrap()) - .collect(); + .map(|data| TransformEntry::try_from(data).map_err(|e| Error::CriticalError(e.to_string()))) + .collect::>>()?; let mut modifiers = HashMap::new(); let mut ids = Vec::::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(()) diff --git a/render-wasm/src/options.rs b/render-wasm/src/options.rs deleted file mode 100644 index beeeec5a30..0000000000 --- a/render-wasm/src/options.rs +++ /dev/null @@ -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; diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 271a7bc38d..ed117bcb60 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -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, 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 { // 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(&mut self, f: F) -> R + fn with_nested_blurs_suppressed(&mut self, f: F) -> Result where - F: FnOnce(&mut RenderState) -> R, + F: FnOnce(&mut RenderState) -> Result, { 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>, outset: Option, - ) { + ) -> 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 = 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, - ) { + ) -> 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 { + 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 { + 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, - ) { + ) -> Result<()> { let mut transformed_shadow: Cow = 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::::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::::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) { + pub fn rebuild_modifier_tiles( + &mut self, + tree: ShapesPoolMutRef<'_>, + ids: Vec, + ) -> 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); } diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index 41f68a663e..624cdaef78 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -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;')")) diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs index 6e098c9752..2a1968a93c 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -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, -) { +) -> 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, inset: Option, -) { +) -> 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)] diff --git a/render-wasm/src/render/filters.rs b/render-wasm/src/render/filters.rs index 149c598e94..34b33f403d 100644 --- a/render-wasm/src/render/filters.rs +++ b/render-wasm/src/render/filters.rs @@ -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( bounds: Rect, target_surface: SurfaceId, draw_fn: F, -) -> bool +) -> Result 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( bounds: Rect, extra_downscale: f32, draw_fn: F, -) -> Option<(skia::Surface, f32)> +) -> Result> 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))) } diff --git a/render-wasm/src/render/fonts.rs b/render-wasm/src/render/fonts.rs index 917fe134a8..d528d7b691 100644 --- a/render-wasm/src/render/fonts.rs +++ b/render-wasm/src/render/fonts.rs @@ -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 { 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 { diff --git a/render-wasm/src/render/gpu_state.rs b/render-wasm/src/render/gpu_state.rs index d4d90faaec..910c5beda2 100644 --- a/render-wasm/src/render/gpu_state.rs +++ b/render-wasm/src/render/gpu_state.rs @@ -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 { + 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 { 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 { 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 { 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) } } diff --git a/render-wasm/src/render/images.rs b/render-wasm/src/render/images.rs index 4faf393895..51bf9dbbe0 100644 --- a/render-wasm/src/render/images.rs +++ b/render-wasm/src/render/images.rs @@ -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 { +) -> Result { 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 diff --git a/render-wasm/src/render/options.rs b/render-wasm/src/render/options.rs index d6f1d1396f..9505d0b254 100644 --- a/render-wasm/src/render/options.rs +++ b/render-wasm/src/render/options.rs @@ -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, + 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 } } diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index ea43322b70..d392305327 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -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, shadows: &[Paint], blur_filter: &Option, -) { +) -> 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(()) } diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 319b921ac5..c5a3a26bf1 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -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, antialias: bool, outset: Option, -) { +) -> 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, -) { +) -> 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, -) { +) -> 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, -) { +) -> 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) diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 11239c2007..7337409923 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -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 { 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 { 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 { + pub fn base64_snapshot_rect( + &mut self, + id: SurfaceId, + irect: skia::IRect, + ) -> Result> { 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(); } diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index 832503505d..85a150284b 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -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 = (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, layer_opacity: Option, -) { +) -> 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, layer_opacity: Option, -) { +) -> 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( diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 390391e11b..dedf9ab770 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -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, }) } diff --git a/render-wasm/src/shapes/blurs.rs b/render-wasm/src/shapes/blurs.rs index 543e11efa8..828dcb10d3 100644 --- a/render-wasm/src/shapes/blurs.rs +++ b/render-wasm/src/shapes/blurs.rs @@ -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)] diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 9713c066a9..4c7f6d69d4 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -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, -) -> VecDeque { +) -> Result> { 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, bounds: &mut HashMap, modifiers: &mut HashMap, -) { +) -> 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, entries: &mut VecDeque, bounds: &mut HashMap, -) { +) -> 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 { +) -> Result> { 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); } diff --git a/render-wasm/src/shapes/modifiers/constraints.rs b/render-wasm/src/shapes/modifiers/constraints.rs index 190fd32734..4f5a9cb228 100644 --- a/render-wasm/src/shapes/modifiers/constraints.rs +++ b/render-wasm/src/shapes/modifiers/constraints.rs @@ -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 { // 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) } diff --git a/render-wasm/src/shapes/modifiers/flex_layout.rs b/render-wasm/src/shapes/modifiers/flex_layout.rs index 72191a32a2..7661d93088 100644 --- a/render-wasm/src/shapes/modifiers/flex_layout.rs +++ b/render-wasm/src/shapes/modifiers/flex_layout.rs @@ -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, -) -> VecDeque { +) -> Result> { 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) } diff --git a/render-wasm/src/shapes/modifiers/grid_layout.rs b/render-wasm/src/shapes/modifiers/grid_layout.rs index 7b2a314989..7ef2cb447b 100644 --- a/render-wasm/src/shapes/modifiers/grid_layout.rs +++ b/render-wasm/src/shapes/modifiers/grid_layout.rs @@ -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, -) -> VecDeque { +) -> Result> { let mut result = VecDeque::new(); let layout_bounds = bounds.find(shape); let children: HashSet = 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) } diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index f9619eaf74..9ef4da3d07 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -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 { diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index b99b768334..a976ef331f 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -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 { + 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 { 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.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) { + pub fn rebuild_modifier_tiles(&mut self, ids: Vec) -> 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 { diff --git a/render-wasm/src/state/text_editor.rs b/render-wasm/src/state/text_editor.rs index 65ffbbc19a..1c40c25053 100644 --- a/render-wasm/src/state/text_editor.rs +++ b/render-wasm/src/state/text_editor.rs @@ -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, diff --git a/render-wasm/src/tiles.rs b/render-wasm/src/tiles.rs index 02bd5c5eb5..13ed4c1aeb 100644 --- a/render-wasm/src/tiles.rs +++ b/render-wasm/src/tiles.rs @@ -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); } diff --git a/render-wasm/src/wasm/blurs.rs b/render-wasm/src/wasm/blurs.rs index 700ac053bc..e8e1a4242c 100644 --- a/render-wasm/src/wasm/blurs.rs +++ b/render-wasm/src/wasm/blurs.rs @@ -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 for RawBlurType { @@ -20,6 +21,7 @@ impl From for BlurType { fn from(value: RawBlurType) -> Self { match value { RawBlurType::LayerBlur => BlurType::LayerBlur, + RawBlurType::BackgroundBlur => BlurType::BackgroundBlur, } } } diff --git a/render-wasm/src/wasm/fills/image.rs b/render-wasm/src/wasm/fills/image.rs index f0e5b36526..b122de4cc8 100644 --- a/render-wasm/src/wasm/fills/image.rs +++ b/render-wasm/src/wasm/fills/image.rs @@ -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> for ShapeImageIds { - type Error = &'static str; + type Error = Error; - fn try_from(value: Vec) -> Result { + fn try_from(value: Vec) -> Result { let mut arr = [0u8; IMAGE_IDS_SIZE]; arr.copy_from_slice(&value); Ok(ShapeImageIds::from(arr)) @@ -68,13 +68,16 @@ impl TryFrom> 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); diff --git a/render-wasm/src/wasm/layouts/grid.rs b/render-wasm/src/wasm/layouts/grid.rs index d1a0476814..aac2fd1928 100644 --- a/render-wasm/src/wasm/layouts/grid.rs +++ b/render-wasm/src/wasm/layouts/grid.rs @@ -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 = bytes .chunks(size_of::()) - .map(|data| data.try_into().unwrap()) - .map(|data: [u8; size_of::()]| RawGridTrack::from(data).into()) - .collect(); + .map(|data| { + let track_bytes: [u8; size_of::()] = data + .try_into() + .map_err(|_| Error::CriticalError("Invalid bytes for grid track".to_string()))?; + Ok(RawGridTrack::from(track_bytes).into()) + }) + .collect::>>()?; 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 = bytes .chunks(size_of::()) - .map(|data| data.try_into().unwrap()) - .map(|data: [u8; size_of::()]| RawGridTrack::from(data).into()) - .collect(); + .map(|data| { + let track_bytes: [u8; size_of::()] = data + .try_into() + .map_err(|_| Error::CriticalError("Invalid bytes for grid track".to_string()))?; + Ok(RawGridTrack::from(track_bytes).into()) + }) + .collect::>>()?; 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 = bytes .chunks(size_of::()) - .map(|data| data.try_into().expect("Invalid grid cell data")) - .map(|data: [u8; size_of::()]| RawGridCell::from(data)) - .collect(); + .map(|data| { + let cell_bytes: [u8; size_of::()] = data + .try_into() + .map_err(|_| Error::CriticalError("Invalid bytes for grid cell".to_string()))?; + Ok(RawGridCell::from(cell_bytes)) + }) + .collect::>>()?; with_current_shape_mut!(state, |shape: &mut Shape| { shape.set_grid_cells(cells.into_iter().map(|raw| raw.into()).collect()); diff --git a/render-wasm/src/wasm/paths.rs b/render-wasm/src/wasm/paths.rs index 0748111533..f700317633 100644 --- a/render-wasm/src/wasm/paths.rs +++ b/render-wasm/src/wasm/paths.rs @@ -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::()]> for RawSegmentData { } impl TryFrom<&[u8]> for RawSegmentData { - type Error = String; - fn try_from(bytes: &[u8]) -> Result { + type Error = Error; + fn try_from(bytes: &[u8]) -> Result { 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> { } #[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::(); + 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::(); - 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] diff --git a/render-wasm/src/wasm/shapes/base_props.rs b/render-wasm/src/wasm/shapes/base_props.rs index 265e4f7841..5e0146f276 100644 --- a/render-wasm/src/wasm/shapes/base_props.rs +++ b/render-wasm/src/wasm/shapes/base_props.rs @@ -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)] diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index 3635e0949f..f34aa10cf2 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -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 {:?}", diff --git a/render-wasm/src/wasm/text_editor.rs b/render-wasm/src/wasm/text_editor.rs index 25182c5050..eeac88ce43 100644 --- a/render-wasm/src/wasm/text_editor.rs +++ b/render-wasm/src/wasm/text_editor.rs @@ -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] diff --git a/render-wasm/src/wasm/transforms.rs b/render-wasm/src/wasm/transforms.rs index 88b888d1ef..b0e0a2d84d 100644 --- a/render-wasm/src/wasm/transforms.rs +++ b/render-wasm/src/wasm/transforms.rs @@ -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 { + type Error = Error; + fn try_from(bytes: &[u8]) -> Result { 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 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 = 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::>>()?; 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)) }) }