diff --git a/frontend/playwright/data/render-wasm/get-file-text-flatten.json b/frontend/playwright/data/render-wasm/get-file-text-flatten.json new file mode 100644 index 0000000000..c93e8869c3 --- /dev/null +++ b/frontend/playwright/data/render-wasm/get-file-text-flatten.json @@ -0,0 +1,1332 @@ +{ + "~:features": { + "~#set": [ + "fdata/path-data", + "plugins/runtime", + "design-tokens/v1", + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "render-wasm/v1", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:team-id": "~u1091e979-bbec-8194-8005-f7aa420b5660", + "~: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": "Text flatten to path", + "~:revn": 7, + "~:modified-at": "~m1749629891313", + "~:vern": 0, + "~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c72", + "~: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", + "0002-clean-shape-interactions", + "0003-fix-root-shape", + "0003-convert-path-content", + "0004-clean-shadow-and-colors", + "0005-deprecate-image-type", + "0006-fix-old-texts-fills", + "0007-clear-invalid-strokes-and-fills-v2", + "0008-fix-library-colors-opacity" + ] + }, + "~:version": 67, + "~:project-id": "~u1091e979-bbec-8194-8005-f7aa420b8b07", + "~:created-at": "~m1749629823499", + "~:data": { + "~:pages": [ + "~u3b0d758a-8c9d-8013-8006-52c8337e5c73" + ], + "~:pages-index": { + "~u3b0d758a-8c9d-8013-8006-52c8337e5c73": { + "~:objects": { + "~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 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0.0, + "~:y": 0.01 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~: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, + "~: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": [ + "~uaaaaaaaa-0000-4000-8000-000000000000", + "~uaaaaaaaa-0000-4000-8000-000000000001", + "~uaaaaaaaa-0000-4000-8000-000000000010", + "~uaaaaaaaa-0000-4000-8000-000000000011", + "~uaaaaaaaa-0000-4000-8000-000000000020", + "~uaaaaaaaa-0000-4000-8000-000000000021" + ] + } + }, + "~uaaaaaaaa-0000-4000-8000-000000000000": { + "~#shape": { + "~:id": "~uaaaaaaaa-0000-4000-8000-000000000000", + "~:name": "Flat spacing", + "~:type": "~:text", + "~:x": 120, + "~:y": 120, + "~:width": 400, + "~:height": 70, + "~:rotation": 0, + "~:selrect": { + "~#rect": { + "~:x": 120, + "~:y": 120, + "~:width": 400, + "~:height": 70, + "~:x1": 120, + "~:y1": 120, + "~:x2": 520, + "~:y2": 190 + } + }, + "~:points": [ + { + "~#point": { + "~:x": 120, + "~:y": 120 + } + }, + { + "~#point": { + "~:x": 520, + "~:y": 120 + } + }, + { + "~#point": { + "~:x": 520, + "~:y": 190 + } + }, + { + "~#point": { + "~:x": 120, + "~:y": 190 + } + } + ], + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:flip-x": null, + "~:flip-y": null, + "~:hide-in-viewer": false, + "~:grow-type": "~:fixed", + "~:layout-item-h-sizing": "~:fix", + "~:layout-item-v-sizing": "~:fix", + "~:content": { + "~:type": "root", + "~:key": "aaaaaaaaroot", + "~:vertical-align": "top", + "~: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": "aaaaaaaa-p0s", + "~:font-size": "44", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "14", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro", + "~:text": "Spacing" + } + ], + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:key": "aaaaaaaa-p0", + "~:font-size": "44", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:type": "paragraph", + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "14", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro" + } + ] + } + ] + } + } + }, + "~uaaaaaaaa-0000-4000-8000-000000000001": { + "~#shape": { + "~:id": "~uaaaaaaaa-0000-4000-8000-000000000001", + "~:name": "Ref spacing", + "~:type": "~:text", + "~:x": 620, + "~:y": 120, + "~:width": 400, + "~:height": 70, + "~:rotation": 0, + "~:selrect": { + "~#rect": { + "~:x": 620, + "~:y": 120, + "~:width": 400, + "~:height": 70, + "~:x1": 620, + "~:y1": 120, + "~:x2": 1020, + "~:y2": 190 + } + }, + "~:points": [ + { + "~#point": { + "~:x": 620, + "~:y": 120 + } + }, + { + "~#point": { + "~:x": 1020, + "~:y": 120 + } + }, + { + "~#point": { + "~:x": 1020, + "~:y": 190 + } + }, + { + "~#point": { + "~:x": 620, + "~:y": 190 + } + } + ], + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:flip-x": null, + "~:flip-y": null, + "~:hide-in-viewer": false, + "~:grow-type": "~:fixed", + "~:layout-item-h-sizing": "~:fix", + "~:layout-item-v-sizing": "~:fix", + "~:content": { + "~:type": "root", + "~:key": "aaaaaaaaroot", + "~:vertical-align": "top", + "~: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": "aaaaaaaa-p0s", + "~:font-size": "44", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "14", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro", + "~:text": "Spacing" + } + ], + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:key": "aaaaaaaa-p0", + "~:font-size": "44", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:type": "paragraph", + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "14", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro" + } + ] + } + ] + } + } + }, + "~uaaaaaaaa-0000-4000-8000-000000000010": { + "~#shape": { + "~:id": "~uaaaaaaaa-0000-4000-8000-000000000010", + "~:name": "Flat paragraph", + "~:type": "~:text", + "~:x": 120, + "~:y": 230, + "~:width": 400, + "~:height": 150, + "~:rotation": 0, + "~:selrect": { + "~#rect": { + "~:x": 120, + "~:y": 230, + "~:width": 400, + "~:height": 150, + "~:x1": 120, + "~:y1": 230, + "~:x2": 520, + "~:y2": 380 + } + }, + "~:points": [ + { + "~#point": { + "~:x": 120, + "~:y": 230 + } + }, + { + "~#point": { + "~:x": 520, + "~:y": 230 + } + }, + { + "~#point": { + "~:x": 520, + "~:y": 380 + } + }, + { + "~#point": { + "~:x": 120, + "~:y": 380 + } + } + ], + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:flip-x": null, + "~:flip-y": null, + "~:hide-in-viewer": false, + "~:grow-type": "~:fixed", + "~:layout-item-h-sizing": "~:fix", + "~:layout-item-v-sizing": "~:fix", + "~:content": { + "~:type": "root", + "~:key": "aaaaaaaaroot", + "~:vertical-align": "top", + "~: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": "aaaaaaaa-p0s", + "~:font-size": "32", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro", + "~:text": "First" + } + ], + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:key": "aaaaaaaa-p0", + "~:font-size": "32", + "~: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": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro" + }, + { + "~: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": "aaaaaaaa-p1s", + "~:font-size": "32", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro", + "~:text": "Second" + } + ], + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:key": "aaaaaaaa-p1", + "~:font-size": "32", + "~: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": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro" + }, + { + "~: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": "aaaaaaaa-p2s", + "~:font-size": "32", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro", + "~:text": "Third" + } + ], + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:key": "aaaaaaaa-p2", + "~:font-size": "32", + "~: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": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro" + } + ] + } + ] + } + } + }, + "~uaaaaaaaa-0000-4000-8000-000000000011": { + "~#shape": { + "~:id": "~uaaaaaaaa-0000-4000-8000-000000000011", + "~:name": "Ref paragraph", + "~:type": "~:text", + "~:x": 620, + "~:y": 230, + "~:width": 400, + "~:height": 150, + "~:rotation": 0, + "~:selrect": { + "~#rect": { + "~:x": 620, + "~:y": 230, + "~:width": 400, + "~:height": 150, + "~:x1": 620, + "~:y1": 230, + "~:x2": 1020, + "~:y2": 380 + } + }, + "~:points": [ + { + "~#point": { + "~:x": 620, + "~:y": 230 + } + }, + { + "~#point": { + "~:x": 1020, + "~:y": 230 + } + }, + { + "~#point": { + "~:x": 1020, + "~:y": 380 + } + }, + { + "~#point": { + "~:x": 620, + "~:y": 380 + } + } + ], + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:flip-x": null, + "~:flip-y": null, + "~:hide-in-viewer": false, + "~:grow-type": "~:fixed", + "~:layout-item-h-sizing": "~:fix", + "~:layout-item-v-sizing": "~:fix", + "~:content": { + "~:type": "root", + "~:key": "aaaaaaaaroot", + "~:vertical-align": "top", + "~: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": "aaaaaaaa-p0s", + "~:font-size": "32", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro", + "~:text": "First" + } + ], + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:key": "aaaaaaaa-p0", + "~:font-size": "32", + "~: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": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro" + }, + { + "~: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": "aaaaaaaa-p1s", + "~:font-size": "32", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro", + "~:text": "Second" + } + ], + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:key": "aaaaaaaa-p1", + "~:font-size": "32", + "~: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": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro" + }, + { + "~: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": "aaaaaaaa-p2s", + "~:font-size": "32", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "0", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro", + "~:text": "Third" + } + ], + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:key": "aaaaaaaa-p2", + "~:font-size": "32", + "~: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": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro" + } + ] + } + ] + } + } + }, + "~uaaaaaaaa-0000-4000-8000-000000000020": { + "~#shape": { + "~:id": "~uaaaaaaaa-0000-4000-8000-000000000020", + "~:name": "Flat centered", + "~:type": "~:text", + "~:x": 120, + "~:y": 420, + "~:width": 400, + "~:height": 230, + "~:rotation": 0, + "~:selrect": { + "~#rect": { + "~:x": 120, + "~:y": 420, + "~:width": 400, + "~:height": 230, + "~:x1": 120, + "~:y1": 420, + "~:x2": 520, + "~:y2": 650 + } + }, + "~:points": [ + { + "~#point": { + "~:x": 120, + "~:y": 420 + } + }, + { + "~#point": { + "~:x": 520, + "~:y": 420 + } + }, + { + "~#point": { + "~:x": 520, + "~:y": 650 + } + }, + { + "~#point": { + "~:x": 120, + "~:y": 650 + } + } + ], + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:flip-x": null, + "~:flip-y": null, + "~:hide-in-viewer": false, + "~:grow-type": "~:fixed", + "~:layout-item-h-sizing": "~:fix", + "~:layout-item-v-sizing": "~:fix", + "~:content": { + "~:type": "root", + "~:key": "aaaaaaaaroot", + "~:vertical-align": "center", + "~: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": "aaaaaaaa-p0s", + "~:font-size": "32", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "8", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro", + "~:text": "Centered" + } + ], + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:key": "aaaaaaaa-p0", + "~:font-size": "32", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:type": "paragraph", + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "8", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro" + }, + { + "~: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": "aaaaaaaa-p1s", + "~:font-size": "32", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "8", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro", + "~:text": "and spaced" + } + ], + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:key": "aaaaaaaa-p1", + "~:font-size": "32", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:type": "paragraph", + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "8", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro" + } + ] + } + ] + } + } + }, + "~uaaaaaaaa-0000-4000-8000-000000000021": { + "~#shape": { + "~:id": "~uaaaaaaaa-0000-4000-8000-000000000021", + "~:name": "Ref centered", + "~:type": "~:text", + "~:x": 620, + "~:y": 420, + "~:width": 400, + "~:height": 230, + "~:rotation": 0, + "~:selrect": { + "~#rect": { + "~:x": 620, + "~:y": 420, + "~:width": 400, + "~:height": 230, + "~:x1": 620, + "~:y1": 420, + "~:x2": 1020, + "~:y2": 650 + } + }, + "~:points": [ + { + "~#point": { + "~:x": 620, + "~:y": 420 + } + }, + { + "~#point": { + "~:x": 1020, + "~:y": 420 + } + }, + { + "~#point": { + "~:x": 1020, + "~:y": 650 + } + }, + { + "~#point": { + "~:x": 620, + "~:y": 650 + } + } + ], + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:flip-x": null, + "~:flip-y": null, + "~:hide-in-viewer": false, + "~:grow-type": "~:fixed", + "~:layout-item-h-sizing": "~:fix", + "~:layout-item-v-sizing": "~:fix", + "~:content": { + "~:type": "root", + "~:key": "aaaaaaaaroot", + "~:vertical-align": "center", + "~: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": "aaaaaaaa-p0s", + "~:font-size": "32", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "8", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro", + "~:text": "Centered" + } + ], + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:key": "aaaaaaaa-p0", + "~:font-size": "32", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:type": "paragraph", + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "8", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro" + }, + { + "~: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": "aaaaaaaa-p1s", + "~:font-size": "32", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "8", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro", + "~:text": "and spaced" + } + ], + "~:typography-ref-id": null, + "~:text-transform": "none", + "~:text-align": "left", + "~:font-id": "sourcesanspro", + "~:key": "aaaaaaaa-p1", + "~:font-size": "32", + "~:font-weight": "400", + "~:typography-ref-file": null, + "~:text-direction": "ltr", + "~:type": "paragraph", + "~:font-variant-id": "regular", + "~:text-decoration": "none", + "~:letter-spacing": "8", + "~:fills": [ + { + "~:fill-color": "#000000", + "~:fill-opacity": 1 + } + ], + "~:font-family": "sourcesanspro" + } + ] + } + ] + } + } + } + }, + "~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c73", + "~:name": "Page 1" + } + }, + "~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c72", + "~:options": { + "~:components-v2": true, + "~:base-font-size": "16px" + } + } +} diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js b/frontend/playwright/ui/render-wasm-specs/texts.spec.js index b4d4c8373b..0af129f1d1 100644 --- a/frontend/playwright/ui/render-wasm-specs/texts.spec.js +++ b/frontend/playwright/ui/render-wasm-specs/texts.spec.js @@ -653,3 +653,32 @@ test("Renders background blur on text shapes", async ({ page }) => { await workspace.waitForFirstRenderWithoutUI(); await expect(workspace.canvas).toHaveScreenshot(); }); + +test("Flattens texts to paths", async ({ page }) => { + const workspace = new WasmWorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockGetFile("render-wasm/get-file-text-flatten.json"); + + await workspace.goToWorkspace({ + id: "3b0d758a-8c9d-8013-8006-52c8337e5c72", + pageId: "3b0d758a-8c9d-8013-8006-52c8337e5c73", + }); + await workspace.waitForFirstRender(); + + const flattenButton = workspace.page.getByRole("button", { + name: "Flatten", + exact: true, + }); + + for (const layer of ["Flat spacing", "Flat paragraph", "Flat centered"]) { + await workspace.clickLeafLayer(layer); + const renderCount = await workspace.getRenderCount(); + await flattenButton.click(); + await workspace.waitForNextRender(renderCount); + } + + await workspace.page.keyboard.press("Escape"); + await workspace.hideUI(); + + await expect(workspace.canvas).toHaveScreenshot({ timeout: 10000 }); +}); diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Flattens-texts-to-paths-1.png b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Flattens-texts-to-paths-1.png new file mode 100644 index 0000000000..3b5390e0a1 Binary files /dev/null and b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Flattens-texts-to-paths-1.png differ diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 0dd5870675..e3cad1058e 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -370,6 +370,12 @@ (.-type ^ShapeProxy self) delegate'))))) +(def ^:private base-fields + "Base fields of the `Shape` record this proxy stands in for: `cr/defrecord` + nils them on dissoc instead of removing them, and the schema requires them." + #{:name :x :y :width :height :rotation :selrect :points + :transform :transform-inverse :parent-id :frame-id :flip-x :flip-y}) + (defn- impl-dissoc [self k] (when shape/*shape-changes* @@ -385,7 +391,9 @@ nil (.-delegate ^ShapeProxy self)) (let [delegate (.-delegate ^ShapeProxy self) - delegate' (dissoc delegate k)] + delegate' (if (contains? base-fields k) + (assoc delegate k nil) + (dissoc delegate k))] (if (identical? delegate delegate') self (ShapeProxy. (.-id ^ShapeProxy self) diff --git a/render-wasm/src/render/fonts.rs b/render-wasm/src/render/fonts.rs index 5df5403bc4..78ae886064 100644 --- a/render-wasm/src/render/fonts.rs +++ b/render-wasm/src/render/fonts.rs @@ -132,10 +132,6 @@ impl FontStore { &self.fallback_fonts } - pub fn get_emoji_font(&self, _size: f32) -> Option { - None - } - pub fn set_source_url(&mut self, alias: &str, url: String) { if !url.is_empty() { self.source_urls.insert(alias.to_string(), url); diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index 6b8e50a4da..19e299b31b 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -3,9 +3,9 @@ use crate::{ error::Result, math::Rect, shapes::{ - add_text_with_tabs, calculate_text_layout_data, set_paint_fill, Paragraph as TextParagraph, - ParagraphBuilderGroup, ParagraphLayout, Stroke, StrokeKind, TextContent, - TextDecorationSegment, VerticalAlign, + add_text_with_tabs, calculate_text_layout_data, set_paint_fill, vertical_align_offset, + Paragraph as TextParagraph, ParagraphBuilderGroup, ParagraphLayout, Stroke, StrokeKind, + TextContent, TextDecorationSegment, }, utils::{get_fallback_fonts, get_font_collection}, }; @@ -379,11 +379,8 @@ fn paint_from_cached_layout(canvas: &Canvas, shape: &Shape, text_content: &TextC .filter_map(|group| group.first()) .map(|p| p.height()) .sum(); - let vertical_offset = match shape.vertical_align() { - VerticalAlign::Center => (selrect.height() - total_text_height) / 2.0, - VerticalAlign::Bottom => selrect.height() - total_text_height, - _ => 0.0, - }; + let vertical_offset = + vertical_align_offset(selrect.height(), total_text_height, shape.vertical_align()); let mut y_accum = base_y + vertical_offset; for (index, group) in paragraphs.iter().enumerate() { @@ -1235,32 +1232,6 @@ fn calculate_decoration_metrics( ) } -// How to use it? -// Type::Text(text_content) => { -// self.surfaces -// .apply_mut(&[SurfaceId::Fills, SurfaceId::Strokes], |s| { -// s.canvas().concat(&matrix); -// }); - -// let text_content = text_content.new_bounds(shape.selrect()); -// let paths = text_content.get_paths(antialias); - -// shadows::render_text_shadows(self, &shape, &paths, antialias); -// text::render(self, &paths, None, None); - -// for stroke in shape.visible_strokes().rev() { -// shadows::render_text_path_stroke_shadows( -// self, &shape, &paths, stroke, antialias, -// ); -// strokes::render_text_paths(self, &shape, stroke, &paths, None, None, antialias); -// shadows::render_text_path_stroke_inner_shadows( -// self, &shape, &paths, stroke, antialias, -// ); -// } - -// shadows::render_text_inner_shadows(self, &shape, &paths, antialias); -// } - #[cfg(test)] mod tests { use super::*; diff --git a/render-wasm/src/render/text_editor.rs b/render-wasm/src/render/text_editor.rs index 8154981e99..d1825aeca8 100644 --- a/render-wasm/src/render/text_editor.rs +++ b/render-wasm/src/render/text_editor.rs @@ -1,5 +1,5 @@ use crate::render::options::RenderOptions; -use crate::shapes::{Shape, TextContent, Type, VerticalAlign}; +use crate::shapes::{vertical_align_offset, Shape, TextContent, Type}; use crate::state::{TextEditorState, TextSelection}; use crate::view::Viewbox; use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle}; @@ -112,16 +112,16 @@ fn render_selection( canvas.restore(); } -fn vertical_align_offset( +fn paragraphs_vertical_offset( shape: &Shape, layout_paragraphs: &[&skia_safe::textlayout::Paragraph], ) -> f32 { let total_height: f32 = layout_paragraphs.iter().map(|p| p.height()).sum(); - match shape.vertical_align() { - VerticalAlign::Center => (shape.selrect().height() - total_height) / 2.0, - VerticalAlign::Bottom => shape.selrect().height() - total_height, - _ => 0.0, - } + vertical_align_offset( + shape.selrect().height(), + total_height, + shape.vertical_align(), + ) } fn calculate_cursor_rect( @@ -141,7 +141,7 @@ fn calculate_cursor_rect( return None; } - let mut y_offset = vertical_align_offset(shape, &layout_paragraphs); + let mut y_offset = paragraphs_vertical_offset(shape, &layout_paragraphs); for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() { if idx == cursor.paragraph { let char_pos = cursor.offset; @@ -236,7 +236,7 @@ fn calculate_selection_rects( let paragraphs = text_content.paragraphs(); let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect(); - let mut y_offset = vertical_align_offset(shape, &layout_paragraphs); + let mut y_offset = paragraphs_vertical_offset(shape, &layout_paragraphs); for (para_idx, laid_out_para) in layout_paragraphs.iter().enumerate() { let para_height = laid_out_para.height(); diff --git a/render-wasm/src/shapes/shape_to_path.rs b/render-wasm/src/shapes/shape_to_path.rs index a797e52837..25feba442f 100644 --- a/render-wasm/src/shapes/shape_to_path.rs +++ b/render-wasm/src/shapes/shape_to_path.rs @@ -264,9 +264,9 @@ impl ToPath for Shape { Type::SVGRaw(_) => Path::default(), Type::Text(ref text) => { - let text_paths = TextPaths::new(text.clone()); + let text_paths = TextPaths::new(text.new_bounds(self.selrect())); let mut result = Path::default(); - for (path, _) in text_paths.get_paths(true) { + for path in text_paths.get_paths(self.vertical_align()) { result = join_paths(result, Path::from_skia_path(path)); } diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index 8f6d8de294..8122ee21be 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -332,7 +332,7 @@ impl TextDecorationSegment { } } -fn vertical_align_offset(container_h: f32, content_h: f32, valign: VerticalAlign) -> f32 { +pub fn vertical_align_offset(container_h: f32, content_h: f32, valign: VerticalAlign) -> f32 { match valign { VerticalAlign::Center => (container_h - content_h) / 2.0, VerticalAlign::Bottom => container_h - content_h, @@ -1859,11 +1859,8 @@ pub fn calculate_text_layout_data( // 2. Position each built paragraph using the heights from step 1. let total_text_height: f32 = paragraph_heights.iter().sum(); - let vertical_offset = match shape.vertical_align() { - VerticalAlign::Center => (selrect_height - total_text_height) / 2.0, - VerticalAlign::Bottom => selrect_height - total_text_height, - _ => 0.0, - }; + let vertical_offset = + vertical_align_offset(selrect_height, total_text_height, shape.vertical_align()); let mut paragraph_layouts: Vec = Vec::new(); let mut y_accum = base_y + vertical_offset; for (i, group_paragraphs) in built_groups.into_iter().enumerate() { @@ -1971,6 +1968,39 @@ pub fn calculate_position_data( mod tests { use super::*; + #[test] + fn vertical_align_top_keeps_the_content_at_the_origin() { + assert_eq!(vertical_align_offset(200.0, 60.0, VerticalAlign::Top), 0.0); + } + + #[test] + fn vertical_align_center_takes_half_the_slack() { + assert_eq!( + vertical_align_offset(200.0, 60.0, VerticalAlign::Center), + 70.0 + ); + } + + #[test] + fn vertical_align_bottom_takes_all_the_slack() { + assert_eq!( + vertical_align_offset(200.0, 60.0, VerticalAlign::Bottom), + 140.0 + ); + } + + #[test] + fn vertical_align_offset_is_negative_when_content_overflows() { + assert_eq!( + vertical_align_offset(60.0, 200.0, VerticalAlign::Center), + -70.0 + ); + assert_eq!( + vertical_align_offset(60.0, 200.0, VerticalAlign::Bottom), + -140.0 + ); + } + #[test] fn capitalize_basic_words() { assert_eq!(capitalize_words("hello world"), "Hello World"); diff --git a/render-wasm/src/shapes/text_paths.rs b/render-wasm/src/shapes/text_paths.rs index c0d48c2e9e..777181a3a4 100644 --- a/render-wasm/src/shapes/text_paths.rs +++ b/render-wasm/src/shapes/text_paths.rs @@ -1,190 +1,81 @@ -use crate::get_resources; -use crate::shapes::text::TextContent; +use crate::render::text::decoration_segments; +use crate::shapes::text::{vertical_align_offset, Paragraph, TextContent}; +use crate::shapes::VerticalAlign; use skia_safe::{ - self as skia, textlayout::Paragraph as SkiaParagraph, FontMetrics, Point, Rect, TextBlob, + self as skia, + textlayout::{paragraph::VisitorInfo, Paragraph as SkiaParagraph}, + Point, }; use std::ops::Deref; pub struct TextPaths(TextContent); -// Note: This class is not being currently used. -// It's an example of how to convert texts to paths -#[allow(dead_code)] impl TextPaths { pub fn new(text_content: TextContent) -> Self { Self(text_content) } - pub fn get_paths(&self, antialias: bool) -> Vec<(skia::Path, skia::Paint)> { - let mut paths = Vec::new(); - let mut offset_y = self.bounds.y(); + pub fn get_paths(&self, vertical_align: VerticalAlign) -> Vec { + let layout_width = self.0.get_width(self.bounds.width()); let mut paragraph_builders = self.0.paragraph_builder_group_from_text(None); + let mut paragraphs: Vec = paragraph_builders + .iter_mut() + .filter_map(|group| group.first_mut()) + .map(|paragraph_builder| { + let mut paragraph = paragraph_builder.build(); + paragraph.layout(layout_width); + paragraph + }) + .collect(); - for paragraphs in paragraph_builders.iter_mut() { - for paragraph_builder in paragraphs.iter_mut() { - // 1. Get paragraph and set the width layout - let mut skia_paragraph = paragraph_builder.build(); - let text = paragraph_builder.get_text(); - let paragraph_width = self.bounds.width(); - skia_paragraph.layout(paragraph_width); + let total_height: f32 = paragraphs.iter().map(|p| p.height()).sum(); + let mut offset_y = self.bounds.y() + + vertical_align_offset(self.bounds.height(), total_height, vertical_align); - let mut line_offset_y = offset_y; - - // 2. Iterate through each line in the paragraph - for line_metrics in skia_paragraph.get_line_metrics() { - let line_baseline = line_metrics.baseline as f32; - let start = line_metrics.start_index; - let end = line_metrics.end_index; - - // 3. Get styles present in line for each text span - let style_metrics = line_metrics.get_style_metrics(start..end); - - let mut offset_x = 0.0; - - for (i, (start_index, style_metric)) in style_metrics.iter().enumerate() { - let end_index = style_metrics.get(i + 1).map_or(end, |next| next.0); - - let start_byte = text - .char_indices() - .nth(*start_index) - .map(|(i, _)| i) - .unwrap_or(0); - let end_byte = text - .char_indices() - .nth(end_index) - .map(|(i, _)| i) - .unwrap_or(text.len()); - - let span_text = &text[start_byte..end_byte]; - - let font = skia_paragraph.get_font_at(*start_index); - - let blob_offset_x = self.bounds.x() + line_metrics.left as f32 + offset_x; - let blob_offset_y = line_offset_y; - - // 4. Get the path for each text span - if let Some((text_path, paint)) = self.generate_text_path( - span_text, - &font, - blob_offset_x, - blob_offset_y, - style_metric, - antialias, - ) { - let text_width = font.measure_text(span_text, None).0; - offset_x += text_width; - paths.push((text_path, paint)); - } - } - line_offset_y = offset_y + line_baseline; - } - offset_y += skia_paragraph.height(); - } + let mut paths = Vec::new(); + for (paragraph, text_paragraph) in paragraphs.iter_mut().zip(self.0.paragraphs()) { + let origin = Point::new(self.bounds.x(), offset_y); + Self::collect_paragraph_paths(paragraph, text_paragraph, origin, &mut paths); + offset_y += paragraph.height(); } + paths } - fn generate_text_path( - &self, - span_text: &str, - font: &skia::Font, - blob_offset_x: f32, - blob_offset_y: f32, - style_metric: &skia::textlayout::StyleMetrics, - antialias: bool, - ) -> Option<(skia::Path, skia::Paint)> { - // Convert text to path, including text decoration - // TextBlob might be empty and, in this case, we return None - // This is used to avoid rendering empty paths, but we can - // revisit this logic later - if let Some((text_blob_path, text_blob_bounds)) = - Self::get_text_blob_path(span_text, font, blob_offset_x, blob_offset_y) - { - let text_width = font.measure_text(span_text, None).0; + fn collect_paragraph_paths( + paragraph: &mut SkiaParagraph, + text_paragraph: &Paragraph, + origin: Point, + paths: &mut Vec, + ) { + for deco in decoration_segments(paragraph, text_paragraph, origin.x, origin.y) { + let mut builder = skia::PathBuilder::new(); + builder.add_rect(deco.rect(), None, None); + paths.push(builder.detach()); + } - let decoration = style_metric.text_style.decoration(); - let font_metrics = style_metric.font_metrics; - - let blob_left = blob_offset_x; - let blob_top = blob_offset_y; - let blob_height = text_blob_bounds.height(); - - 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() + paragraph.visit(|_: usize, info: Option<&VisitorInfo>| { + let Some(info) = info else { + return; }; - let mut paint = style_metric.text_style.foreground(); - paint.set_anti_alias(antialias); + let font = info.font(); + let run_origin = origin + info.origin(); + let mut builder = skia::PathBuilder::new(); + let mut has_glyphs = false; - return Some((text_path, paint)); - } - None - } - - fn calculate_text_decoration_rect( - &self, - decoration: skia::textlayout::TextDecoration, - font_metrics: FontMetrics, - blob_left: f32, - blob_offset_y: f32, - text_width: f32, - blob_height: f32, - ) -> Option { - match decoration { - skia::textlayout::TextDecoration::LINE_THROUGH => { - let underline_thickness = font_metrics.underline_thickness().unwrap_or(0.0); - let underline_position = blob_height / 2.0; - Some(Rect::new( - blob_left, - blob_offset_y + underline_position - underline_thickness / 2.0, - blob_left + text_width, - blob_offset_y + underline_position + underline_thickness / 2.0, - )) + for (glyph, position) in info.glyphs().iter().zip(info.positions().iter()) { + let Some(glyph_path) = font.get_path(*glyph) else { + continue; + }; + builder.add_path(&glyph_path.with_offset(run_origin + *position)); + has_glyphs = true; } - skia::textlayout::TextDecoration::UNDERLINE => { - let underline_thickness = font_metrics.underline_thickness().unwrap_or(0.0); - let underline_position = blob_height - underline_thickness; - Some(Rect::new( - blob_left, - blob_offset_y + underline_position - underline_thickness / 2.0, - blob_left + text_width, - blob_offset_y + underline_position + underline_thickness / 2.0, - )) + + if has_glyphs { + paths.push(builder.detach()); } - _ => None, - } - } - - fn get_text_blob_path( - span_text: &str, - font: &skia::Font, - blob_offset_x: f32, - blob_offset_y: f32, - ) -> Option<(skia::Path, skia::Rect)> { - let utf16_text = span_text.encode_utf16().collect::>(); - let text = unsafe { skia_safe::as_utf16_unchecked(&utf16_text) }; - let emoji_font = get_resources().fonts.get_emoji_font(font.size()); - let use_font = emoji_font.as_ref().unwrap_or(font); - - if let Some(mut text_blob) = TextBlob::from_text(text, use_font) { - let path = SkiaParagraph::get_path(&mut text_blob); - let d = Point::new(blob_offset_x, blob_offset_y); - let offset_path = path.with_offset(d); - let bounds = text_blob.bounds(); - return Some((offset_path, *bounds)); - } - None + }); } }