diff --git a/frontend/playwright/data/render-wasm/get-file-inner-strokes-artifacts.json b/frontend/playwright/data/render-wasm/get-file-inner-strokes-artifacts.json new file mode 100644 index 0000000000..5987b72225 --- /dev/null +++ b/frontend/playwright/data/render-wasm/get-file-inner-strokes-artifacts.json @@ -0,0 +1,814 @@ +{ + "~:features": { + "~#set": [ + "fdata/path-data", + "plugins/runtime", + "design-tokens/v1", + "variants/v1", + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "render-wasm/v1", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:team-id": "~ueba8fa2e-4140-8084-8005-448635d7a724", + "~: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": "gaps", + "~:revn": 79, + "~:modified-at": "~m1771855365377", + "~:vern": 0, + "~:id": "~ueffcbebc-b8c8-802f-8007-9a0b2e2c863f", + "~: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": "~ueba8fa2e-4140-8084-8005-448635da32b4", + "~:created-at": "~m1771591980210", + "~:backend": "legacy-db", + "~:data": { + "~:pages": [ + "~ueffcbebc-b8c8-802f-8007-9a0b2e2c8640" + ], + "~:pages-index": { + "~ueffcbebc-b8c8-802f-8007-9a0b2e2c8640": { + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0, + "~:y": 0.01 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 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, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 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, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [ + "~u36e8a3ad-2b63-8008-8007-9a0b2f24ca4e", + "~ufbc43ead-a2ce-8058-8007-9a0daf843e09", + "~ufbc43ead-a2ce-8058-8007-9a0dbe2f49b8", + "~u5bebb998-d617-801b-8007-9a3fbd5cc804", + "~u80e2fa5a-cd1c-8043-8007-9d8aaca49f40" + ] + } + }, + "~ufbc43ead-a2ce-8058-8007-9a0dbe2f49b8": { + "~#shape": { + "~:y": null, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:content": { + "~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAD/f5dEM2EsRAIAAAAAAAAAAAAAAAAAAAAAAAAAUhmnRABACkQCAAAAAAAAAAAAAAAAAAAAAAAAAP8/vET//01EAgAAAAAAAAAAAAAAAAAAAAAAAAD/f5dEM2EsRA==" + }, + "~:name": "Path", + "~:width": null, + "~:type": "~:path", + "~:points": [ + { + "~#point": { + "~:x": 1212.00003372852, + "~:y": 553.000012923003 + } + }, + { + "~#point": { + "~:x": 1506.00004755679, + "~:y": 553.000012923003 + } + }, + { + "~#point": { + "~:x": 1506.00004755679, + "~:y": 823.999993849517 + } + }, + { + "~#point": { + "~:x": 1212.00003372852, + "~:y": 823.999993849517 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~ufbc43ead-a2ce-8058-8007-9a0dbe2f49b8", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-alignment": "~:inner", + "~:stroke-style": "~:solid", + "~:stroke-color": "#000000", + "~:stroke-opacity": 1, + "~:stroke-width": 10 + } + ], + "~:x": null, + "~:proportion": 1, + "~:shadow": [], + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 1212.00003372852, + "~:y": 553.000012923003, + "~:width": 294.000013828278, + "~:height": 270.999980926514, + "~:x1": 1212.00003372852, + "~:y1": 553.000012923003, + "~:x2": 1506.00004755679, + "~:y2": 823.999993849517 + } + }, + "~:fills": [ + { + "~:fill-color": "#ffffff", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": null, + "~:flip-y": null + } + }, + "~u36e8a3ad-2b63-8008-8007-9a0b2f24ca4e": { + "~#shape": { + "~:y": 122.000001761754, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 463.999987447937, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 694.000014750112, + "~:y": 122.000001761754 + } + }, + { + "~#point": { + "~:x": 1158.00000219805, + "~:y": 122.000001761754 + } + }, + { + "~#point": { + "~:x": 1158.00000219805, + "~:y": 499.999980116278 + } + }, + { + "~#point": { + "~:x": 694.000014750112, + "~:y": 499.999980116278 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~u36e8a3ad-2b63-8008-8007-9a0b2f24ca4e", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-alignment": "~:inner", + "~:stroke-style": "~:solid", + "~:stroke-color": "#000000", + "~:stroke-opacity": 1, + "~:stroke-width": 100 + }, + { + "~:stroke-alignment": "~:outer", + "~:stroke-style": "~:solid", + "~:stroke-color": "#000000", + "~:stroke-opacity": 1, + "~:stroke-width": 100 + } + ], + "~:x": 694.000014750113, + "~:proportion": 1, + "~:shadow": [], + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 694.000014750113, + "~:y": 122.000001761754, + "~:width": 463.999987447937, + "~:height": 377.999978354524, + "~:x1": 694.000014750113, + "~:y1": 122.000001761754, + "~:x2": 1158.00000219805, + "~:y2": 499.999980116278 + } + }, + "~:fills": [ + { + "~:fill-color": "#ffffff", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 377.999978354524, + "~:flip-y": null + } + }, + "~ufbc43ead-a2ce-8058-8007-9a0daf843e09": { + "~#shape": { + "~:y": 262.999997589325, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Ellipse", + "~:width": 266.000036716461, + "~:type": "~:circle", + "~:points": [ + { + "~#point": { + "~:x": 1271.00000137752, + "~:y": 262.999997589325 + } + }, + { + "~#point": { + "~:x": 1537.00003809398, + "~:y": 262.999997589325 + } + }, + { + "~#point": { + "~:x": 1537.00003809398, + "~:y": 483.000033828949 + } + }, + { + "~#point": { + "~:x": 1271.00000137752, + "~:y": 483.000033828949 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~ufbc43ead-a2ce-8058-8007-9a0daf843e09", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-alignment": "~:inner", + "~:stroke-style": "~:solid", + "~:stroke-color": "#000000", + "~:stroke-opacity": 1, + "~:stroke-width": 10 + } + ], + "~:x": 1271.00000137752, + "~:proportion": 1, + "~:shadow": [ + { + "~:id": "~u9c6321b5-aeab-809f-8007-971f9e232191", + "~:style": "~:drop-shadow", + "~:color": { + "~:color": "#000000", + "~:opacity": 1 + }, + "~:offset-x": 4, + "~:offset-y": 4, + "~:blur": 0, + "~:spread": 0, + "~:hidden": true + } + ], + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 1271.00000137752, + "~:y": 262.999997589325, + "~:width": 266.000036716461, + "~:height": 220.000036239624, + "~:x1": 1271.00000137752, + "~:y1": 262.999997589325, + "~:x2": 1537.00003809398, + "~:y2": 483.000033828949 + } + }, + "~:fills": [ + { + "~:fill-color": "#ffffff", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 220.000036239624, + "~:flip-y": null + } + }, + "~u80e2fa5a-cd1c-8043-8007-9d8aaca49f40": { + "~#shape": { + "~:y": -286.999972473494, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:auto-width", + "~:content": { + "~:type": "root", + "~:key": "1srkh8oc2vd", + "~:children": [ + { + "~:type": "paragraph-set", + "~:children": [ + { + "~:line-height": "1.2", + "~:font-style": "normal", + "~:children": [ + { + "~:line-height": "1.2", + "~:font-style": "normal", + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:font-id": "sourcesanspro", + "~:key": "170uyffw5ph", + "~:font-size": "400", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#ffffff", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro", + "~:text": "HELLO" + } + ], + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:key": "psg8ayj675", + "~:font-size": "400", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:type": "paragraph", + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#ffffff", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro" + } + ] + } + ], + "~:vertical-align": "top" + }, + "~:hide-in-viewer": false, + "~:name": "HELLO", + "~:width": 1116.00003953244, + "~:type": "~:text", + "~:points": [ + { + "~#point": { + "~:x": 545.000013504691, + "~:y": -286.999972473494 + } + }, + { + "~#point": { + "~:x": 1661.00005303713, + "~:y": -286.999972473494 + } + }, + { + "~#point": { + "~:x": 1661.00005303713, + "~:y": 193.000017549648 + } + }, + { + "~#point": { + "~:x": 545.000013504691, + "~:y": 193.000017549648 + } + } + ], + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~u80e2fa5a-cd1c-8043-8007-9d8aaca49f40", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:position-data": [ + { + "~:y": 211.980041503906, + "~:line-height": "1.2", + "~:font-style": "normal", + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:font-size": "400", + "~:font-weight": "400", + "~:text-direction": "ltr", + "~:width": 1115.22998046875, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:x": 545, + "~:fills": [ + { + "~:fill-color": "#ffffff", + "~:fill-opacity": 1 + } + ], + "~:direction": "ltr", + "~:font-family": "sourcesanspro", + "~:height": 517.960021972656, + "~:text": "HELLO" + } + ], + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-style": "~:solid", + "~:stroke-alignment": "~:inner", + "~:stroke-width": 5, + "~:stroke-color": "#000000", + "~:stroke-opacity": 1 + } + ], + "~:x": 545.000013504691, + "~:selrect": { + "~#rect": { + "~:x": 545.000013504691, + "~:y": -286.999972473494, + "~:width": 1116.00003953244, + "~:height": 479.999990023141, + "~:x1": 545.000013504691, + "~:y1": -286.999972473494, + "~:x2": 1661.00005303713, + "~:y2": 193.000017549648 + } + }, + "~:flip-x": null, + "~:height": 479.999990023141, + "~:flip-y": null + } + }, + "~u5bebb998-d617-801b-8007-9a3fbd5cc804": { + "~#shape": { + "~:y": 543.00001095581, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 463.999987447937, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 693.999990768432, + "~:y": 543.00001095581 + } + }, + { + "~#point": { + "~:x": 1157.99997821637, + "~:y": 543.00001095581 + } + }, + { + "~#point": { + "~:x": 1157.99997821637, + "~:y": 920.999989310334 + } + }, + { + "~#point": { + "~:x": 693.999990768432, + "~:y": 920.999989310334 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~u5bebb998-d617-801b-8007-9a3fbd5cc804", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [ + { + "~:stroke-alignment": "~:inner", + "~:stroke-style": "~:solid", + "~:stroke-color": "#000000", + "~:stroke-opacity": 1, + "~:stroke-width": 100 + } + ], + "~:x": 693.999990768432, + "~:proportion": 1, + "~:shadow": [], + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 693.999990768432, + "~:y": 543.00001095581, + "~:width": 463.999987447937, + "~:height": 377.999978354524, + "~:x1": 693.999990768432, + "~:y1": 543.00001095581, + "~:x2": 1157.99997821637, + "~:y2": 920.999989310334 + } + }, + "~:fills": [ + { + "~:fill-color": "#ffffff", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 377.999978354524, + "~:flip-y": null + } + } + }, + "~:id": "~ueffcbebc-b8c8-802f-8007-9a0b2e2c8640", + "~:name": "Page 1", + "~:background": "#000000" + } + }, + "~:id": "~ueffcbebc-b8c8-802f-8007-9a0b2e2c863f", + "~:options": { + "~:components-v2": true, + "~:base-font-size": "16px" + } + } + } \ No newline at end of file diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js index 9a4b26809b..242f0bf6d2 100644 --- a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js +++ b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js @@ -432,3 +432,27 @@ test("Keeps component visible when focusing after creating it", async ({ await workspace.hideUI(); await expect(workspace.canvas).toHaveScreenshot(); }); + +test("Check inner stroke artifacts", async ({ + page, +}) => { + const workspace = new WasmWorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockGetFile("render-wasm/get-file-inner-strokes-artifacts.json"); + + await workspace.goToWorkspace({ + id: "effcbebc-b8c8-802f-8007-9a0b2e2c863f", + pageId: "effcbebc-b8c8-802f-8007-9a0b2e2c8640", + }); + await workspace.waitForFirstRenderWithoutUI(); + + const previousRenderCount = await workspace.getRenderCount(); + await page.keyboard.press("ControlOrMeta++"); + await workspace.waitForNextRender(previousRenderCount); + + // Stricter comparison: artifacts are very subtle + await expect(workspace.canvas).toHaveScreenshot({ + maxDiffPixelRatio: 0, + threshold: 0.1, + }); +}); \ No newline at end of file diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Check-inner-stroke-artifacts-1.png b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Check-inner-stroke-artifacts-1.png new file mode 100644 index 0000000000..8b0fdc9c61 Binary files /dev/null and b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Check-inner-stroke-artifacts-1.png differ diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss index b34628c932..642a4cabf3 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss +++ b/frontend/src/app/main/ui/workspace/shapes/text/v2_editor.scss @@ -73,12 +73,12 @@ } .grow-type-auto-width { - [data-itype="inline"], + [data-itype="span"], [data-itype="paragraph"] { white-space: nowrap; } - [data-itype="inline"] { + [data-itype="span"] { white-space-collapse: preserve; } } diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index f0a3bc3600..041cb6f53a 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -19,7 +19,6 @@ [app.main.data.workspace.media :as dwm] [app.main.data.workspace.path :as dwdp] [app.main.data.workspace.specialized-panel :as-alias dwsp] - [app.main.data.workspace.texts :as dwt] [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] @@ -50,41 +49,42 @@ (mf/deps id blocked hidden type selected edition drawing-tool text-editing? node-editing? grid-editing? drawing-path? create-comment? @z? @space? panning read-only?) - (fn [bevent] + (fn [event] ;; We need to handle editor related stuff here because ;; handling on editor dom node does not works properly. - (let [target (dom/get-target bevent) + (let [target (dom/get-target event) editor (txu/closest-text-editor-content target)] ;; Capture mouse pointer to detect the movements even if cursor ;; leaves the viewport or the browser itself ;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture (if editor - (.setPointerCapture editor (.-pointerId bevent)) - (.setPointerCapture target (.-pointerId bevent)))) + (.setPointerCapture editor (.-pointerId event)) + (.setPointerCapture target (.-pointerId event)))) - (when (or (dom/class? (dom/get-target bevent) "viewport-controls") - (dom/class? (dom/get-target bevent) "viewport-selrect") - (dom/child? (dom/get-target bevent) (dom/query ".grid-layout-editor"))) + (when (or (dom/class? (dom/get-target event) "viewport-controls") + (dom/class? (dom/get-target event) "viewport-selrect") + (dom/child? (dom/get-target event) (dom/query ".grid-layout-editor"))) - (dom/stop-propagation bevent) + (dom/stop-propagation event) (when-not @z? - (let [event (dom/event->native-event bevent) - ctrl? (kbd/ctrl? event) - meta? (kbd/meta? event) - shift? (kbd/shift? event) - alt? (kbd/alt? event) - mod? (kbd/mod? event) + (let [native-event (dom/event->native-event event) + ctrl? (kbd/ctrl? native-event) + meta? (kbd/meta? native-event) + shift? (kbd/shift? native-event) + alt? (kbd/alt? native-event) + mod? (kbd/mod? native-event) + off-pt (dom/get-offset-position native-event) - left-click? (and (not panning) (dom/left-mouse? bevent)) - middle-click? (and (not panning) (dom/middle-mouse? bevent))] + left-click? (and (not panning) (dom/left-mouse? event)) + middle-click? (and (not panning) (dom/middle-mouse? event))] (cond (or middle-click? (and left-click? @space?)) (do - (dom/prevent-default bevent) + (dom/prevent-default event) (if mod? - (let [raw-pt (dom/get-client-position event) + (let [raw-pt (dom/get-client-position native-event) pt (uwvv/point->viewport raw-pt)] (st/emit! (dw/start-zooming pt))) (st/emit! (dw/start-panning)))) @@ -94,18 +94,23 @@ (st/emit! (mse/->MouseEvent :down ctrl? shift? alt? meta?) ::dwsp/interrupt) + (when (wasm.api/text-editor-is-active?) + (wasm.api/text-editor-pointer-down (.-x off-pt) (.-y off-pt))) + (when (and (not= edition id) (or text-editing? grid-editing?)) (st/emit! (dw/clear-edition-mode)) + ;; FIXME: I think this is not completely correct because this + ;; is going to happen even when clicking or selecting text. ;; Sync and stop WASM text editor when exiting edit mode - (when (and text-editing? - (features/active-feature? @st/state "render-wasm/v1") - wasm.wasm/context-initialized?) - (when-let [{:keys [shape-id content]} (wasm.api/text-editor-sync-content)] - (st/emit! (dwt/v2-update-text-shape-content - shape-id content - :update-name? true - :finalize? true))) - (wasm.api/text-editor-stop))) + #_(when (and text-editing? + (features/active-feature? @st/state "render-wasm/v1") + wasm.wasm/context-initialized?) + (when-let [{:keys [shape-id content]} (wasm.api/text-editor-sync-content)] + (st/emit! (dwt/v2-update-text-shape-content + shape-id content + :update-name? true + :finalize? true))) + (wasm.api/text-editor-stop))) (when (and (not text-editing?) (not blocked) @@ -187,10 +192,14 @@ alt? (kbd/alt? event) meta? (kbd/meta? event) hovering? (some? @hover) + native-event (dom/event->native-event event) + off-pt (dom/get-offset-position native-event) raw-pt (dom/get-client-position event) pt (uwvv/point->viewport raw-pt)] (st/emit! (mse/->MouseEvent :click ctrl? shift? alt? meta?)) + ;; FIXME: Maybe we can transform this into a cond instead + ;; of multiple (when)s. (when (and hovering? (not @space?) (not edition) @@ -198,6 +207,8 @@ (not drawing-tool)) (st/emit! (dw/select-shape (:id @hover) shift?))) + ;; FIXME: Maybe we can move into a function of the kind + ;; "text-editor-on-click" ;; If clicking on a text shape and wasm render is enabled, forward cursor position (when (and hovering? (not @space?) @@ -208,9 +219,7 @@ (when (and (= :text (:type hover-shape)) (features/active-feature? @st/state "text-editor-wasm/v1") wasm.wasm/context-initialized?) - (let [raw-pt (dom/get-client-position event)] - ;; FIXME - (wasm.api/text-editor-set-cursor-from-point (.-x raw-pt) (.-y raw-pt)))))) + (wasm.api/text-editor-set-cursor-from-point (.-x off-pt) (.-y off-pt))))) (when (and @z? (not @space?) @@ -261,6 +270,12 @@ wasm.wasm/context-initialized?) (wasm.api/text-editor-start id))) + (and editable? (= id edition) (not read-only?) + (= type :text) + (features/active-feature? @st/state "text-editor-wasm/v1") + wasm.wasm/context-initialized?) + (wasm.api/text-editor-select-all) + (some? selected-shape) (do (reset! hover selected-shape) @@ -310,20 +325,24 @@ ;; Release pointer on mouse up (.releasePointerCapture target (.-pointerId event))) - (let [event (dom/event->native-event event) - ctrl? (kbd/ctrl? event) - shift? (kbd/shift? event) - alt? (kbd/alt? event) - meta? (kbd/meta? event) + (let [native-event (dom/event->native-event event) + off-pt (dom/get-offset-position native-event) + ctrl? (kbd/ctrl? native-event) + shift? (kbd/shift? native-event) + alt? (kbd/alt? native-event) + meta? (kbd/meta? native-event) - left-click? (= 1 (.-which event)) - middle-click? (= 2 (.-which event))] + left-click? (= 1 (.-which native-event)) + middle-click? (= 2 (.-which native-event))] (when left-click? - (st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?))) + (st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?)) + + (when (wasm.api/text-editor-is-active?) + (wasm.api/text-editor-pointer-up (.-x off-pt) (.-y off-pt)))) (when middle-click? - (dom/prevent-default event) + (dom/prevent-default native-event) ;; We store this so in Firefox the middle button won't do a paste of the content (mf/set-ref-val! disable-paste-ref true) @@ -381,7 +400,9 @@ (let [last-position (mf/use-var nil)] (mf/use-fn (fn [event] - (let [raw-pt (dom/get-client-position event) + (let [native-event (unchecked-get event "nativeEvent") + off-pt (dom/get-offset-position native-event) + raw-pt (dom/get-client-position event) pt (uwvv/point->viewport raw-pt) ;; We calculate the delta because Safari's MouseEvent.movementX/Y drop @@ -390,6 +411,12 @@ (gpt/subtract raw-pt @last-position) (gpt/point 0 0))] + ;; IMPORTANT! This function, right now it's called on EVERY pointermove. I think + ;; in the future (when we handle the UI in the render) should be better to + ;; have a "wasm.api/pointer-move" function that works as an entry point for + ;; all the pointer-move events. + (wasm.api/text-editor-pointer-move (.-x off-pt) (.-y off-pt)) + (rx/push! move-stream pt) (reset! last-position raw-pt) (st/emit! (mse/->PointerEvent :delta delta diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 849be1089b..e3f55b0d37 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -87,7 +87,11 @@ (def text-editor-start text-editor/text-editor-start) (def text-editor-stop text-editor/text-editor-stop) (def text-editor-set-cursor-from-point text-editor/text-editor-set-cursor-from-point) +(def text-editor-pointer-down text-editor/text-editor-pointer-down) +(def text-editor-pointer-move text-editor/text-editor-pointer-move) +(def text-editor-pointer-up text-editor/text-editor-pointer-up) (def text-editor-is-active? text-editor/text-editor-is-active?) +(def text-editor-select-all text-editor/text-editor-select-all) (def text-editor-sync-content text-editor/text-editor-sync-content) (def dpr @@ -263,22 +267,6 @@ [attrs] (text-editor/apply-style-to-selection attrs use-shape set-shape-text-content)) -(defn update-text-rect! - [id] - (when wasm/context-initialized? - (mw/emit! - {:cmd :index/update-text-rect - :page-id (:current-page-id @st/state) - :shape-id id - :dimensions (get-text-dimensions id)}))) - -(defn- ensure-text-content - "Guarantee that the shape always sends a valid text tree to WASM. When the - content is nil (freshly created text) we fall back to - tc/default-text-content so the renderer receives typography information." - [content] - (or content (tc/v2-default-text-content))) - (defn set-parent-id [id] (let [buffer (uuid/get-u32 id)] @@ -996,6 +984,22 @@ (render-finish) (perf/end-measure "set-view-box::zoom"))))) +(defn update-text-rect! + [id] + (when wasm/context-initialized? + (mw/emit! + {:cmd :index/update-text-rect + :page-id (:current-page-id @st/state) + :shape-id id + :dimensions (get-text-dimensions id)}))) + +(defn- ensure-text-content + "Guarantee that the shape always sends a valid text tree to WASM. When the + content is nil (freshly created text) we fall back to + tc/default-text-content so the renderer receives typography information." + [content] + (or content (tc/v2-default-text-content))) + (defn set-object [shape] (perf/begin-measure "set-object") diff --git a/frontend/src/app/render_wasm/text_editor.cljs b/frontend/src/app/render_wasm/text_editor.cljs index 882f24f890..21bcca45d2 100644 --- a/frontend/src/app/render_wasm/text_editor.cljs +++ b/frontend/src/app/render_wasm/text_editor.cljs @@ -27,6 +27,21 @@ (when wasm/context-initialized? (h/call wasm/internal-module "_text_editor_set_cursor_from_point" x y))) +(defn text-editor-pointer-down + [x y] + (when wasm/context-initialized? + (h/call wasm/internal-module "_text_editor_pointer_down" x y))) + +(defn text-editor-pointer-move + [x y] + (when wasm/context-initialized? + (h/call wasm/internal-module "_text_editor_pointer_move" x y))) + +(defn text-editor-pointer-up + [x y] + (when wasm/context-initialized? + (h/call wasm/internal-module "_text_editor_pointer_up" x y))) + (defn text-editor-update-blink [timestamp-ms] (when wasm/context-initialized? @@ -83,9 +98,12 @@ (h/call wasm/internal-module "_text_editor_stop"))) (defn text-editor-is-active? - [] - (when wasm/context-initialized? - (not (zero? (h/call wasm/internal-module "_text_editor_is_active"))))) + ([id] + (when wasm/context-initialized? + (not (zero? (h/call wasm/internal-module "_text_editor_is_active_with_id" id))))) + ([] + (when wasm/context-initialized? + (not (zero? (h/call wasm/internal-module "_text_editor_is_active")))))) (defn text-editor-export-content [] diff --git a/frontend/text-editor/src/editor/content/dom/Color.js b/frontend/text-editor/src/editor/content/dom/Color.js index ba798dd67a..69f4805495 100644 --- a/frontend/text-editor/src/editor/content/dom/Color.js +++ b/frontend/text-editor/src/editor/content/dom/Color.js @@ -76,3 +76,4 @@ export function getFills(fillStyle) { const [color, opacity] = getColor(fillStyle); return `[["^ ","~:fill-color","${color}","~:fill-opacity",${opacity}]]`; } + diff --git a/frontend/text-editor/src/playground.js b/frontend/text-editor/src/playground.js index ba36cfb046..0bdf72db8c 100644 --- a/frontend/text-editor/src/playground.js +++ b/frontend/text-editor/src/playground.js @@ -162,12 +162,15 @@ class TextEditorPlayground { } this.#module.call("use_shape", ...textShape.id); + // FIXME: This function doesn't exists anymore. + /* const caretPosition = this.#module.call( "get_caret_position_at", e.offsetX, e.offsetY, ); console.log("caretPosition", caretPosition); + */ }; #onResize = (_entries) => { diff --git a/render-wasm/Cargo.lock b/render-wasm/Cargo.lock index 9a9d050849..e5c289d99e 100644 --- a/render-wasm/Cargo.lock +++ b/render-wasm/Cargo.lock @@ -202,9 +202,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -214,9 +214,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "indexmap" -version = "2.7.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", @@ -253,12 +253,6 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" version = "0.2.161" @@ -468,18 +462,27 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -500,11 +503,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -515,9 +518,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "skia-bindings" -version = "0.87.0" +version = "0.93.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704242769235d2ffe66a2a0a3002661262fc4af08d32807c362d7b0160ee703c" +checksum = "2359f7e30c9da3f322f8ca3d4ec0abbc12a40035ce758309db0cdab07b5d4476" dependencies = [ "bindgen", "cc", @@ -532,13 +535,12 @@ dependencies = [ [[package]] name = "skia-safe" -version = "0.87.0" +version = "0.93.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7d94f3e7537c71ad4cf132eb26e3be8c8a886ed3649c4525c089041fc312b2" +checksum = "7f9e837ea9d531c9efee8f980bfcdb7226b21db0285b0c3171d8be745829f940" dependencies = [ "base64", "bitflags", - "lazy_static", "percent-encoding", "skia-bindings", "skia-svg-macros", @@ -579,38 +581,43 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.19" +version = "1.0.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" dependencies = [ - "serde", + "indexmap", + "serde_core", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ - "serde", + "serde_core", ] [[package]] -name = "toml_edit" -version = "0.22.22" +name = "toml_parser" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", "winnow", ] +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "unicode-ident" version = "1.0.13" @@ -775,12 +782,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" -dependencies = [ - "memchr", -] +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" [[package]] name = "xattr" diff --git a/render-wasm/Cargo.toml b/render-wasm/Cargo.toml index 82cde41199..ca37fe4104 100644 --- a/render-wasm/Cargo.toml +++ b/render-wasm/Cargo.toml @@ -25,7 +25,7 @@ gl = "0.14.0" glam = "0.24.2" indexmap = "2.7.1" macros = { path = "macros" } -skia-safe = { version = "0.87.0", default-features = false, features = [ +skia-safe = { version = "0.93.1", default-features = false, features = [ "gl", "svg", "textlayout", diff --git a/render-wasm/_build_env b/render-wasm/_build_env index 2bfe0e778f..94716a05e4 100644 --- a/render-wasm/_build_env +++ b/render-wasm/_build_env @@ -10,7 +10,7 @@ fi export BUILD_NAME="${BUILD_NAME:-render-wasm}" export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"wasm32-unknown-emscripten"}; -export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.87.0/skia-binaries-e551f334ad5cbdf43abf-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"} +export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"} # 256 MB of initial heap to perform less # initial calls to memory grow. diff --git a/render-wasm/lint b/render-wasm/lint index aaca98bc27..e94145189a 100755 --- a/render-wasm/lint +++ b/render-wasm/lint @@ -11,7 +11,7 @@ fi . ./_build_env export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"wasm32-unknown-emscripten"}; -export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.87.0/skia-binaries-e551f334ad5cbdf43abf-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"} +export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"} ALLOWED_RULES="-D static_mut_refs" diff --git a/render-wasm/pnpm-workspace.yaml b/render-wasm/pnpm-workspace.yaml new file mode 100644 index 0000000000..efc037aa84 --- /dev/null +++ b/render-wasm/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - esbuild diff --git a/render-wasm/src/math.rs b/render-wasm/src/math.rs index 9392cb00e8..de819a86bb 100644 --- a/render-wasm/src/math.rs +++ b/render-wasm/src/math.rs @@ -356,7 +356,7 @@ impl Bounds { } pub fn from_rect(r: &Rect) -> Self { - let [nw, ne, se, sw] = r.to_quad(); + let [nw, ne, se, sw] = r.to_quad(None); Self::new(nw, ne, se, sw) } diff --git a/render-wasm/src/math/bools.rs b/render-wasm/src/math/bools.rs index 123e1b262a..67de1e874e 100644 --- a/render-wasm/src/math/bools.rs +++ b/render-wasm/src/math/bools.rs @@ -477,30 +477,32 @@ pub fn debug_render_bool_paths( paint.set_alpha_f(1.0); paint.set_style(skia::PaintStyle::Stroke); - let mut path = skia::Path::default(); - path.move_to((b.1.start.x as f32, b.1.start.y as f32)); - - match b.1.handles { - BezierHandles::Linear => { - path.line_to((b.1.end.x as f32, b.1.end.y as f32)); + let path = { + let mut pb = skia::PathBuilder::new(); + pb.move_to((b.1.start.x as f32, b.1.start.y as f32)); + match b.1.handles { + BezierHandles::Linear => { + pb.line_to((b.1.end.x as f32, b.1.end.y as f32)); + } + BezierHandles::Quadratic { handle } => { + pb.quad_to( + (handle.x as f32, handle.y as f32), + (b.1.end.x as f32, b.1.end.y as f32), + ); + } + BezierHandles::Cubic { + handle_start, + handle_end, + } => { + pb.cubic_to( + (handle_start.x as f32, handle_start.y as f32), + (handle_end.x as f32, handle_end.y as f32), + (b.1.end.x as f32, b.1.end.y as f32), + ); + } } - BezierHandles::Quadratic { handle } => { - path.quad_to( - (handle.x as f32, handle.y as f32), - (b.1.end.x as f32, b.1.end.y as f32), - ); - } - BezierHandles::Cubic { - handle_start, - handle_end, - } => { - path.cubic_to( - (handle_start.x as f32, handle_start.y as f32), - (handle_end.x as f32, handle_end.y as f32), - (b.1.end.x as f32, b.1.end.y as f32), - ); - } - } + pb.detach() + }; canvas.draw_path(&path, &paint); let mut v1 = b.1.normal(TValue::Parametric(1.0)); diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 2265ef4de7..1493b9851e 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -642,7 +642,7 @@ impl RenderState { apply_to_current_surface: bool, offset: Option<(f32, f32)>, parent_shadows: Option>, - spread: Option, + outset: Option, ) { let surface_ids = fills_surface_id as u32 | strokes_surface_id as u32 @@ -718,7 +718,7 @@ impl RenderState { &visible_strokes, Some(SurfaceId::Current), antialias, - spread, + outset, ); self.surfaces.apply_mut(SurfaceId::Current as u32, |s| { @@ -860,6 +860,8 @@ impl RenderState { let text_content = text_content.new_bounds(shape.selrect()); let count_inner_strokes = shape.count_visible_inner_strokes(); + // Erode the main text fill by 1px when there are inner strokes, to avoid a visible seam at the glyph edge. + let text_fill_inset = (count_inner_strokes > 0).then(|| 1.0 / self.get_scale()); let text_stroke_blur_outset = Stroke::max_bounds_width(shape.visible_strokes(), false); let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None); @@ -886,6 +888,7 @@ impl RenderState { Some(fills_surface_id), None, None, + text_fill_inset, ); for stroke_paragraphs in stroke_paragraphs_list.iter_mut() { @@ -898,6 +901,7 @@ impl RenderState { None, None, text_stroke_blur_outset, + None, ); } } else { @@ -936,6 +940,7 @@ impl RenderState { text_drop_shadows_surface_id.into(), Some(&shadow), blur_filter.as_ref(), + None, ); } } else { @@ -961,6 +966,7 @@ impl RenderState { text_drop_shadows_surface_id.into(), Some(shadow), blur_filter.as_ref(), + None, ); } } @@ -974,6 +980,7 @@ impl RenderState { Some(fills_surface_id), None, blur_filter.as_ref(), + text_fill_inset, ); // 3. Stroke drop shadows @@ -998,6 +1005,7 @@ impl RenderState { None, blur_filter.as_ref(), text_stroke_blur_outset, + None, ); } @@ -1023,6 +1031,7 @@ impl RenderState { Some(innershadows_surface_id), Some(shadow), blur_filter.as_ref(), + None, ); } } @@ -1070,7 +1079,7 @@ impl RenderState { &fills_to_render, antialias, fills_surface_id, - spread, + outset, ); } } else { @@ -1080,7 +1089,7 @@ impl RenderState { &shape.fills, antialias, fills_surface_id, - spread, + outset, ); } @@ -1096,7 +1105,7 @@ impl RenderState { &visible_strokes, Some(strokes_surface_id), antialias, - spread, + outset, ); if !fast_mode { for stroke in &visible_strokes { @@ -1715,7 +1724,7 @@ impl RenderState { false, Some(shadow.offset), // Offset is geometric None, - Some(shadow.spread), // Spread is geometric + Some(shadow.spread), ); }); @@ -1756,7 +1765,7 @@ impl RenderState { false, Some(shadow.offset), // Offset is geometric None, - Some(shadow.spread), // Spread is geometric + Some(shadow.spread), ); }); diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs index f6f8a2e4ea..6e098c9752 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -2,7 +2,15 @@ use skia_safe::{self as skia, Paint, RRect}; use super::{filters, RenderState, SurfaceId}; use crate::render::get_source_rect; -use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, Type}; +use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, StrokeKind, Type}; + +/// True when the shape has at least one visible inner stroke. +fn has_inner_stroke(shape: &Shape) -> bool { + let is_open = shape.is_open(); + shape + .visible_strokes() + .any(|s| s.render_kind(is_open) == StrokeKind::Inner) +} fn draw_image_fill( render_state: &mut RenderState, @@ -51,15 +59,18 @@ fn draw_image_fill( canvas.clip_rect(container, skia::ClipOp::Intersect, antialias); } Type::Circle => { - let mut oval_path = skia::Path::new(); - oval_path.add_oval(container, None); + let oval_path = { + let mut pb = skia::PathBuilder::new(); + pb.add_oval(container, None, None); + pb.detach() + }; canvas.clip_path(&oval_path, skia::ClipOp::Intersect, antialias); } shape_type @ (Type::Path(_) | Type::Bool(_)) => { if let Some(path) = shape_type.path() { if let Some(path_transform) = path_transform { canvas.clip_path( - path.to_skia_path().transform(&path_transform), + &path.to_skia_path().make_transform(&path_transform), skia::ClipOp::Intersect, antialias, ); @@ -97,18 +108,33 @@ pub fn render( fills: &[Fill], antialias: bool, surface_id: SurfaceId, - spread: Option, + outset: Option, ) { if fills.is_empty() { return; } + let scale = render_state.get_scale().max(1e-6); + let inset = if has_inner_stroke(shape) { + Some(1.0 / scale) + } else { + None + }; + // Image fills use draw_image_fill which needs render_state for GPU images // and sampling options that get_fill_shader (used by merge_fills) lacks. let has_image_fills = fills.iter().any(|f| matches!(f, Fill::Image(_))); if has_image_fills { for fill in fills.iter().rev() { - render_single_fill(render_state, shape, fill, antialias, surface_id, spread); + render_single_fill( + render_state, + shape, + fill, + antialias, + surface_id, + outset, + inset, + ); } return; } @@ -125,7 +151,7 @@ pub fn render( |state, temp_surface| { let mut filtered_paint = paint.clone(); filtered_paint.set_image_filter(image_filter.clone()); - draw_fill_to_surface(state, shape, temp_surface, &filtered_paint, spread); + draw_fill_to_surface(state, shape, temp_surface, &filtered_paint, outset, inset); }, ) { return; @@ -134,33 +160,35 @@ pub fn render( } } - draw_fill_to_surface(render_state, shape, surface_id, &paint, spread); + draw_fill_to_surface(render_state, shape, surface_id, &paint, outset, inset); } /// Draws a single paint (with a merged shader) to the appropriate surface /// based on the shape type. +/// When `inset` is Some(eps), the fill is inset by eps (e.g. to avoid seam with inner strokes). fn draw_fill_to_surface( render_state: &mut RenderState, shape: &Shape, surface_id: SurfaceId, paint: &Paint, - spread: Option, + outset: Option, + inset: Option, ) { match &shape.shape_type { Type::Rect(_) | Type::Frame(_) => { render_state .surfaces - .draw_rect_to(surface_id, shape, paint, spread); + .draw_rect_to(surface_id, shape, paint, outset, inset); } Type::Circle => { render_state .surfaces - .draw_circle_to(surface_id, shape, paint, spread); + .draw_circle_to(surface_id, shape, paint, outset, inset); } Type::Path(_) | Type::Bool(_) => { render_state .surfaces - .draw_path_to(surface_id, shape, paint, spread); + .draw_path_to(surface_id, shape, paint, outset, inset); } Type::Group(_) => {} _ => unreachable!("This shape should not have fills"), @@ -173,7 +201,8 @@ fn render_single_fill( fill: &Fill, antialias: bool, surface_id: SurfaceId, - spread: Option, + outset: Option, + inset: Option, ) { let mut paint = fill.to_paint(&shape.selrect, antialias); if let Some(image_filter) = shape.image_filter(1.) { @@ -192,7 +221,8 @@ fn render_single_fill( antialias, temp_surface, &filtered_paint, - spread, + outset, + inset, ); }, ) { @@ -209,10 +239,12 @@ fn render_single_fill( antialias, surface_id, &paint, - spread, + outset, + inset, ); } +#[allow(clippy::too_many_arguments)] fn draw_single_fill_to_surface( render_state: &mut RenderState, shape: &Shape, @@ -220,7 +252,8 @@ fn draw_single_fill_to_surface( antialias: bool, surface_id: SurfaceId, paint: &Paint, - spread: Option, + outset: Option, + inset: Option, ) { match (fill, &shape.shape_type) { (Fill::Image(image_fill), _) => { @@ -236,17 +269,17 @@ fn draw_single_fill_to_surface( (_, Type::Rect(_) | Type::Frame(_)) => { render_state .surfaces - .draw_rect_to(surface_id, shape, paint, spread); + .draw_rect_to(surface_id, shape, paint, outset, inset); } (_, Type::Circle) => { render_state .surfaces - .draw_circle_to(surface_id, shape, paint, spread); + .draw_circle_to(surface_id, shape, paint, outset, inset); } (_, Type::Path(_)) | (_, Type::Bool(_)) => { render_state .surfaces - .draw_path_to(surface_id, shape, paint, spread); + .draw_path_to(surface_id, shape, paint, outset, inset); } (_, Type::Group(_)) => { // Groups can have fills but they propagate them to their children diff --git a/render-wasm/src/render/grid_layout.rs b/render-wasm/src/render/grid_layout.rs index 699aea8cde..9f5067469e 100644 --- a/render-wasm/src/render/grid_layout.rs +++ b/render-wasm/src/render/grid_layout.rs @@ -24,7 +24,11 @@ pub fn render_overlay(zoom: f32, canvas: &skia::Canvas, shape: &Shape, shapes: S cell.anchor + hv + vv, cell.anchor + vv, ]; - let polygon = skia::Path::polygon(&points, true, None, None); + let polygon = { + let mut pb = skia::PathBuilder::new(); + pb.add_polygon(&points, true); + pb.detach() + }; canvas.draw_path(&polygon, &paint); } } diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index 4906714389..b5077f0688 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -109,17 +109,17 @@ fn render_shadow_paint( Type::Rect(_) | Type::Frame(_) => { render_state .surfaces - .draw_rect_to(surface_id, shape, paint, None); + .draw_rect_to(surface_id, shape, paint, None, None); } Type::Circle => { render_state .surfaces - .draw_circle_to(surface_id, shape, paint, None); + .draw_circle_to(surface_id, shape, paint, None, None); } Type::Path(_) | Type::Bool(_) => { render_state .surfaces - .draw_path_to(surface_id, shape, paint, None); + .draw_path_to(surface_id, shape, paint, None, None); } _ => {} } @@ -154,6 +154,7 @@ pub fn render_text_shadows( surface_id, None, blur_filter.as_ref(), + None, ); for stroke_paragraphs in stroke_paragraphs_group.iter_mut() { @@ -165,6 +166,7 @@ pub fn render_text_shadows( surface_id, None, blur_filter.as_ref(), + None, ); } diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index d48a41bfa9..e18b5f5e63 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -83,8 +83,11 @@ fn draw_stroke_on_circle( if let Some(clip_op) = stroke.clip_op() { let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); canvas.save_layer(&layer_rec); - let mut clip_path = skia::Path::new(); - clip_path.add_oval(rect, None); + let clip_path = { + let mut pb = skia::PathBuilder::new(); + pb.add_oval(rect, None, None); + pb.detach() + }; canvas.clip_path(&clip_path, clip_op, antialias); canvas.draw_oval(stroke_rect, &paint); canvas.restore(); @@ -153,8 +156,9 @@ fn draw_stroke_on_path( blur: Option<&ImageFilter>, antialias: bool, ) { - let mut skia_path = path.to_skia_path(); - skia_path.transform(path_transform.unwrap_or(&Matrix::default())); + let skia_path = path + .to_skia_path() + .make_transform(path_transform.unwrap_or(&Matrix::default())); let is_open = path.is_open(); @@ -174,15 +178,7 @@ fn draw_stroke_on_path( } } - handle_stroke_caps( - &mut skia_path, - stroke, - canvas, - is_open, - paint, - blur, - antialias, - ); + handle_stroke_caps(&skia_path, stroke, canvas, is_open, paint, blur, antialias); } fn handle_stroke_cap( @@ -224,7 +220,7 @@ fn handle_stroke_cap( #[allow(clippy::too_many_arguments)] fn handle_stroke_caps( - path: &mut skia::Path, + path: &skia::Path, stroke: &Stroke, canvas: &skia::Canvas, is_open: bool, @@ -232,8 +228,7 @@ fn handle_stroke_caps( blur: Option<&ImageFilter>, _antialias: bool, ) { - let mut points = vec![Point::default(); path.count_points()]; - path.get_points(&mut points); + 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(); @@ -304,13 +299,16 @@ fn draw_square_cap( let mut transformed_points = points; matrix.map_points(&mut transformed_points, &points); - let mut path = skia::Path::new(); - path.move_to(Point::new(center.x, center.y)); - path.move_to(transformed_points[0]); - path.line_to(transformed_points[1]); - path.line_to(transformed_points[2]); - path.line_to(transformed_points[3]); - path.close(); + let path = { + let mut pb = skia::PathBuilder::new(); + pb.move_to(Point::new(center.x, center.y)); + pb.move_to(transformed_points[0]); + pb.line_to(transformed_points[1]); + pb.line_to(transformed_points[2]); + pb.line_to(transformed_points[3]); + pb.close(); + pb.detach() + }; canvas.draw_path(&path, paint); } @@ -338,13 +336,15 @@ fn draw_arrow_cap( let mut transformed_points = points; matrix.map_points(&mut transformed_points, &points); - let mut path = skia::Path::new(); - path.move_to(transformed_points[1]); - path.line_to(transformed_points[0]); - path.line_to(transformed_points[2]); - path.move_to(Point::new(center.x, center.y)); - path.line_to(transformed_points[0]); - + let path = { + let mut pb = skia::PathBuilder::new(); + pb.move_to(transformed_points[1]); + pb.line_to(transformed_points[0]); + pb.line_to(transformed_points[2]); + pb.move_to(Point::new(center.x, center.y)); + pb.line_to(transformed_points[0]); + pb.detach() + }; canvas.draw_path(&path, paint); } @@ -372,12 +372,14 @@ fn draw_triangle_cap( let mut transformed_points = points; matrix.map_points(&mut transformed_points, &points); - let mut path = skia::Path::new(); - path.move_to(transformed_points[0]); - path.line_to(transformed_points[1]); - path.line_to(transformed_points[2]); - path.close(); - + let path = { + let mut pb = skia::PathBuilder::new(); + pb.move_to(transformed_points[0]); + pb.line_to(transformed_points[1]); + pb.line_to(transformed_points[2]); + pb.close(); + pb.detach() + }; canvas.draw_path(&path, paint); } @@ -441,8 +443,7 @@ fn draw_image_stroke_in_container( shape_type @ (Type::Path(_) | Type::Bool(_)) => { if let Some(p) = shape_type.path() { canvas.save(); - let mut path = p.to_skia_path(); - path.transform(&path_transform.unwrap()); + let path = p.to_skia_path().make_transform(&path_transform.unwrap()); let stroke_kind = stroke.render_kind(p.is_open()); match stroke_kind { StrokeKind::Inner => { @@ -464,7 +465,7 @@ fn draw_image_stroke_in_container( canvas.draw_path(&path, &thin_paint); } handle_stroke_caps( - &mut path, + &path, stroke, canvas, is_open, @@ -504,8 +505,7 @@ 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 mut path = p.to_skia_path(); - path.transform(&path_transform.unwrap()); + let path = p.to_skia_path().make_transform(&path_transform.unwrap()); let mut clear_paint = skia::Paint::default(); clear_paint.set_blend_mode(skia::BlendMode::Clear); clear_paint.set_anti_alias(antialias); @@ -526,7 +526,7 @@ pub fn render( strokes: &[&Stroke], surface_id: Option, antialias: bool, - spread: Option, + outset: Option, ) { if strokes.is_empty() { return; @@ -541,8 +541,8 @@ pub fn render( // edges semi-transparent and revealing strokes underneath. if let Some(image_filter) = shape.image_filter(1.) { let mut content_bounds = shape.selrect; - // Expand for spread if provided - if let Some(s) = spread.filter(|&s| s > 0.0) { + // Expand for outset if provided + if let Some(s) = outset.filter(|&s| s > 0.0) { content_bounds.outset((s, s)); } let max_margin = strokes @@ -588,7 +588,7 @@ pub fn render( antialias, true, true, - spread, + outset, ); } @@ -608,7 +608,7 @@ pub fn render( surface_id, None, antialias, - spread, + outset, ); } return; @@ -621,7 +621,7 @@ pub fn render( surface_id, antialias, false, - spread, + outset, ); } @@ -642,7 +642,7 @@ fn render_merged( surface_id: Option, antialias: bool, bypass_filter: bool, - spread: Option, + outset: Option, ) { let representative = *strokes .last() @@ -658,8 +658,8 @@ fn render_merged( if !bypass_filter { if let Some(image_filter) = blur_filter.clone() { let mut content_bounds = shape.selrect; - // Expand for spread if provided - if let Some(s) = spread.filter(|&s| s > 0.0) { + // Expand for outset if provided + if let Some(s) = outset.filter(|&s| s > 0.0) { content_bounds.outset((s, s)); } let stroke_margin = representative.bounds_width(shape.is_open()); @@ -694,7 +694,7 @@ fn render_merged( Some(temp_surface), antialias, true, - spread, + outset, ); state.surfaces.apply_mut(temp_surface as u32, |surface| { @@ -711,8 +711,8 @@ fn render_merged( // via SrcOver), matching the non-merged path where strokes[0] is drawn last (on top). let fills: Vec = strokes.iter().map(|s| s.fill.clone()).collect(); - // Expand selrect if spread is provided - let selrect = if let Some(s) = spread.filter(|&s| s > 0.0) { + // Expand selrect if outset is provided + let selrect = if let Some(s) = outset.filter(|&s| s > 0.0) { let mut r = shape.selrect; r.outset((s, s)); r @@ -790,7 +790,7 @@ pub fn render_single( surface_id: Option, shadow: Option<&ImageFilter>, antialias: bool, - spread: Option, + outset: Option, ) { render_single_internal( render_state, @@ -801,7 +801,7 @@ pub fn render_single( antialias, false, false, - spread, + outset, ); } @@ -815,13 +815,13 @@ fn render_single_internal( antialias: bool, bypass_filter: bool, skip_blur: bool, - spread: Option, + outset: Option, ) { if !bypass_filter { if let Some(image_filter) = shape.image_filter(1.) { let mut content_bounds = shape.selrect; - // Expand for spread if provided - if let Some(s) = spread.filter(|&s| s > 0.0) { + // Expand for outset if provided + if let Some(s) = outset.filter(|&s| s > 0.0) { content_bounds.outset((s, s)); } let stroke_margin = stroke.bounds_width(shape.is_open()); @@ -849,7 +849,7 @@ fn render_single_internal( antialias, true, true, - spread, + outset, ); }, ) { @@ -920,18 +920,18 @@ fn render_single_internal( let is_open = path.is_open(); let mut paint = stroke.to_stroked_paint(is_open, &selrect, svg_attrs, antialias); - // Apply spread by increasing stroke width - if let Some(s) = spread.filter(|&s| s > 0.0) { + // Apply outset by increasing stroke width + if let Some(s) = outset.filter(|&s| s > 0.0) { let current_width = paint.stroke_width(); // Path stroke kinds are built differently: // - Center uses the stroke width directly. // - Inner/Outer use a doubled width plus clipping/clearing logic. - // Compensate spread so visual growth is comparable across kinds. - let spread_growth = match stroke.render_kind(is_open) { + // Compensate outset so visual growth is comparable across kinds. + let outset_growth = match stroke.render_kind(is_open) { StrokeKind::Center => s * 2.0, StrokeKind::Inner | StrokeKind::Outer => s * 4.0, }; - paint.set_stroke_width(current_width + spread_growth); + paint.set_stroke_width(current_width + outset_growth); } draw_stroke_on_path( canvas, diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 56d26a48c7..5bb227ab04 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -360,16 +360,30 @@ impl Surfaces { id: SurfaceId, shape: &Shape, paint: &Paint, - spread: Option, + outset: Option, + inset: Option, ) { - let rect = if let Some(s) = spread.filter(|&s| s > 0.0) { + let mut rect = if let Some(s) = outset.filter(|&s| s > 0.0) { let mut r = shape.selrect; r.outset((s, s)); r } else { shape.selrect }; + if let Some(eps) = inset.filter(|&e| e > 0.0) { + rect.inset((eps, eps)); + } if let Some(corners) = shape.shape_type.corners() { + let corners = if let Some(eps) = inset.filter(|&e| e > 0.0) { + let mut c = corners; + for r in c.iter_mut() { + r.x = (r.x - eps).max(0.0); + r.y = (r.y - eps).max(0.0); + } + c + } else { + corners + }; let rrect = RRect::new_rect_radii(rect, &corners); self.canvas_and_mark_dirty(id).draw_rrect(rrect, paint); } else { @@ -382,15 +396,19 @@ impl Surfaces { id: SurfaceId, shape: &Shape, paint: &Paint, - spread: Option, + outset: Option, + inset: Option, ) { - let rect = if let Some(s) = spread.filter(|&s| s > 0.0) { + let mut rect = if let Some(s) = outset.filter(|&s| s > 0.0) { let mut r = shape.selrect; r.outset((s, s)); r } else { shape.selrect }; + if let Some(eps) = inset.filter(|&e| e > 0.0) { + rect.inset((eps, eps)); + } self.canvas_and_mark_dirty(id).draw_oval(rect, paint); } @@ -399,17 +417,27 @@ impl Surfaces { id: SurfaceId, shape: &Shape, paint: &Paint, - spread: Option, + outset: Option, + inset: Option, ) { if let Some(path) = shape.get_skia_path() { let canvas = self.canvas_and_mark_dirty(id); - if let Some(s) = spread.filter(|&s| s > 0.0) { + if let Some(s) = outset.filter(|&s| s > 0.0) { // Draw path as a thick stroke to get outset (expanded) silhouette let mut stroke_paint = paint.clone(); stroke_paint.set_stroke_width(s * 2.0); canvas.draw_path(&path, &stroke_paint); } else { canvas.draw_path(&path, paint); + // Inset: avoid seam with inner strokes by clearing a thin border from the fill + if let Some(eps) = inset.filter(|&e| e > 0.0) { + let mut clear_paint = skia::Paint::default(); + clear_paint.set_style(skia::PaintStyle::Stroke); + clear_paint.set_stroke_width(eps * 2.0); + clear_paint.set_blend_mode(skia::BlendMode::Clear); + clear_paint.set_anti_alias(paint.is_anti_alias()); + canvas.draw_path(&path, &clear_paint); + } } } } @@ -456,11 +484,7 @@ impl Surfaces { self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height, ); - let snapshot = self.current.image_snapshot(); - let mut direct_context = self.current.direct_context(); - let tile_image_opt = snapshot - .make_subset(direct_context.as_mut(), rect) - .or_else(|| self.current.image_snapshot_with_bounds(rect)); + let tile_image_opt = self.current.image_snapshot_with_bounds(rect); if let Some(tile_image) = tile_image_opt { // Draw to cache first (takes reference), then move to tile cache diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index 2761962071..42c35b450f 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -166,6 +166,7 @@ pub fn render_with_bounds_outset( shadow: Option<&Paint>, blur: Option<&ImageFilter>, stroke_bounds_outset: f32, + fill_inset: Option, ) { if let Some(render_state) = render_state { let target_surface = surface_id.unwrap_or(SurfaceId::Fills); @@ -193,6 +194,7 @@ pub fn render_with_bounds_outset( paragraph_builders, shadow, Some(&blur_filter_clone), + fill_inset, ); }, ) { @@ -202,15 +204,16 @@ pub fn render_with_bounds_outset( } let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface); - render_text_on_canvas(canvas, shape, paragraph_builders, shadow, blur); + render_text_on_canvas(canvas, shape, paragraph_builders, shadow, blur, fill_inset); return; } if let Some(canvas) = canvas { - render_text_on_canvas(canvas, shape, paragraph_builders, shadow, blur); + render_text_on_canvas(canvas, shape, paragraph_builders, shadow, blur, fill_inset); } } +#[allow(clippy::too_many_arguments)] pub fn render( render_state: Option<&mut RenderState>, canvas: Option<&Canvas>, @@ -219,6 +222,7 @@ pub fn render( surface_id: Option, shadow: Option<&Paint>, blur: Option<&ImageFilter>, + fill_inset: Option, ) { render_with_bounds_outset( render_state, @@ -229,6 +233,7 @@ pub fn render( shadow, blur, 0.0, + fill_inset, ); } @@ -238,6 +243,7 @@ fn render_text_on_canvas( paragraph_builders: &mut [Vec], shadow: Option<&Paint>, blur: Option<&ImageFilter>, + fill_inset: Option, ) { if let Some(blur_filter) = blur { let mut blur_paint = Paint::default(); @@ -251,6 +257,17 @@ fn render_text_on_canvas( canvas.save_layer(&layer_rec); draw_text(canvas, shape, paragraph_builders); canvas.restore(); + } else if let Some(eps) = fill_inset.filter(|&e| e > 0.0) { + if let Some(erode) = skia_safe::image_filters::erode((eps, eps), None, None) { + let mut layer_paint = Paint::default(); + layer_paint.set_image_filter(erode); + let layer_rec = SaveLayerRec::default().paint(&layer_paint); + canvas.save_layer(&layer_rec); + draw_text(canvas, shape, paragraph_builders); + canvas.restore(); + } else { + draw_text(canvas, shape, paragraph_builders); + } } else { draw_text(canvas, shape, paragraph_builders); } diff --git a/render-wasm/src/render/text_editor.rs b/render-wasm/src/render/text_editor.rs index be37ce627d..8d4243f80b 100644 --- a/render-wasm/src/render/text_editor.rs +++ b/render-wasm/src/render/text_editor.rs @@ -45,7 +45,11 @@ fn render_cursor( paint.set_color(editor_state.theme.cursor_color); paint.set_anti_alias(true); + let shape_matrix = shape.get_matrix(); + canvas.save(); + canvas.concat(&shape_matrix); canvas.draw_rect(rect, &paint); + canvas.restore(); } fn render_selection( @@ -65,9 +69,14 @@ fn render_selection( paint.set_blend_mode(BlendMode::Multiply); paint.set_color(editor_state.theme.selection_color); paint.set_anti_alias(true); + + let shape_matrix = shape.get_matrix(); + canvas.save(); + canvas.concat(&shape_matrix); for rect in rects { canvas.draw_rect(rect, &paint); } + canvas.restore(); } fn vertical_align_offset( @@ -99,8 +108,6 @@ fn calculate_cursor_rect( return None; } - let selrect = shape.selrect(); - let mut y_offset = vertical_align_offset(shape, &layout_paragraphs); for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() { if idx == cursor.paragraph { @@ -157,8 +164,8 @@ fn calculate_cursor_rect( }; return Some(Rect::from_xywh( - selrect.x() + cursor_x, - selrect.y() + y_offset, + cursor_x, + y_offset, editor_state.theme.cursor_width, cursor_height, )); @@ -182,7 +189,6 @@ fn calculate_selection_rects( let paragraphs = text_content.paragraphs(); let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect(); - let selrect = shape.selrect(); let mut y_offset = vertical_align_offset(shape, &layout_paragraphs); for (para_idx, laid_out_para) in layout_paragraphs.iter().enumerate() { @@ -225,8 +231,8 @@ fn calculate_selection_rects( for text_box in text_boxes { let r = text_box.rect; rects.push(Rect::from_xywh( - selrect.x() + r.left(), - selrect.y() + y_offset + r.top(), + r.left(), + y_offset + r.top(), r.width(), r.height(), )); diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 8967b6ee49..8e7e1e7c99 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -258,6 +258,18 @@ pub fn all_with_ancestors( } impl Shape { + pub fn get_relative_point( + point: &Point, + view_matrix: &Matrix, + shape_matrix: &Matrix, + ) -> Option { + let inv_view_matrix = view_matrix.invert()?; + let inv_shape_matrix = shape_matrix.invert()?; + let transform_matrix: Matrix = Matrix::concat(&inv_shape_matrix, &inv_view_matrix); + let shape_relative_point = transform_matrix.map_point(*point); + Some(shape_relative_point) + } + pub fn new(id: Uuid) -> Self { Self { id, @@ -1336,7 +1348,7 @@ impl Shape { if let Some(path) = self.shape_type.path() { let mut skia_path = path.to_skia_path(); if let Some(path_transform) = self.to_path_transform() { - skia_path.transform(&path_transform); + skia_path = skia_path.make_transform(&path_transform); } if let Some(svg_attrs) = &self.svg_attrs { if svg_attrs.fill_rule == FillRule::Evenodd { diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index 443669c121..cf8a930894 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -51,10 +51,10 @@ impl Gradient { rect.left + self.end.0 * rect.width(), rect.top + self.end.1 * rect.height(), ); - skia::shader::Shader::linear_gradient( + skia::gradient_shader::linear( (start, end), self.colors.as_slice(), - self.offsets.as_slice(), + Some(self.offsets.as_slice()), skia::TileMode::Clamp, None, None, @@ -83,11 +83,11 @@ impl Gradient { transform.pre_scale((self.width * rect.width() / rect.height(), 1.), None); transform.pre_translate((-center.x, -center.y)); - skia::shader::Shader::radial_gradient( + skia::gradient_shader::radial( center, distance, self.colors.as_slice(), - self.offsets.as_slice(), + Some(self.offsets.as_slice()), skia::TileMode::Clamp, None, Some(&transform), diff --git a/render-wasm/src/shapes/paths.rs b/render-wasm/src/shapes/paths.rs index dfdc06ae01..d6d6a8d1ef 100644 --- a/render-wasm/src/shapes/paths.rs +++ b/render-wasm/src/shapes/paths.rs @@ -29,40 +29,28 @@ impl Default for Path { } } -fn to_verb(v: u8) -> skia::path::Verb { - match v { - 0 => skia::path::Verb::Move, - 1 => skia::path::Verb::Line, - 2 => skia::path::Verb::Quad, - 3 => skia::path::Verb::Conic, - 4 => skia::path::Verb::Cubic, - 5 => skia::path::Verb::Close, - _ => skia::path::Verb::Done, - } -} - impl Path { pub fn new(segments: Vec) -> Self { - let mut skia_path = skia::Path::new(); + let mut pb = skia::PathBuilder::new(); let mut start = None; for segment in segments.iter() { let destination = match *segment { Segment::MoveTo(xy) => { start = Some(xy); - skia_path.move_to(xy); + pb.move_to(xy); None } Segment::LineTo(xy) => { - skia_path.line_to(xy); + pb.line_to(xy); Some(xy) } Segment::CurveTo((c1, c2, xy)) => { - skia_path.cubic_to(c1, c2, xy); + pb.cubic_to(c1, c2, xy); Some(xy) } Segment::Close => { - skia_path.close(); + pb.close(); None } }; @@ -71,11 +59,12 @@ impl Path { if math::is_close_to(destination.0, start.0) && math::is_close_to(destination.1, start.1) { - skia_path.close(); + pb.close(); } } } + let skia_path = pb.detach(); let open = subpaths::is_open_path(&segments); Self { @@ -86,38 +75,31 @@ impl Path { } pub fn from_skia_path(path: skia::Path) -> Self { - let nv = path.count_verbs(); - let mut verbs = vec![0; nv]; - path.get_verbs(&mut verbs); - - let np = path.count_points(); - let mut points = Vec::with_capacity(np); - points.resize(np, skia::Point::default()); - path.get_points(&mut points); + let verbs = path.verbs(); + let points = path.points(); let mut segments = Vec::new(); let mut current_point = 0; for verb in verbs { - let verb = to_verb(verb); match verb { - skia::path::Verb::Move => { + skia::PathVerb::Move => { let p = points[current_point]; segments.push(Segment::MoveTo((p.x, p.y))); current_point += 1; } - skia::path::Verb::Line => { + skia::PathVerb::Line => { let p = points[current_point]; segments.push(Segment::LineTo((p.x, p.y))); current_point += 1; } - skia::path::Verb::Quad => { + skia::PathVerb::Quad => { let p1 = points[current_point]; let p2 = points[current_point + 1]; segments.push(Segment::CurveTo(((p1.x, p1.y), (p1.x, p1.y), (p2.x, p2.y)))); current_point += 2; } - skia::path::Verb::Conic => { + skia::PathVerb::Conic => { // TODO: There is no way currently to access the conic weight // to transform this correctly let p1 = points[current_point]; @@ -125,17 +107,14 @@ impl Path { segments.push(Segment::CurveTo(((p1.x, p1.y), (p1.x, p1.y), (p2.x, p2.y)))); current_point += 2; } - skia::path::Verb::Cubic => { + skia::PathVerb::Cubic => { let p1 = points[current_point]; let p2 = points[current_point + 1]; let p3 = points[current_point + 2]; segments.push(Segment::CurveTo(((p1.x, p1.y), (p2.x, p2.y), (p3.x, p3.y)))); current_point += 3; } - skia::path::Verb::Close => { - segments.push(Segment::Close); - } - skia::path::Verb::Done => { + skia::PathVerb::Close => { segments.push(Segment::Close); } } @@ -184,7 +163,7 @@ impl Path { _ => {} }); - self.skia_path.transform(mtx); + self.skia_path = self.skia_path.make_transform(mtx); } pub fn segments(&self) -> &Vec { diff --git a/render-wasm/src/shapes/strokes.rs b/render-wasm/src/shapes/strokes.rs index 599cd83f3d..426d5939c3 100644 --- a/render-wasm/src/shapes/strokes.rs +++ b/render-wasm/src/shapes/strokes.rs @@ -225,13 +225,16 @@ impl Stroke { if self.style != StrokeStyle::Solid { let path_effect = match self.style { StrokeStyle::Dotted => { - let mut circle_path = skia::Path::new(); let width = match self.kind { StrokeKind::Inner => self.width, StrokeKind::Center => self.width / 2.0, StrokeKind::Outer => self.width, }; - circle_path.add_circle((0.0, 0.0), width, None); + let circle_path = { + let mut pb = skia::PathBuilder::new(); + pb.add_circle((0.0, 0.0), width, None); + pb.detach() + }; let advance = self.width + 5.0; skia::PathEffect::path_1d( &circle_path, diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index feaab039fb..1cad369768 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -112,12 +112,15 @@ impl TextContentSize { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Copy, Clone)] pub struct TextPositionWithAffinity { + #[allow(dead_code)] pub position_with_affinity: PositionWithAffinity, pub paragraph: i32, #[allow(dead_code)] pub span: i32, + #[allow(dead_code)] + pub span_relative_offset: i32, pub offset: i32, } @@ -126,12 +129,14 @@ impl TextPositionWithAffinity { position_with_affinity: PositionWithAffinity, paragraph: i32, span: i32, + span_relative_offset: i32, offset: i32, ) -> Self { Self { position_with_affinity, paragraph, span, + span_relative_offset, offset, } } @@ -421,7 +426,10 @@ impl TextContent { self.bounds = Rect::from_ltrb(p1.x, p1.y, p2.x, p2.y); } - pub fn get_caret_position_at(&self, point: &Point) -> Option { + pub fn get_caret_position_from_shape_coords( + &self, + point: &Point, + ) -> Option { let mut offset_y = 0.0; let layout_paragraphs = self.layout.paragraphs.iter().flatten(); @@ -487,6 +495,7 @@ impl TextContent { paragraph_index, span_index, span_offset, + position_with_affinity.position, )); } } @@ -509,12 +518,23 @@ impl TextContent { 0, // paragraph 0 0, // span 0 0, // offset 0 + 0, )); } None } + pub fn get_caret_position_from_screen_coords( + &self, + point: &Point, + view_matrix: &Matrix, + shape_matrix: &Matrix, + ) -> Option { + let shape_rel_point = Shape::get_relative_point(point, view_matrix, shape_matrix)?; + self.get_caret_position_from_shape_coords(&shape_rel_point) + } + /// Builds the ParagraphBuilders necessary to render /// this text. pub fn paragraph_builder_group_from_text( diff --git a/render-wasm/src/shapes/text_paths.rs b/render-wasm/src/shapes/text_paths.rs index e5b155dbd2..238207152b 100644 --- a/render-wasm/src/shapes/text_paths.rs +++ b/render-wasm/src/shapes/text_paths.rs @@ -101,7 +101,6 @@ impl TextPaths { if let Some((text_blob_path, text_blob_bounds)) = Self::get_text_blob_path(span_text, font, blob_offset_x, blob_offset_y) { - let mut text_path = text_blob_path.clone(); let text_width = font.measure_text(span_text, None).0; let decoration = style_metric.text_style.decoration(); @@ -111,16 +110,20 @@ impl TextPaths { let blob_top = blob_offset_y; let blob_height = text_blob_bounds.height(); - if let Some(decoration_rect) = self.calculate_text_decoration_rect( - decoration.ty, - font_metrics, - blob_left, - blob_top, - text_width, - blob_height, - ) { - text_path.add_rect(decoration_rect, None); - } + let text_path = { + let mut pb = skia::PathBuilder::new_path(&text_blob_path); + if let Some(decoration_rect) = self.calculate_text_decoration_rect( + decoration.ty, + font_metrics, + blob_left, + blob_top, + text_width, + blob_height, + ) { + pb.add_rect(decoration_rect, None, None); + } + pb.detach() + }; let mut paint = style_metric.text_style.foreground(); paint.set_anti_alias(antialias); diff --git a/render-wasm/src/state/text_editor.rs b/render-wasm/src/state/text_editor.rs index d7474cc92f..8c384db2b3 100644 --- a/render-wasm/src/state/text_editor.rs +++ b/render-wasm/src/state/text_editor.rs @@ -1,8 +1,11 @@ #![allow(dead_code)] -use crate::shapes::TextPositionWithAffinity; +use crate::shapes::{TextContent, TextPositionWithAffinity}; use crate::uuid::Uuid; -use skia_safe::Color; +use skia_safe::{ + textlayout::{Affinity, PositionWithAffinity}, + Color, +}; /// Cursor position within text content. /// Uses character offsets for precise positioning. @@ -122,6 +125,9 @@ pub struct TextEditorState { pub theme: TextEditorTheme, pub selection: TextSelection, pub is_active: bool, + // This property indicates that we've started + // selecting something with the pointer. + pub is_pointer_selection_active: bool, pub active_shape_id: Option, pub cursor_visible: bool, pub last_blink_time: f64, @@ -138,6 +144,7 @@ impl TextEditorState { }, selection: TextSelection::new(), is_active: false, + is_pointer_selection_active: false, active_shape_id: None, cursor_visible: true, last_blink_time: 0.0, @@ -151,6 +158,7 @@ impl TextEditorState { self.cursor_visible = true; self.last_blink_time = 0.0; self.selection = TextSelection::new(); + self.is_pointer_selection_active = false; self.pending_events.clear(); } @@ -158,7 +166,65 @@ impl TextEditorState { self.is_active = false; self.active_shape_id = None; self.cursor_visible = false; + self.is_pointer_selection_active = false; self.pending_events.clear(); + self.reset_blink(); + } + + pub fn start_pointer_selection(&mut self) -> bool { + if self.is_pointer_selection_active { + return false; + } + self.is_pointer_selection_active = true; + true + } + + pub fn stop_pointer_selection(&mut self) -> bool { + if !self.is_pointer_selection_active { + return false; + } + self.is_pointer_selection_active = false; + true + } + + pub fn select_all(&mut self, content: &TextContent) -> bool { + self.is_pointer_selection_active = false; + self.set_caret_from_position(TextPositionWithAffinity::new( + PositionWithAffinity { + position: 0, + affinity: Affinity::Downstream, + }, + 0, + 0, + 0, + 0, + )); + let num_paragraphs = (content.paragraphs().len() - 1) as i32; + let Some(last_paragraph) = content.paragraphs().last() else { + return false; + }; + let num_spans = (last_paragraph.children().len() - 1) as i32; + let Some(last_text_span) = last_paragraph.children().last() else { + return false; + }; + let mut offset = 0; + for span in last_paragraph.children() { + offset += span.text.len(); + } + self.extend_selection_from_position(TextPositionWithAffinity::new( + PositionWithAffinity { + position: offset as i32, + affinity: Affinity::Upstream, + }, + num_paragraphs, + num_spans, + last_text_span.text.len() as i32, + offset as i32, + )); + self.reset_blink(); + self.push_event(crate::state::EditorEvent::SelectionChanged); + + true } pub fn set_caret_from_position(&mut self, position: TextPositionWithAffinity) { diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index e4617575aa..ab4b14541e 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -1,16 +1,13 @@ use macros::ToJs; use super::{fills::RawFillData, fonts::RawFontStyle}; -use crate::math::{Matrix, Point}; + use crate::mem::{self, SerializableResult}; use crate::shapes::{ self, GrowType, Shape, TextAlign, TextDecoration, TextDirection, TextTransform, Type, }; use crate::utils::{uuid_from_u32, uuid_from_u32_quartet}; -use crate::{ - with_current_shape, with_current_shape_mut, with_state, with_state_mut, - with_state_mut_current_shape, STATE, -}; +use crate::{with_current_shape, with_current_shape_mut, with_state, with_state_mut, STATE}; const RAW_SPAN_DATA_SIZE: usize = std::mem::size_of::(); const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::(); @@ -388,32 +385,6 @@ pub extern "C" fn update_shape_text_layout_for(a: u32, b: u32, c: u32, d: u32) { }); } -#[no_mangle] -pub extern "C" fn get_caret_position_at(x: f32, y: f32) -> i32 { - with_state_mut_current_shape!(state, |shape: &Shape| { - if let Type::Text(text_content) = &shape.shape_type { - let mut matrix = Matrix::new_identity(); - let shape_matrix = shape.get_concatenated_matrix(&state.shapes); - let view_matrix = state.render_state.viewbox.get_matrix(); - if let Some(inv_view_matrix) = view_matrix.invert() { - matrix.post_concat(&inv_view_matrix); - matrix.post_concat(&shape_matrix); - - let mapped_point = matrix.map_point(Point::new(x, y)); - - if let Some(position_with_affinity) = - text_content.get_caret_position_at(&mapped_point) - { - return position_with_affinity.position_with_affinity.position; - } - } - } else { - panic!("Trying to get caret position of a shape that it's not a text shape"); - } - }); - -1 -} - const RAW_POSITION_DATA_SIZE: usize = size_of::(); impl From<[u8; RAW_POSITION_DATA_SIZE]> for shapes::PositionData { diff --git a/render-wasm/src/wasm/text_editor.rs b/render-wasm/src/wasm/text_editor.rs index 37758f5bb1..771ee82627 100644 --- a/render-wasm/src/wasm/text_editor.rs +++ b/render-wasm/src/wasm/text_editor.rs @@ -1,5 +1,3 @@ -use macros::ToJs; - use crate::math::{Matrix, Point, Rect}; use crate::mem; use crate::shapes::{Paragraph, Shape, TextContent, Type, VerticalAlign}; @@ -7,6 +5,7 @@ use crate::state::{TextCursor, TextSelection}; use crate::utils::uuid_from_u32_quartet; use crate::utils::uuid_to_u32_quartet; use crate::{with_state, with_state_mut, STATE}; +use macros::ToJs; #[derive(PartialEq, ToJs)] #[repr(u8)] @@ -54,6 +53,17 @@ pub extern "C" fn text_editor_is_active() -> bool { with_state!(state, { state.text_editor_state.is_active }) } +#[no_mangle] +pub extern "C" fn text_editor_is_active_with_id(a: u32, b: u32, c: u32, d: u32) -> bool { + with_state!(state, { + let shape_id = uuid_from_u32_quartet(a, b, c, d); + let Some(active_shape_id) = state.text_editor_state.active_shape_id else { + return false; + }; + state.text_editor_state.is_active && active_shape_id == shape_id + }) +} + #[no_mangle] pub extern "C" fn text_editor_get_active_shape_id(buffer_ptr: *mut u32) { with_state!(state, { @@ -70,45 +80,25 @@ pub extern "C" fn text_editor_get_active_shape_id(buffer_ptr: *mut u32) { } #[no_mangle] -pub extern "C" fn text_editor_select_all() { +pub extern "C" fn text_editor_select_all() -> bool { with_state_mut!(state, { if !state.text_editor_state.is_active { - return; + return false; } let Some(shape_id) = state.text_editor_state.active_shape_id else { - return; + return false; }; let Some(shape) = state.shapes.get(&shape_id) else { - return; + return false; }; let Type::Text(text_content) = &shape.shape_type else { - return; + return false; }; - - let paragraphs = text_content.paragraphs(); - if paragraphs.is_empty() { - return; - } - - let last_para_idx = paragraphs.len() - 1; - let last_para = ¶graphs[last_para_idx]; - let total_chars: usize = last_para - .children() - .iter() - .map(|span| span.text.chars().count()) - .sum(); - - use crate::state::TextCursor; - state.text_editor_state.selection.anchor = TextCursor::new(0, 0); - state.text_editor_state.selection.focus = TextCursor::new(last_para_idx, total_chars); - state.text_editor_state.reset_blink(); - state - .text_editor_state - .push_event(crate::state::EditorEvent::SelectionChanged); - }); + state.text_editor_state.select_all(text_content) + }) } #[no_mangle] @@ -121,146 +111,127 @@ pub extern "C" fn text_editor_poll_event() -> u8 { // ============================================================================ #[no_mangle] -pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) { +pub extern "C" fn text_editor_pointer_down(x: f32, y: f32) { with_state_mut!(state, { if !state.text_editor_state.is_active { return; } - let Some(shape_id) = state.text_editor_state.active_shape_id else { return; }; - - let (shape_matrix, view_matrix, selrect, vertical_align) = { - let Some(shape) = state.shapes.get(&shape_id) else { - return; - }; - ( - shape.get_concatenated_matrix(&state.shapes), - state.render_state.viewbox.get_matrix(), - shape.selrect(), - shape.vertical_align(), - ) - }; - - let Some(inv_view_matrix) = view_matrix.invert() else { + let Some(shape) = state.shapes.get(&shape_id) else { return; }; - - let Some(inv_shape_matrix) = shape_matrix.invert() else { + let Type::Text(text_content) = &shape.shape_type else { return; }; - - let mut matrix = Matrix::new_identity(); - matrix.post_concat(&inv_view_matrix); - matrix.post_concat(&inv_shape_matrix); - - let mapped_point = matrix.map_point(Point::new(x, y)); - - let Some(shape) = state.shapes.get_mut(&shape_id) else { - return; - }; - - let Type::Text(text_content) = &mut shape.shape_type else { - return; - }; - - if text_content.layout.paragraphs.is_empty() && !text_content.paragraphs().is_empty() { - let bounds = text_content.bounds; - text_content.update_layout(bounds); - } - - // Calculate vertical alignment offset (same as in render/text_editor.rs) - let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect(); - let total_height: f32 = layout_paragraphs.iter().map(|p| p.height()).sum(); - let vertical_offset = match vertical_align { - crate::shapes::VerticalAlign::Center => (selrect.height() - total_height) / 2.0, - crate::shapes::VerticalAlign::Bottom => selrect.height() - total_height, - _ => 0.0, - }; - - // Adjust point: subtract selrect offset and vertical alignment - // The text layout expects coordinates where (0, 0) is the top-left of the text content - let adjusted_point = Point::new( - mapped_point.x - selrect.x(), - mapped_point.y - selrect.y() - vertical_offset, - ); - - if let Some(position) = text_content.get_caret_position_at(&adjusted_point) { + let point = Point::new(x, y); + let view_matrix: Matrix = state.render_state.viewbox.get_matrix(); + let shape_matrix = shape.get_matrix(); + state.text_editor_state.start_pointer_selection(); + if let Some(position) = + text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix) + { state.text_editor_state.set_caret_from_position(position); } }); } #[no_mangle] -pub extern "C" fn text_editor_extend_selection_to_point(x: f32, y: f32) { +pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) { + with_state_mut!(state, { + if !state.text_editor_state.is_active { + return; + } + let view_matrix: Matrix = state.render_state.viewbox.get_matrix(); + let point = Point::new(x, y); + let Some(shape_id) = state.text_editor_state.active_shape_id else { + return; + }; + let Some(shape) = state.shapes.get(&shape_id) else { + return; + }; + let shape_matrix = shape.get_matrix(); + let Some(_shape_rel_point) = Shape::get_relative_point(&point, &view_matrix, &shape_matrix) + else { + return; + }; + if !state.text_editor_state.is_pointer_selection_active { + return; + } + let Type::Text(text_content) = &shape.shape_type else { + return; + }; + + if let Some(position) = + text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix) + { + state + .text_editor_state + .extend_selection_from_position(position); + } + }); +} + +#[no_mangle] +pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) { + with_state_mut!(state, { + if !state.text_editor_state.is_active { + return; + } + let view_matrix: Matrix = state.render_state.viewbox.get_matrix(); + let point = Point::new(x, y); + let Some(shape_id) = state.text_editor_state.active_shape_id else { + return; + }; + let Some(shape) = state.shapes.get(&shape_id) else { + return; + }; + let shape_matrix = shape.get_matrix(); + let Some(_shape_rel_point) = Shape::get_relative_point(&point, &view_matrix, &shape_matrix) + else { + return; + }; + if !state.text_editor_state.is_pointer_selection_active { + return; + } + let Type::Text(text_content) = &shape.shape_type else { + return; + }; + if let Some(position) = + text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix) + { + state + .text_editor_state + .extend_selection_from_position(position); + } + state.text_editor_state.stop_pointer_selection(); + }); +} + +#[no_mangle] +pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) { with_state_mut!(state, { if !state.text_editor_state.is_active { return; } + let view_matrix: Matrix = state.render_state.viewbox.get_matrix(); + let point = Point::new(x, y); let Some(shape_id) = state.text_editor_state.active_shape_id else { return; }; - - let (shape_matrix, view_matrix, selrect, vertical_align) = { - let Some(shape) = state.shapes.get(&shape_id) else { - return; - }; - ( - shape.get_concatenated_matrix(&state.shapes), - state.render_state.viewbox.get_matrix(), - shape.selrect(), - shape.vertical_align(), - ) - }; - - let Some(inv_view_matrix) = view_matrix.invert() else { + let Some(shape) = state.shapes.get(&shape_id) else { return; }; - - let Some(inv_shape_matrix) = shape_matrix.invert() else { + let shape_matrix = shape.get_matrix(); + let Type::Text(text_content) = &shape.shape_type else { return; }; - - let mut matrix = Matrix::new_identity(); - matrix.post_concat(&inv_view_matrix); - matrix.post_concat(&inv_shape_matrix); - - let mapped_point = matrix.map_point(Point::new(x, y)); - - let Some(shape) = state.shapes.get_mut(&shape_id) else { - return; - }; - - let Type::Text(text_content) = &mut shape.shape_type else { - return; - }; - - if text_content.layout.paragraphs.is_empty() && !text_content.paragraphs().is_empty() { - let bounds = text_content.bounds; - text_content.update_layout(bounds); - } - - // Calculate vertical alignment offset (same as in render/text_editor.rs) - let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect(); - let total_height: f32 = layout_paragraphs.iter().map(|p| p.height()).sum(); - let vertical_offset = match vertical_align { - crate::shapes::VerticalAlign::Center => (selrect.height() - total_height) / 2.0, - crate::shapes::VerticalAlign::Bottom => selrect.height() - total_height, - _ => 0.0, - }; - - // Adjust point: subtract selrect offset and vertical alignment - let adjusted_point = Point::new( - mapped_point.x - selrect.x(), - mapped_point.y - selrect.y() - vertical_offset, - ); - - if let Some(position) = text_content.get_caret_position_at(&adjusted_point) { - state - .text_editor_state - .extend_selection_from_position(position); + if let Some(position) = + text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix) + { + state.text_editor_state.set_caret_from_position(position); } }); } diff --git a/render-wasm/test b/render-wasm/test index 85d5547d4a..f416e6c6bb 100755 --- a/render-wasm/test +++ b/render-wasm/test @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -x -export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.87.0/skia-binaries-e551f334ad5cbdf43abf-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp.tar.gz"} +export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp.tar.gz"} export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"x86_64-unknown-linux-gnu"}; _SCRIPT_DIR=$(dirname $0); diff --git a/render-wasm/watch_test b/render-wasm/watch_test index 5f1346c333..798eb84bf0 100755 --- a/render-wasm/watch_test +++ b/render-wasm/watch_test @@ -1,7 +1,7 @@ #!/usr/bin/env bash _SCRIPT_DIR=$(dirname $0); -export SKIA_BINARIES_URL="https://github.com/penpot/skia-binaries/releases/download/0.87.0/skia-binaries-e551f334ad5cbdf43abf-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp.tar.gz" +export SKIA_BINARIES_URL="https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp.tar.gz" pushd $_SCRIPT_DIR; cargo watch -x "test --bin render_wasm -- --show-output"