From 0913545b41d350142295372aecb051d043a58f1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Torr=C3=B3?= Date: Fri, 11 Sep 2026 08:20:22 +0200 Subject: [PATCH] :bug: Fix letter spacing and position when flattening text to path (#11555) * :bug: Fix letter spacing and position when flattening text to path * :recycle: Simplify text to path conversion and add flatten tests --- .../render-wasm/get-file-text-flatten.json | 1332 +++++++++++++++++ .../ui/render-wasm-specs/texts.spec.js | 29 + .../Flattens-texts-to-paths-1.png | Bin 0 -> 51053 bytes frontend/src/app/render_wasm/shape.cljs | 10 +- render-wasm/src/render/fonts.rs | 4 - render-wasm/src/render/text.rs | 39 +- render-wasm/src/render/text_editor.rs | 18 +- render-wasm/src/shapes/shape_to_path.rs | 4 +- render-wasm/src/shapes/text.rs | 42 +- render-wasm/src/shapes/text_paths.rs | 217 +-- 10 files changed, 1476 insertions(+), 219 deletions(-) create mode 100644 frontend/playwright/data/render-wasm/get-file-text-flatten.json create mode 100644 frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Flattens-texts-to-paths-1.png 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 0000000000000000000000000000000000000000..3b5390e0a143b4d7a7d0a4cf85b5c6663514c52b GIT binary patch literal 51053 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 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 + }); } }