🐛 Fix text shadows and blur and refactor text rendering
@@ -23,6 +23,7 @@ export default defineConfig({
|
||||
expect: {
|
||||
timeout: process.env.CI ? 20000 : 5000,
|
||||
},
|
||||
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: "html",
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
@@ -62,7 +63,9 @@ export default defineConfig({
|
||||
},
|
||||
testDir: "./playwright/ui/render-wasm-specs",
|
||||
snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}.png",
|
||||
timeout: 2 * 60 * 1000,
|
||||
expect: {
|
||||
timeout: process.env.CI ? 20000 : 10000,
|
||||
toHaveScreenshot: {
|
||||
maxDiffPixelRatio: 0.001,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,905 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"fdata/path-data",
|
||||
"plugins/runtime",
|
||||
"design-tokens/v1",
|
||||
"variants/v1",
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/objects-map",
|
||||
"render-wasm/v1",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
},
|
||||
"~:team-id": "~u6bd7c17d-4f59-815e-8006-5c1f6882469a",
|
||||
"~: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": "New File 2",
|
||||
"~:revn": 401,
|
||||
"~:modified-at": "~m1757076417573",
|
||||
"~:vern": 0,
|
||||
"~:id": "~u15b74473-2908-8094-8006-bc90c3982c73",
|
||||
"~: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",
|
||||
"0004-clean-shadow-color",
|
||||
"0005-deprecate-image-type",
|
||||
"0006-fix-old-texts-fills",
|
||||
"0007-clear-invalid-strokes-and-fills-v2",
|
||||
"0008-fix-library-colors-v4",
|
||||
"0009-clean-library-colors",
|
||||
"0009-add-partial-text-touched-flags",
|
||||
"0010-fix-swap-slots-pointing-non-existent-shapes"
|
||||
]
|
||||
},
|
||||
"~:version": 67,
|
||||
"~:project-id": "~u6bd7c17d-4f59-815e-8006-5c1f68846e43",
|
||||
"~:created-at": "~m1756728830560",
|
||||
"~:data": {
|
||||
"~:pages": [
|
||||
"~u15b74473-2908-8094-8006-bc90c3982c74"
|
||||
],
|
||||
"~:pages-index": {
|
||||
"~u15b74473-2908-8094-8006-bc90c3982c74": {
|
||||
"~: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": [
|
||||
"~u88db2850-996a-804f-8006-c063323d68a4",
|
||||
"~uf94516f3-2d43-80b3-8006-c1b5c96d7dae",
|
||||
"~uf94516f3-2d43-80b3-8006-c1b60a252bcf"
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u88db2850-996a-804f-8006-c063323d68a4": {
|
||||
"~#shape": {
|
||||
"~:y": -865.0000046417117,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:last-resize-direction": "~:horizontal",
|
||||
"~:grow-type": "~:auto-height",
|
||||
"~:content": {
|
||||
"~:type": "root",
|
||||
"~:key": "1ygxrlda8tl",
|
||||
"~:children": [
|
||||
{
|
||||
"~:type": "paragraph-set",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "23svy7uenp6",
|
||||
"~:font-size": "48",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "shadows with multiple strokes and no fill"
|
||||
}
|
||||
],
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "center",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "e92bgu67k4",
|
||||
"~:font-size": "0",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:type": "paragraph",
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"~:vertical-align": "top"
|
||||
},
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Text",
|
||||
"~:width": 506.1299901710943,
|
||||
"~:type": "~:text",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": -251.00000411998855,
|
||||
"~:y": -865.0000046417117
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 255.12998605110573,
|
||||
"~:y": -865.0000046417117
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 255.12998605110573,
|
||||
"~:y": -749.0000040708983
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": -251.00000411998855,
|
||||
"~:y": -749.0000040708983
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:layout-item-h-sizing": "~:fix",
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:hidden": false,
|
||||
"~:id": "~u88db2850-996a-804f-8006-c063323d68a4",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:position-data": [
|
||||
{
|
||||
"~#rect": {
|
||||
"~:y": -804.7000015899539,
|
||||
"~:font-style": "normal",
|
||||
"~:text-transform": "none",
|
||||
"~:font-size": "48px",
|
||||
"~:font-weight": "400",
|
||||
"~:y1": -2.6999969482421875,
|
||||
"~:width": 464.2166748046875,
|
||||
"~:text-decoration": "rgb(0, 0, 0)",
|
||||
"~:letter-spacing": "normal",
|
||||
"~:x": -230.05000717174636,
|
||||
"~:x1": 20.949996948242188,
|
||||
"~:y2": 60.30000305175781,
|
||||
"~:fills": [],
|
||||
"~:x2": 485.1666717529297,
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "\"sourcesanspro\"",
|
||||
"~:height": 63,
|
||||
"~:text": "shadows with multiple "
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#rect": {
|
||||
"~:y": -747.1000107452273,
|
||||
"~:font-style": "normal",
|
||||
"~:text-transform": "none",
|
||||
"~:font-size": "48px",
|
||||
"~:font-weight": "400",
|
||||
"~:y1": 54.899993896484375,
|
||||
"~:width": 354.3333435058594,
|
||||
"~:text-decoration": "rgb(0, 0, 0)",
|
||||
"~:letter-spacing": "normal",
|
||||
"~:x": -175.10001022350417,
|
||||
"~:x1": 75.89999389648438,
|
||||
"~:y2": 117.89999389648438,
|
||||
"~:fills": [],
|
||||
"~:x2": 430.23333740234375,
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "\"sourcesanspro\"",
|
||||
"~:height": 63,
|
||||
"~:text": "strokes and no fill"
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [
|
||||
{
|
||||
"~:stroke-style": "~:solid",
|
||||
"~:stroke-alignment": "~:inner",
|
||||
"~:stroke-width": 1,
|
||||
"~:stroke-color": "#00ff11",
|
||||
"~:stroke-opacity": 1
|
||||
},
|
||||
{
|
||||
"~:stroke-style": "~:solid",
|
||||
"~:stroke-alignment": "~:outer",
|
||||
"~:stroke-width": 1,
|
||||
"~:stroke-color": "#ff00b1",
|
||||
"~:stroke-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:x": -251.00000411998855,
|
||||
"~:shadow": [
|
||||
{
|
||||
"~:color": {
|
||||
"~:color": "#7750e1",
|
||||
"~:opacity": 0.4722222222222222
|
||||
},
|
||||
"~:spread": 0,
|
||||
"~:offset-y": 10,
|
||||
"~:style": "~:drop-shadow",
|
||||
"~:blur": 0,
|
||||
"~:hidden": false,
|
||||
"~:id": "~uf94516f3-2d43-80b3-8006-c1b5b7323de5",
|
||||
"~:offset-x": 10
|
||||
},
|
||||
{
|
||||
"~:color": {
|
||||
"~:color": "#559fe1",
|
||||
"~:opacity": 0.7333333333333333
|
||||
},
|
||||
"~:spread": 0,
|
||||
"~:offset-y": -10,
|
||||
"~:style": "~:drop-shadow",
|
||||
"~:blur": 0,
|
||||
"~:hidden": false,
|
||||
"~:id": "~u427eca67-5b7f-80e6-8006-c0a7398ff4b4",
|
||||
"~:offset-x": -10
|
||||
}
|
||||
],
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": -251.00000411998855,
|
||||
"~:y": -865.0000046417117,
|
||||
"~:width": 506.1299901710943,
|
||||
"~:height": 116.00000057081343,
|
||||
"~:x1": -251.00000411998855,
|
||||
"~:y1": -865.0000046417117,
|
||||
"~:x2": 255.12998605110573,
|
||||
"~:y2": -749.0000040708983
|
||||
}
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 116.00000057081343,
|
||||
"~:flip-y": null
|
||||
}
|
||||
},
|
||||
"~uf94516f3-2d43-80b3-8006-c1b5c96d7dae": {
|
||||
"~#shape": {
|
||||
"~:y": -723.9999884292483,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:last-resize-direction": "~:horizontal",
|
||||
"~:grow-type": "~:auto-height",
|
||||
"~:content": {
|
||||
"~:type": "root",
|
||||
"~:key": "1ygxrlda8tl",
|
||||
"~: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": "23svy7uenp6",
|
||||
"~:font-size": "48",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#214ccd",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "shadows with multiple strokes and solid fill"
|
||||
}
|
||||
],
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "center",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "e92bgu67k4",
|
||||
"~:font-size": "48",
|
||||
"~: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": "#214ccd",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"~:vertical-align": "top"
|
||||
},
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Text",
|
||||
"~:width": 609.1300277709961,
|
||||
"~:type": "~:text",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": -303.0000086630885,
|
||||
"~:y": -723.9999884292483
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 306.1300191079076,
|
||||
"~:y": -723.9999884292483
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 306.1300191079076,
|
||||
"~:y": -607.9999878584349
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": -303.0000086630885,
|
||||
"~:y": -607.9999878584349
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:layout-item-h-sizing": "~:fix",
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:hidden": false,
|
||||
"~:id": "~uf94516f3-2d43-80b3-8006-c1b5c96d7dae",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:position-data": [
|
||||
{
|
||||
"~#rect": {
|
||||
"~:y": -663.6999853774905,
|
||||
"~:font-style": "normal",
|
||||
"~:text-transform": "none",
|
||||
"~:font-size": "48px",
|
||||
"~:font-weight": "400",
|
||||
"~:y1": -2.6999969482421875,
|
||||
"~:width": 464.2166748046875,
|
||||
"~:text-decoration": "rgb(33, 76, 205)",
|
||||
"~:letter-spacing": "normal",
|
||||
"~:x": -230.55001171484633,
|
||||
"~:x1": 72.44999694824219,
|
||||
"~:y2": 60.30000305175781,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#214ccd",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:x2": 536.6666717529297,
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "\"sourcesanspro\"",
|
||||
"~:height": 63,
|
||||
"~:text": "shadows with multiple "
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#rect": {
|
||||
"~:y": -606.099994532764,
|
||||
"~:font-style": "normal",
|
||||
"~:text-transform": "none",
|
||||
"~:font-size": "48px",
|
||||
"~:font-weight": "400",
|
||||
"~:y1": 54.899993896484375,
|
||||
"~:width": 398.8500061035156,
|
||||
"~:text-decoration": "rgb(33, 76, 205)",
|
||||
"~:letter-spacing": "normal",
|
||||
"~:x": -197.8666773642604,
|
||||
"~:x1": 105.13333129882812,
|
||||
"~:y2": 117.89999389648438,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#214ccd",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:x2": 503.98333740234375,
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "\"sourcesanspro\"",
|
||||
"~:height": 63,
|
||||
"~:text": "strokes and solid fill"
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [
|
||||
{
|
||||
"~:stroke-style": "~:solid",
|
||||
"~:stroke-alignment": "~:inner",
|
||||
"~:stroke-width": 1,
|
||||
"~:stroke-color": "#00ff11",
|
||||
"~:stroke-opacity": 1
|
||||
},
|
||||
{
|
||||
"~:stroke-style": "~:solid",
|
||||
"~:stroke-alignment": "~:outer",
|
||||
"~:stroke-width": 3,
|
||||
"~:stroke-color": "#ff00b1",
|
||||
"~:stroke-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:x": -303.0000086630885,
|
||||
"~:shadow": [
|
||||
{
|
||||
"~:color": {
|
||||
"~:color": "#7750e1",
|
||||
"~:opacity": 0.4722222222222222
|
||||
},
|
||||
"~:spread": 0,
|
||||
"~:offset-y": 4,
|
||||
"~:style": "~:drop-shadow",
|
||||
"~:blur": 0,
|
||||
"~:hidden": false,
|
||||
"~:id": "~uf94516f3-2d43-80b3-8006-c1b5b7323de5",
|
||||
"~:offset-x": 4
|
||||
},
|
||||
{
|
||||
"~:color": {
|
||||
"~:color": "#559fe1",
|
||||
"~:opacity": 0.7333333333333333
|
||||
},
|
||||
"~:spread": 0,
|
||||
"~:offset-y": -4,
|
||||
"~:style": "~:drop-shadow",
|
||||
"~:blur": 0,
|
||||
"~:hidden": false,
|
||||
"~:id": "~u427eca67-5b7f-80e6-8006-c0a7398ff4b4",
|
||||
"~:offset-x": -4
|
||||
}
|
||||
],
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": -303.0000086630885,
|
||||
"~:y": -723.9999884292483,
|
||||
"~:width": 609.1300277709961,
|
||||
"~:height": 116.00000057081343,
|
||||
"~:x1": -303.0000086630885,
|
||||
"~:y1": -723.9999884292483,
|
||||
"~:x2": 306.1300191079076,
|
||||
"~:y2": -607.9999878584349
|
||||
}
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 116.00000057081343,
|
||||
"~:flip-y": null
|
||||
}
|
||||
},
|
||||
"~uf94516f3-2d43-80b3-8006-c1b60a252bcf": {
|
||||
"~#shape": {
|
||||
"~:y": -581.9999718032777,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:fixed",
|
||||
"~:content": {
|
||||
"~:type": "root",
|
||||
"~:key": "1ygxrlda8tl",
|
||||
"~:children": [
|
||||
{
|
||||
"~:type": "paragraph-set",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "23svy7uenp6",
|
||||
"~:font-size": "48",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#ff7700",
|
||||
"~:fill-opacity": 0.38333333333333336
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "shadows with multiple strokes and transparent fill"
|
||||
}
|
||||
],
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "center",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "e92bgu67k4",
|
||||
"~:font-size": "0",
|
||||
"~: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": "#ff7700",
|
||||
"~:fill-opacity": 0.38333333333333336
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"~:vertical-align": "top"
|
||||
},
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Text",
|
||||
"~:width": 753.1299834251404,
|
||||
"~:type": "~:text",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": -374.99998553834683,
|
||||
"~:y": -581.9999718032777
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 378.1299978867936,
|
||||
"~:y": -581.9999718032777
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 378.1299978867936,
|
||||
"~:y": -523.999971517871
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": -374.99998553834683,
|
||||
"~:y": -523.999971517871
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:layout-item-h-sizing": "~:fix",
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1,
|
||||
"~:b": 0,
|
||||
"~:c": 0,
|
||||
"~:d": 1,
|
||||
"~:e": 0,
|
||||
"~:f": 0
|
||||
}
|
||||
},
|
||||
"~:hidden": false,
|
||||
"~:id": "~uf94516f3-2d43-80b3-8006-c1b60a252bcf",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:position-data": [
|
||||
{
|
||||
"~#rect": {
|
||||
"~:y": -521.6999687515199,
|
||||
"~:font-style": "normal",
|
||||
"~:text-transform": "none",
|
||||
"~:font-size": "48px",
|
||||
"~:font-weight": "400",
|
||||
"~:y1": -2.6999969482421875,
|
||||
"~:width": 706.3333740234375,
|
||||
"~:text-decoration": "rgba(255, 119, 0, 0.383)",
|
||||
"~:letter-spacing": "normal",
|
||||
"~:x": -351.5999916418624,
|
||||
"~:x1": 23.399993896484375,
|
||||
"~:y2": 60.30000305175781,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#ff7700",
|
||||
"~:fill-opacity": 0.38333333333333336
|
||||
}
|
||||
],
|
||||
"~:x2": 729.7333679199219,
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "\"sourcesanspro\"",
|
||||
"~:height": 63,
|
||||
"~:text": "shadows with multiple strokes and "
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#rect": {
|
||||
"~:y": -464.09997790679336,
|
||||
"~:font-style": "normal",
|
||||
"~:text-transform": "none",
|
||||
"~:font-size": "48px",
|
||||
"~:font-weight": "400",
|
||||
"~:y1": 54.899993896484375,
|
||||
"~:width": 295.48333740234375,
|
||||
"~:text-decoration": "rgba(255, 119, 0, 0.383)",
|
||||
"~:letter-spacing": "normal",
|
||||
"~:x": -146.18331988893272,
|
||||
"~:x1": 228.81666564941406,
|
||||
"~:y2": 117.89999389648438,
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#ff7700",
|
||||
"~:fill-opacity": 0.38333333333333336
|
||||
}
|
||||
],
|
||||
"~:x2": 524.3000030517578,
|
||||
"~:direction": "ltr",
|
||||
"~:font-family": "\"sourcesanspro\"",
|
||||
"~:height": 63,
|
||||
"~:text": "transparent fill"
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [
|
||||
{
|
||||
"~:stroke-style": "~:solid",
|
||||
"~:stroke-alignment": "~:outer",
|
||||
"~:stroke-width": 3,
|
||||
"~:stroke-color": "#ff00b0",
|
||||
"~:stroke-opacity": 0.37222222222222223
|
||||
}
|
||||
],
|
||||
"~:x": -374.9999855383468,
|
||||
"~:shadow": [
|
||||
{
|
||||
"~:color": {
|
||||
"~:color": "#00fb08",
|
||||
"~:opacity": 1
|
||||
},
|
||||
"~:spread": 0,
|
||||
"~:offset-y": 4,
|
||||
"~:style": "~:drop-shadow",
|
||||
"~:blur": 0,
|
||||
"~:hidden": false,
|
||||
"~:id": "~uf94516f3-2d43-80b3-8006-c1b5b7323de5",
|
||||
"~:offset-x": 4
|
||||
},
|
||||
{
|
||||
"~:color": {
|
||||
"~:color": "#559fe1",
|
||||
"~:opacity": 0.7333333333333333
|
||||
},
|
||||
"~:spread": 0,
|
||||
"~:offset-y": -4,
|
||||
"~:style": "~:drop-shadow",
|
||||
"~:blur": 0,
|
||||
"~:hidden": false,
|
||||
"~:id": "~u427eca67-5b7f-80e6-8006-c0a7398ff4b4",
|
||||
"~:offset-x": -4
|
||||
}
|
||||
],
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": -374.9999855383468,
|
||||
"~:y": -581.9999718032777,
|
||||
"~:width": 753.1299834251404,
|
||||
"~:height": 58.00000028540671,
|
||||
"~:x1": -374.9999855383468,
|
||||
"~:y1": -581.9999718032777,
|
||||
"~:x2": 378.1299978867936,
|
||||
"~:y2": -523.999971517871
|
||||
}
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 58.00000028540671,
|
||||
"~:flip-y": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:id": "~u15b74473-2908-8094-8006-bc90c3982c74",
|
||||
"~:name": "Page 1"
|
||||
}
|
||||
},
|
||||
"~:id": "~u15b74473-2908-8094-8006-bc90c3982c73",
|
||||
"~:options": {
|
||||
"~:components-v2": true,
|
||||
"~:base-font-size": "16px"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,6 +227,71 @@ test("Renders a file with multiple emoji", async ({ page }) => {
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with multiple text shadows, strokes, and blur combinations", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
|
||||
await workspace.mockGetFile(
|
||||
"render-wasm/get-file-text-shadows-and-blurs.json",
|
||||
);
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "15b74473-2908-8094-8006-bdb4fbd2c6a3",
|
||||
pageId: "15b74473-2908-8094-8006-bdb4fbd2c6a4",
|
||||
});
|
||||
await workspace.waitForFirstRender();
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with different text leaves decoration", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile(
|
||||
"render-wasm/get-file-text-leaves-decoration.json",
|
||||
);
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "15b74473-2908-8094-8006-bdb4fbd2c6a3",
|
||||
pageId: "15b74473-2908-8094-8006-bdb4fbd2c6a4",
|
||||
});
|
||||
|
||||
await workspace.waitForFirstRender();
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with different text shadows combinations", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile(
|
||||
"render-wasm/get-file-text-shadows-combination.json",
|
||||
);
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "15b74473-2908-8094-8006-bdb4fbd2c6a3",
|
||||
pageId: "15b74473-2908-8094-8006-bc90c3982c74",
|
||||
});
|
||||
|
||||
await workspace.waitForFirstRender();
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
|
||||
test("Renders a file with text in frames and different strokes, shadows, and blurs", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile(
|
||||
"render-wasm/get-file-frame-clipping-shadows-and-texts.json",
|
||||
);
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "44471494-966a-8178-8006-c5bd93f0fe72",
|
||||
pageId: "44471494-966a-8178-8006-c5bd93f0fe73",
|
||||
});
|
||||
|
||||
await workspace.waitForFirstRender();
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with texts with different alignments", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 430 KiB |
|
Before Width: | Height: | Size: 524 KiB After Width: | Height: | Size: 523 KiB |
|
After Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 126 KiB |
@@ -436,6 +436,9 @@ impl RenderState {
|
||||
|
||||
let paint = skia::Paint::default();
|
||||
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint));
|
||||
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint));
|
||||
|
||||
@@ -457,8 +460,10 @@ impl RenderState {
|
||||
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
|
||||
}
|
||||
|
||||
let surface_ids =
|
||||
SurfaceId::Strokes as u32 | SurfaceId::Fills as u32 | SurfaceId::InnerShadows as u32;
|
||||
let surface_ids = SurfaceId::Strokes as u32
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32;
|
||||
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().clear(skia::Color::TRANSPARENT);
|
||||
@@ -485,8 +490,10 @@ impl RenderState {
|
||||
fills_surface_id: SurfaceId,
|
||||
strokes_surface_id: SurfaceId,
|
||||
innershadows_surface_id: SurfaceId,
|
||||
text_drop_shadows_surface_id: SurfaceId,
|
||||
apply_to_current_surface: bool,
|
||||
offset: Option<(f32, f32)>,
|
||||
parent_shadows: Option<Vec<skia_safe::Paint>>,
|
||||
) {
|
||||
let shape = if let Some(scale_content) = scale_content {
|
||||
&shape.scale_content(*scale_content)
|
||||
@@ -494,8 +501,10 @@ impl RenderState {
|
||||
shape
|
||||
};
|
||||
|
||||
let surface_ids =
|
||||
fills_surface_id as u32 | strokes_surface_id as u32 | innershadows_surface_id as u32;
|
||||
let surface_ids = fills_surface_id as u32
|
||||
| strokes_surface_id as u32
|
||||
| innershadows_surface_id as u32
|
||||
| text_drop_shadows_surface_id as u32;
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().save();
|
||||
});
|
||||
@@ -604,72 +613,139 @@ impl RenderState {
|
||||
});
|
||||
|
||||
let text_content = text_content.new_bounds(shape.selrect());
|
||||
let drop_shadows = shape.drop_shadow_paints();
|
||||
let inner_shadows = shape.inner_shadow_paints();
|
||||
let blur_filter = shape.image_filter(1.);
|
||||
let blur_mask = shape.mask_filter(1.);
|
||||
let mut paragraphs = paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
blur_filter.as_ref(),
|
||||
blur_mask.as_ref(),
|
||||
None,
|
||||
);
|
||||
|
||||
let count_inner_strokes = shape.count_visible_inner_strokes();
|
||||
text::render(self, &shape, &mut paragraphs, Some(fills_surface_id));
|
||||
for stroke in shape.visible_strokes().rev() {
|
||||
let mut stroke_paragraphs = stroke_paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
stroke,
|
||||
&shape.selrect(),
|
||||
blur_filter.as_ref(),
|
||||
blur_mask.as_ref(),
|
||||
None,
|
||||
count_inner_strokes,
|
||||
);
|
||||
let mut paragraphs = paragraph_builder_group_from_text(&text_content, None);
|
||||
let mut paragraphs_with_shadows =
|
||||
paragraph_builder_group_from_text(&text_content, Some(true));
|
||||
let mut stroke_paragraphs_list = shape
|
||||
.visible_strokes()
|
||||
.map(|stroke| {
|
||||
stroke_paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
stroke,
|
||||
&shape.selrect(),
|
||||
count_inner_strokes,
|
||||
None,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
strokes::render(
|
||||
self,
|
||||
&shape,
|
||||
stroke,
|
||||
Some(strokes_surface_id),
|
||||
None,
|
||||
Some(&mut stroke_paragraphs),
|
||||
antialias,
|
||||
);
|
||||
let mut stroke_paragraphs_with_shadows_list = shape
|
||||
.visible_strokes()
|
||||
.map(|stroke| {
|
||||
stroke_paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
stroke,
|
||||
&shape.selrect(),
|
||||
count_inner_strokes,
|
||||
Some(true),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for inner_shadow in &inner_shadows {
|
||||
let mut stroke_paragraphs_with_inner_shadows =
|
||||
stroke_paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
stroke,
|
||||
&shape.selrect(),
|
||||
if let Some(parent_shadows) = parent_shadows {
|
||||
if !shape.has_visible_strokes() {
|
||||
for shadow in &parent_shadows {
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut paragraphs_with_shadows,
|
||||
text_drop_shadows_surface_id.into(),
|
||||
Some(shadow),
|
||||
blur_filter.as_ref(),
|
||||
blur_mask.as_ref(),
|
||||
Some(inner_shadow),
|
||||
count_inner_strokes,
|
||||
);
|
||||
shadows::render_text_inner_shadows(
|
||||
}
|
||||
} else {
|
||||
shadows::render_text_shadows(
|
||||
self,
|
||||
&shape,
|
||||
&mut stroke_paragraphs_with_inner_shadows,
|
||||
innershadows_surface_id,
|
||||
&mut paragraphs_with_shadows,
|
||||
&mut stroke_paragraphs_with_shadows_list,
|
||||
text_drop_shadows_surface_id.into(),
|
||||
&parent_shadows,
|
||||
&blur_filter,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 1. Text drop shadows
|
||||
if !shape.has_visible_strokes() {
|
||||
for shadow in &drop_shadows {
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut paragraphs_with_shadows,
|
||||
text_drop_shadows_surface_id.into(),
|
||||
Some(shadow),
|
||||
blur_filter.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for inner_shadow in &inner_shadows {
|
||||
let mut paragraphs_with_inner_shadows = paragraph_builder_group_from_text(
|
||||
&text_content,
|
||||
// 2. Text fills
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut paragraphs,
|
||||
Some(fills_surface_id),
|
||||
None,
|
||||
blur_filter.as_ref(),
|
||||
blur_mask.as_ref(),
|
||||
Some(inner_shadow),
|
||||
);
|
||||
shadows::render_text_inner_shadows(
|
||||
|
||||
// 3. Stroke drop shadows
|
||||
shadows::render_text_shadows(
|
||||
self,
|
||||
&shape,
|
||||
&mut paragraphs_with_inner_shadows,
|
||||
innershadows_surface_id,
|
||||
&mut paragraphs_with_shadows,
|
||||
&mut stroke_paragraphs_with_shadows_list,
|
||||
text_drop_shadows_surface_id.into(),
|
||||
&drop_shadows,
|
||||
&blur_filter,
|
||||
);
|
||||
|
||||
// 4. Stroke fills
|
||||
for stroke_paragraphs in stroke_paragraphs_list.iter_mut() {
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
stroke_paragraphs,
|
||||
Some(strokes_surface_id),
|
||||
None,
|
||||
blur_filter.as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Stroke inner shadows
|
||||
shadows::render_text_shadows(
|
||||
self,
|
||||
&shape,
|
||||
&mut paragraphs_with_shadows,
|
||||
&mut stroke_paragraphs_with_shadows_list,
|
||||
Some(innershadows_surface_id),
|
||||
&inner_shadows,
|
||||
&blur_filter,
|
||||
);
|
||||
|
||||
// 6. Fill Inner shadows
|
||||
if !shape.has_visible_strokes() {
|
||||
for shadow in &inner_shadows {
|
||||
text::render(
|
||||
Some(self),
|
||||
None,
|
||||
&shape,
|
||||
&mut paragraphs_with_shadows,
|
||||
Some(innershadows_surface_id),
|
||||
Some(shadow),
|
||||
blur_filter.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -718,7 +794,6 @@ impl RenderState {
|
||||
stroke,
|
||||
Some(strokes_surface_id),
|
||||
None,
|
||||
None,
|
||||
antialias,
|
||||
);
|
||||
shadows::render_stroke_inner_shadows(
|
||||
@@ -815,8 +890,10 @@ impl RenderState {
|
||||
performance::begin_measure!("start_render_loop");
|
||||
|
||||
self.reset_canvas();
|
||||
let surface_ids =
|
||||
SurfaceId::Strokes as u32 | SurfaceId::Fills as u32 | SurfaceId::InnerShadows as u32;
|
||||
let surface_ids = SurfaceId::Strokes as u32
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32;
|
||||
self.surfaces.apply_mut(surface_ids, |s| {
|
||||
s.canvas().scale((scale, scale));
|
||||
});
|
||||
@@ -997,8 +1074,10 @@ impl RenderState {
|
||||
SurfaceId::Fills,
|
||||
SurfaceId::Strokes,
|
||||
SurfaceId::InnerShadows,
|
||||
SurfaceId::TextDropShadows,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1098,7 +1177,6 @@ impl RenderState {
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.save_layer(&layer_rec);
|
||||
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.scale((scale, scale));
|
||||
@@ -1116,8 +1194,10 @@ impl RenderState {
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
false,
|
||||
Some((shadow.offset.0, shadow.offset.1)),
|
||||
None,
|
||||
);
|
||||
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
@@ -1133,6 +1213,7 @@ impl RenderState {
|
||||
) -> Result<(bool, bool), String> {
|
||||
let mut iteration = 0;
|
||||
let mut is_empty = true;
|
||||
|
||||
while let Some(node_render_state) = self.pending_nodes.pop() {
|
||||
let NodeRenderState {
|
||||
id: node_id,
|
||||
@@ -1199,80 +1280,128 @@ impl RenderState {
|
||||
}
|
||||
|
||||
self.render_shape_enter(element, mask);
|
||||
|
||||
if !node_render_state.is_root() && self.focus_mode.is_active() {
|
||||
let scale = self.get_scale();
|
||||
let scale: f32 = self.get_scale();
|
||||
let translation = self
|
||||
.surfaces
|
||||
.get_render_context_translation(self.render_area, scale);
|
||||
|
||||
// Shadow rendering technique: Two-pass approach for proper opacity handling
|
||||
//
|
||||
// The shadow rendering uses a two-pass technique to ensure that overlapping
|
||||
// shadow areas maintain correct opacity without unwanted darkening:
|
||||
//
|
||||
// 1. First pass: Render shadow shape in pure black (alpha channel preserved)
|
||||
// - This creates the shadow silhouette with proper alpha gradients
|
||||
// - The black color acts as a mask for the final shadow color
|
||||
//
|
||||
// 2. Second pass: Apply actual shadow color using SrcIn blend mode
|
||||
// - SrcIn preserves the alpha channel from the black shadow
|
||||
// - Only the color channels are replaced, maintaining transparency
|
||||
// - This prevents overlapping shadows from accumulating opacity
|
||||
//
|
||||
// This approach is essential for complex shapes with transparency where
|
||||
// multiple shadow areas might overlap, ensuring visual consistency.
|
||||
for shadow in element.drop_shadows().rev().filter(|s| !s.hidden()) {
|
||||
let paint = skia::Paint::default();
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.save_layer(&layer_rec);
|
||||
// For text shapes, render drop shadow using text rendering logic
|
||||
if !matches!(element.shape_type, Type::Text(_)) {
|
||||
// Shadow rendering technique: Two-pass approach for proper opacity handling
|
||||
//
|
||||
// The shadow rendering uses a two-pass technique to ensure that overlapping
|
||||
// shadow areas maintain correct opacity without unwanted darkening:
|
||||
//
|
||||
// 1. First pass: Render shadow shape in pure black (alpha channel preserved)
|
||||
// - This creates the shadow silhouette with proper alpha gradients
|
||||
// - The black color acts as a mask for the final shadow color
|
||||
//
|
||||
// 2. Second pass: Apply actual shadow color using SrcIn blend mode
|
||||
// - SrcIn preserves the alpha channel from the black shadow
|
||||
// - Only the color channels are replaced, maintaining transparency
|
||||
// - This prevents overlapping shadows from accumulating opacity
|
||||
//
|
||||
// This approach is essential for complex shapes with transparency where
|
||||
// multiple shadow areas might overlap, ensuring visual consistency.
|
||||
for shadow in element.drop_shadows_visible() {
|
||||
let paint = skia::Paint::default();
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
|
||||
// First pass: Render shadow in black to establish alpha mask
|
||||
self.render_drop_black_shadow(
|
||||
tree,
|
||||
modifiers,
|
||||
structure,
|
||||
element,
|
||||
shadow,
|
||||
scale_content.get(&element.id),
|
||||
clip_bounds,
|
||||
scale,
|
||||
translation,
|
||||
);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.save_layer(&layer_rec);
|
||||
|
||||
// Nested shapes shadowing - apply black shadow to child shapes too
|
||||
for shadow_shape_id in element.children.iter() {
|
||||
let shadow_shape = tree.get(shadow_shape_id).unwrap();
|
||||
let clip_bounds = node_render_state.get_shadow_clip_bounds(
|
||||
element,
|
||||
modifiers.get(&element.id),
|
||||
shadow,
|
||||
);
|
||||
// First pass: Render shadow in black to establish alpha mask
|
||||
self.render_drop_black_shadow(
|
||||
tree,
|
||||
modifiers,
|
||||
structure,
|
||||
shadow_shape,
|
||||
element,
|
||||
shadow,
|
||||
scale_content.get(&element.id),
|
||||
clip_bounds,
|
||||
scale,
|
||||
translation,
|
||||
);
|
||||
|
||||
// Nested shapes shadowing - apply black shadow to child shapes too
|
||||
for shadow_shape_id in element.children.iter() {
|
||||
let shadow_shape = tree.get(shadow_shape_id).unwrap();
|
||||
let clip_bounds = node_render_state.get_shadow_clip_bounds(
|
||||
element,
|
||||
modifiers.get(&element.id),
|
||||
shadow,
|
||||
);
|
||||
|
||||
if !matches!(shadow_shape.shape_type, Type::Text(_)) {
|
||||
self.render_drop_black_shadow(
|
||||
tree,
|
||||
modifiers,
|
||||
structure,
|
||||
shadow_shape,
|
||||
shadow,
|
||||
scale_content.get(&element.id),
|
||||
clip_bounds,
|
||||
scale,
|
||||
translation,
|
||||
);
|
||||
} else {
|
||||
let paint = skia::Paint::default();
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.save_layer(&layer_rec);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.scale((scale, scale));
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.translate(translation);
|
||||
|
||||
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
|
||||
// transformed_shadow.to_mut().offset = (0., 0.);
|
||||
transformed_shadow.to_mut().color = skia::Color::BLACK;
|
||||
transformed_shadow.to_mut().blur = transformed_shadow.blur * scale;
|
||||
|
||||
let mut new_shadow_paint = skia::Paint::default();
|
||||
new_shadow_paint
|
||||
.set_image_filter(transformed_shadow.get_drop_shadow_filter());
|
||||
new_shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||
|
||||
self.render_shape(
|
||||
tree,
|
||||
modifiers,
|
||||
structure,
|
||||
shadow_shape,
|
||||
scale_content.get(&element.id),
|
||||
clip_bounds,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
SurfaceId::DropShadows,
|
||||
true,
|
||||
None,
|
||||
Some(vec![new_shadow_paint.clone()]),
|
||||
);
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: Apply actual shadow color using SrcIn blend mode
|
||||
// This preserves the alpha channel from the black shadow while
|
||||
// replacing only the color channels, preventing opacity accumulation
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(shadow.color);
|
||||
paint.set_blend_mode(skia::BlendMode::SrcIn);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.draw_paint(&paint);
|
||||
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
}
|
||||
|
||||
// Second pass: Apply actual shadow color using SrcIn blend mode
|
||||
// This preserves the alpha channel from the black shadow while
|
||||
// replacing only the color channels, preventing opacity accumulation
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(shadow.color);
|
||||
paint.set_blend_mode(skia::BlendMode::SrcIn);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.draw_paint(&paint);
|
||||
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
}
|
||||
|
||||
self.surfaces
|
||||
@@ -1292,8 +1421,10 @@ impl RenderState {
|
||||
SurfaceId::Fills,
|
||||
SurfaceId::Strokes,
|
||||
SurfaceId::InnerShadows,
|
||||
SurfaceId::TextDropShadows,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
self.surfaces
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use super::{RenderState, SurfaceId};
|
||||
use crate::render::strokes;
|
||||
use crate::render::text::{self};
|
||||
use crate::shapes::{Shadow, Shape, Stroke, Type};
|
||||
use skia_safe::textlayout::ParagraphBuilder;
|
||||
use skia_safe::{Paint, Path};
|
||||
use skia_safe::{canvas::SaveLayerRec, Paint, Path};
|
||||
|
||||
use crate::render::text;
|
||||
use crate::textlayout::ParagraphBuilderGroup;
|
||||
|
||||
// Fill Shadows
|
||||
pub fn render_fill_inner_shadows(
|
||||
@@ -46,7 +47,6 @@ pub fn render_stroke_inner_shadows(
|
||||
stroke,
|
||||
Some(surface_id),
|
||||
filter.as_ref(),
|
||||
None,
|
||||
antialias,
|
||||
)
|
||||
}
|
||||
@@ -76,15 +76,6 @@ pub fn render_text_path_stroke_drop_shadows(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_text_inner_shadows(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
paragraphs: &mut [Vec<ParagraphBuilder>],
|
||||
surface_id: SurfaceId,
|
||||
) {
|
||||
text::render(render_state, shape, paragraphs, Some(surface_id));
|
||||
}
|
||||
|
||||
// Render text paths (unused)
|
||||
#[allow(dead_code)]
|
||||
pub fn render_text_path_stroke_inner_shadows(
|
||||
@@ -129,3 +120,50 @@ fn render_shadow_paint(
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_text_shadows(
|
||||
render_state: &mut RenderState,
|
||||
shape: &Shape,
|
||||
paragraphs: &mut [ParagraphBuilderGroup],
|
||||
stroke_paragraphs_group: &mut [Vec<ParagraphBuilderGroup>],
|
||||
surface_id: Option<SurfaceId>,
|
||||
shadows: &[Paint],
|
||||
blur_filter: &Option<skia_safe::ImageFilter>,
|
||||
) {
|
||||
if stroke_paragraphs_group.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let canvas = render_state
|
||||
.surfaces
|
||||
.canvas(surface_id.unwrap_or(SurfaceId::TextDropShadows));
|
||||
|
||||
for shadow in shadows {
|
||||
let shadow_layer = SaveLayerRec::default().paint(shadow);
|
||||
canvas.save_layer(&shadow_layer);
|
||||
|
||||
text::render(
|
||||
None,
|
||||
Some(canvas),
|
||||
shape,
|
||||
paragraphs,
|
||||
surface_id,
|
||||
None,
|
||||
blur_filter.as_ref(),
|
||||
);
|
||||
|
||||
for stroke_paragraphs in stroke_paragraphs_group.iter_mut() {
|
||||
text::render(
|
||||
None,
|
||||
Some(canvas),
|
||||
shape,
|
||||
stroke_paragraphs,
|
||||
surface_id,
|
||||
None,
|
||||
blur_filter.as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ use std::collections::HashMap;
|
||||
use crate::math::{Matrix, Point, Rect};
|
||||
|
||||
use crate::shapes::{Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, Type};
|
||||
use skia_safe::{self as skia, textlayout::ParagraphBuilder, ImageFilter, RRect};
|
||||
use skia_safe::{self as skia, ImageFilter, RRect};
|
||||
|
||||
use super::{RenderState, SurfaceId};
|
||||
use crate::render::filters::compose_filters;
|
||||
use crate::render::text::{self};
|
||||
use crate::render::{get_dest_rect, get_source_rect};
|
||||
|
||||
// FIXME: See if we can simplify these arguments
|
||||
@@ -519,7 +518,6 @@ pub fn render(
|
||||
stroke: &Stroke,
|
||||
surface_id: Option<SurfaceId>,
|
||||
shadow: Option<&ImageFilter>,
|
||||
paragraphs: Option<&mut Vec<Vec<ParagraphBuilder>>>,
|
||||
antialias: bool,
|
||||
) {
|
||||
let scale = render_state.get_scale();
|
||||
@@ -564,14 +562,7 @@ pub fn render(
|
||||
shape.image_filter(1.).as_ref(),
|
||||
antialias,
|
||||
),
|
||||
Type::Text(_) => {
|
||||
text::render(
|
||||
render_state,
|
||||
shape,
|
||||
paragraphs.expect("Text shapes should have paragraphs"),
|
||||
surface_id,
|
||||
);
|
||||
}
|
||||
Type::Text(_) => {}
|
||||
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
|
||||
if let Some(path) = shape_type.path() {
|
||||
draw_stroke_on_path(
|
||||
|
||||
@@ -17,15 +17,16 @@ const TILE_SIZE_MULTIPLIER: i32 = 2;
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum SurfaceId {
|
||||
Target = 0b0_0000_0001,
|
||||
Cache = 0b0_0000_0010,
|
||||
Current = 0b0_0000_0100,
|
||||
Fills = 0b0_0000_1000,
|
||||
Strokes = 0b0_0001_0000,
|
||||
DropShadows = 0b0_0010_0000,
|
||||
InnerShadows = 0b0_0100_0000,
|
||||
UI = 0b0_1000_0000,
|
||||
Debug = 0b1_0000_0000,
|
||||
Target = 0b00_0000_0001,
|
||||
Cache = 0b00_0000_0010,
|
||||
Current = 0b00_0000_0100,
|
||||
Fills = 0b00_0000_1000,
|
||||
Strokes = 0b00_0001_0000,
|
||||
DropShadows = 0b00_0010_0000,
|
||||
InnerShadows = 0b00_0100_0000,
|
||||
TextDropShadows = 0b00_1000_0000,
|
||||
UI = 0b01_0000_0000,
|
||||
Debug = 0b10_0000_0001,
|
||||
}
|
||||
|
||||
pub struct Surfaces {
|
||||
@@ -42,6 +43,8 @@ pub struct Surfaces {
|
||||
drop_shadows: skia::Surface,
|
||||
// used for rendering over shadows.
|
||||
inner_shadows: skia::Surface,
|
||||
// used for rendering text drop shadows
|
||||
text_drop_shadows: skia::Surface,
|
||||
// used for displaying auxiliary workspace elements
|
||||
ui: skia::Surface,
|
||||
// for drawing debug info.
|
||||
@@ -73,6 +76,8 @@ impl Surfaces {
|
||||
gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims);
|
||||
let inner_shadows =
|
||||
gpu_state.create_surface_with_isize("inner_shadows".to_string(), extra_tile_dims);
|
||||
let text_drop_shadows =
|
||||
gpu_state.create_surface_with_isize("text_drop_shadows".to_string(), extra_tile_dims);
|
||||
let shape_fills =
|
||||
gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims);
|
||||
let shape_strokes =
|
||||
@@ -88,6 +93,7 @@ impl Surfaces {
|
||||
current,
|
||||
drop_shadows,
|
||||
inner_shadows,
|
||||
text_drop_shadows,
|
||||
shape_fills,
|
||||
shape_strokes,
|
||||
ui,
|
||||
@@ -166,6 +172,9 @@ impl Surfaces {
|
||||
if ids & SurfaceId::InnerShadows as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::InnerShadows));
|
||||
}
|
||||
if ids & SurfaceId::TextDropShadows as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::TextDropShadows));
|
||||
}
|
||||
if ids & SurfaceId::DropShadows as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::DropShadows));
|
||||
}
|
||||
@@ -189,11 +198,15 @@ impl Surfaces {
|
||||
pub fn update_render_context(&mut self, render_area: skia::Rect, scale: f32) {
|
||||
let translation = self.get_render_context_translation(render_area, scale);
|
||||
self.apply_mut(
|
||||
SurfaceId::Fills as u32 | SurfaceId::Strokes as u32 | SurfaceId::InnerShadows as u32,
|
||||
SurfaceId::Fills as u32
|
||||
| SurfaceId::Strokes as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32,
|
||||
|s| {
|
||||
s.canvas().restore();
|
||||
s.canvas().save();
|
||||
s.canvas().translate(translation);
|
||||
let canvas = s.canvas();
|
||||
canvas.reset_matrix();
|
||||
canvas.scale((scale, scale));
|
||||
canvas.translate(translation);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -206,6 +219,7 @@ impl Surfaces {
|
||||
SurfaceId::Current => &mut self.current,
|
||||
SurfaceId::DropShadows => &mut self.drop_shadows,
|
||||
SurfaceId::InnerShadows => &mut self.inner_shadows,
|
||||
SurfaceId::TextDropShadows => &mut self.text_drop_shadows,
|
||||
SurfaceId::Fills => &mut self.shape_fills,
|
||||
SurfaceId::Strokes => &mut self.shape_strokes,
|
||||
SurfaceId::Debug => &mut self.debug,
|
||||
@@ -257,13 +271,15 @@ impl Surfaces {
|
||||
pub fn reset(&mut self, color: skia::Color) {
|
||||
self.canvas(SurfaceId::Fills).restore_to_count(1);
|
||||
self.canvas(SurfaceId::InnerShadows).restore_to_count(1);
|
||||
self.canvas(SurfaceId::TextDropShadows).restore_to_count(1);
|
||||
self.canvas(SurfaceId::Strokes).restore_to_count(1);
|
||||
self.canvas(SurfaceId::Current).restore_to_count(1);
|
||||
self.apply_mut(
|
||||
SurfaceId::Fills as u32
|
||||
| SurfaceId::Strokes as u32
|
||||
| SurfaceId::Current as u32
|
||||
| SurfaceId::InnerShadows as u32,
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32,
|
||||
|s| {
|
||||
s.canvas().clear(color).reset_matrix();
|
||||
},
|
||||
|
||||
@@ -1,22 +1,55 @@
|
||||
use super::{RenderState, Shape, SurfaceId};
|
||||
use crate::shapes::VerticalAlign;
|
||||
use skia_safe::{
|
||||
canvas::SaveLayerRec, textlayout::LineMetrics, textlayout::Paragraph,
|
||||
textlayout::ParagraphBuilder, textlayout::RectHeightStyle, textlayout::RectWidthStyle,
|
||||
textlayout::StyleMetrics, textlayout::TextDecoration, textlayout::TextStyle, Canvas, Paint,
|
||||
Path,
|
||||
canvas::SaveLayerRec,
|
||||
textlayout::{
|
||||
LineMetrics, Paragraph, ParagraphBuilder, RectHeightStyle, RectWidthStyle, StyleMetrics,
|
||||
TextDecoration, TextStyle,
|
||||
},
|
||||
Canvas, ImageFilter, Paint, Path,
|
||||
};
|
||||
|
||||
pub fn render(
|
||||
render_state: &mut RenderState,
|
||||
render_state: Option<&mut RenderState>,
|
||||
canvas: Option<&Canvas>,
|
||||
shape: &Shape,
|
||||
paragraphs: &mut [Vec<ParagraphBuilder>],
|
||||
surface_id: Option<SurfaceId>,
|
||||
shadow: Option<&Paint>,
|
||||
blur: Option<&ImageFilter>,
|
||||
) {
|
||||
let canvas = render_state
|
||||
.surfaces
|
||||
.canvas(surface_id.unwrap_or(SurfaceId::Fills));
|
||||
let render_canvas = if let Some(rs) = render_state {
|
||||
rs.surfaces.canvas(surface_id.unwrap_or(SurfaceId::Fills))
|
||||
} else if let Some(c) = canvas {
|
||||
c
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(blur_filter) = blur {
|
||||
let mut blur_paint = Paint::default();
|
||||
blur_paint.set_image_filter(blur_filter.clone());
|
||||
let blur_layer = SaveLayerRec::default().paint(&blur_paint);
|
||||
render_canvas.save_layer(&blur_layer);
|
||||
}
|
||||
|
||||
if let Some(shadow_paint) = shadow {
|
||||
let layer_rec = SaveLayerRec::default().paint(shadow_paint);
|
||||
render_canvas.save_layer(&layer_rec);
|
||||
draw_text(render_canvas, shape, paragraphs);
|
||||
render_canvas.restore();
|
||||
} else {
|
||||
draw_text(render_canvas, shape, paragraphs);
|
||||
}
|
||||
|
||||
if blur.is_some() {
|
||||
render_canvas.restore();
|
||||
}
|
||||
|
||||
render_canvas.restore();
|
||||
}
|
||||
|
||||
fn draw_text(canvas: &Canvas, shape: &Shape, paragraphs: &mut [Vec<ParagraphBuilder>]) {
|
||||
// Width
|
||||
let paragraph_width = if let crate::shapes::Type::Text(text_content) = &shape.shape_type {
|
||||
text_content.width()
|
||||
@@ -63,8 +96,6 @@ pub fn render(
|
||||
global_offset_y = group_offset_y;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
fn draw_text_decorations(
|
||||
@@ -286,11 +317,11 @@ pub fn render_as_path(
|
||||
// let text_content = text_content.new_bounds(shape.selrect());
|
||||
// let paths = text_content.get_paths(antialias);
|
||||
|
||||
// shadows::render_text_drop_shadows(self, &shape, &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_drop_shadows(
|
||||
// shadows::render_text_path_stroke_shadows(
|
||||
// self, &shape, &paths, stroke, antialias,
|
||||
// );
|
||||
// strokes::render_text_paths(self, &shape, stroke, &paths, None, None, antialias);
|
||||
|
||||
@@ -573,6 +573,12 @@ impl Shape {
|
||||
.filter(|stroke| stroke.width > MIN_STROKE_WIDTH)
|
||||
}
|
||||
|
||||
pub fn has_visible_strokes(&self) -> bool {
|
||||
self.strokes
|
||||
.iter()
|
||||
.any(|stroke| stroke.width > MIN_STROKE_WIDTH)
|
||||
}
|
||||
|
||||
pub fn add_stroke(&mut self, s: Stroke) {
|
||||
self.invalidate_extrect();
|
||||
self.strokes.push(s)
|
||||
@@ -984,6 +990,7 @@ impl Shape {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn mask_filter(&self, scale: f32) -> Option<skia::MaskFilter> {
|
||||
if !self.blur.hidden {
|
||||
match self.blur.blur_type {
|
||||
@@ -1022,6 +1029,12 @@ impl Shape {
|
||||
.filter(|shadow| shadow.style() == ShadowStyle::Drop)
|
||||
}
|
||||
|
||||
pub fn drop_shadows_visible(&self) -> impl DoubleEndedIterator<Item = &Shadow> {
|
||||
self.shadows
|
||||
.iter()
|
||||
.filter(|shadow| shadow.style() == ShadowStyle::Drop && !shadow.hidden())
|
||||
}
|
||||
|
||||
pub fn inner_shadows(&self) -> impl DoubleEndedIterator<Item = &Shadow> {
|
||||
self.shadows
|
||||
.iter()
|
||||
@@ -1162,11 +1175,6 @@ impl Shape {
|
||||
!self.fills.is_empty()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn has_visible_inner_strokes(&self) -> bool {
|
||||
self.visible_strokes().any(|s| s.kind == StrokeKind::Inner)
|
||||
}
|
||||
|
||||
pub fn count_visible_inner_strokes(&self) -> usize {
|
||||
self.visible_strokes()
|
||||
.filter(|s| s.kind == StrokeKind::Inner)
|
||||
@@ -1210,6 +1218,20 @@ impl Shape {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drop_shadow_paints(&self) -> Vec<skia_safe::Paint> {
|
||||
let drop_shadows: Vec<&crate::shapes::shadows::Shadow> =
|
||||
self.drop_shadows().filter(|s| !s.hidden()).collect();
|
||||
drop_shadows
|
||||
.into_iter()
|
||||
.map(|shadow| {
|
||||
let mut paint = skia_safe::Paint::default();
|
||||
let filter = shadow.get_drop_shadow_filter();
|
||||
paint.set_image_filter(filter);
|
||||
paint
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn inner_shadow_paints(&self) -> Vec<skia_safe::Paint> {
|
||||
let inner_shadows: Vec<&crate::shapes::shadows::Shadow> =
|
||||
self.inner_shadows().filter(|s| !s.hidden()).collect();
|
||||
|
||||
@@ -255,7 +255,12 @@ pub fn merge_fills(fills: &[Fill], bounding_box: Rect) -> skia::Paint {
|
||||
fills_paint
|
||||
}
|
||||
|
||||
pub fn set_paint_fill(paint: &mut Paint, fill: &Fill, bounding_box: &Rect) {
|
||||
pub fn set_paint_fill(paint: &mut Paint, fill: &Fill, bounding_box: &Rect, remove_alpha: bool) {
|
||||
if remove_alpha {
|
||||
paint.set_color(skia::Color::BLACK);
|
||||
paint.set_alpha(255);
|
||||
return;
|
||||
}
|
||||
let shader = get_fill_shader(fill, bounding_box);
|
||||
if let Some(shader) = shader {
|
||||
paint.set_shader(shader);
|
||||
|
||||
@@ -200,7 +200,7 @@ fn propagate_transform(
|
||||
match content.grow_type() {
|
||||
GrowType::AutoHeight => {
|
||||
let paragraph_width = shape_bounds_after.width();
|
||||
let mut paragraphs = paragraph_builder_group_from_text(content, None, None, None);
|
||||
let mut paragraphs = paragraph_builder_group_from_text(content, None);
|
||||
let height = auto_height(&mut paragraphs, paragraph_width);
|
||||
let resize_transform = math::resize_matrix(
|
||||
&shape_bounds_after,
|
||||
@@ -213,7 +213,7 @@ fn propagate_transform(
|
||||
}
|
||||
GrowType::AutoWidth => {
|
||||
let paragraph_width = content.width();
|
||||
let mut paragraphs = paragraph_builder_group_from_text(content, None, None, None);
|
||||
let mut paragraphs = paragraph_builder_group_from_text(content, None);
|
||||
let height = auto_height(&mut paragraphs, paragraph_width);
|
||||
let resize_transform = math::resize_matrix(
|
||||
&shape_bounds_after,
|
||||
|
||||
@@ -264,4 +264,11 @@ impl Stroke {
|
||||
|
||||
paint
|
||||
}
|
||||
|
||||
pub fn is_transparent(&self) -> bool {
|
||||
match &self.fill {
|
||||
Fill::Solid(SolidColor(color)) => color.a() == 0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ use crate::{
|
||||
textlayout::paragraph_builder_group_from_text,
|
||||
};
|
||||
|
||||
use skia_safe::{self as skia, paint::Paint, textlayout::ParagraphStyle, ImageFilter, MaskFilter};
|
||||
use skia_safe::{
|
||||
self as skia,
|
||||
paint::{self, Paint},
|
||||
textlayout::ParagraphStyle,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::FontFamily;
|
||||
@@ -86,7 +90,7 @@ impl TextContent {
|
||||
|
||||
pub fn width(&self) -> f32 {
|
||||
if self.grow_type() == GrowType::AutoWidth {
|
||||
let temp_paragraphs = paragraph_builder_group_from_text(self, None, None, None);
|
||||
let temp_paragraphs = paragraph_builder_group_from_text(self, None);
|
||||
let mut temp_paragraphs = temp_paragraphs;
|
||||
auto_width(&mut temp_paragraphs, f32::MAX).ceil()
|
||||
} else {
|
||||
@@ -104,7 +108,7 @@ impl TextContent {
|
||||
|
||||
pub fn visual_bounds(&self) -> (f32, f32) {
|
||||
let paragraph_width = self.width();
|
||||
let mut paragraphs = paragraph_builder_group_from_text(self, None, None, None);
|
||||
let mut paragraphs = paragraph_builder_group_from_text(self, None);
|
||||
let paragraph_height = auto_height(&mut paragraphs, paragraph_width);
|
||||
(paragraph_width, paragraph_height)
|
||||
}
|
||||
@@ -308,26 +312,20 @@ impl TextLeaf {
|
||||
&self,
|
||||
content_bounds: &Rect,
|
||||
fallback_fonts: &HashSet<String>,
|
||||
_blur: Option<&ImageFilter>,
|
||||
blur_mask: Option<&MaskFilter>,
|
||||
shadow: Option<&Paint>,
|
||||
remove_alpha: bool,
|
||||
) -> skia::textlayout::TextStyle {
|
||||
let mut style = skia::textlayout::TextStyle::default();
|
||||
|
||||
if shadow.is_some() {
|
||||
let paint = shadow.unwrap().clone();
|
||||
style.set_foreground_paint(&paint);
|
||||
let mut paint = paint::Paint::default();
|
||||
|
||||
if remove_alpha {
|
||||
paint.set_color(skia::Color::BLACK);
|
||||
paint.set_alpha(255);
|
||||
} else {
|
||||
let paint = merge_fills(&self.fills, *content_bounds);
|
||||
style.set_foreground_paint(&paint);
|
||||
}
|
||||
|
||||
if let Some(blur_mask) = blur_mask {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_mask_filter(blur_mask.clone());
|
||||
style.set_foreground_paint(&paint);
|
||||
paint = merge_fills(&self.fills, *content_bounds);
|
||||
}
|
||||
|
||||
style.set_foreground_paint(&paint);
|
||||
style.set_font_size(self.font_size);
|
||||
style.set_letter_spacing(self.letter_spacing);
|
||||
style.set_half_leading(false);
|
||||
@@ -359,12 +357,20 @@ impl TextLeaf {
|
||||
&self,
|
||||
stroke_paint: &Paint,
|
||||
fallback_fonts: &HashSet<String>,
|
||||
blur: Option<&ImageFilter>,
|
||||
blur_mask: Option<&MaskFilter>,
|
||||
shadow: Option<&Paint>,
|
||||
remove_alpha: bool,
|
||||
) -> skia::textlayout::TextStyle {
|
||||
let mut style = self.to_style(&Rect::default(), fallback_fonts, blur, blur_mask, shadow);
|
||||
style.set_foreground_paint(stroke_paint);
|
||||
let mut style = self.to_style(&Rect::default(), fallback_fonts, remove_alpha);
|
||||
if remove_alpha {
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_style(stroke_paint.style());
|
||||
paint.set_stroke_width(stroke_paint.stroke_width());
|
||||
paint.set_color(skia::Color::BLACK);
|
||||
paint.set_alpha(255);
|
||||
style.set_foreground_paint(&paint);
|
||||
} else {
|
||||
style.set_foreground_paint(stroke_paint);
|
||||
}
|
||||
|
||||
style.set_font_size(self.font_size);
|
||||
style.set_letter_spacing(self.letter_spacing);
|
||||
style.set_decoration_type(match self.text_decoration {
|
||||
@@ -406,9 +412,6 @@ impl TextLeaf {
|
||||
}
|
||||
|
||||
pub fn is_transparent(&self) -> bool {
|
||||
if self.fills.is_empty() {
|
||||
return true;
|
||||
}
|
||||
self.fills.iter().all(|fill| match fill {
|
||||
shapes::Fill::Solid(shapes::SolidColor(color)) => color.a() == 0,
|
||||
_ => false,
|
||||
|
||||
@@ -20,7 +20,7 @@ impl TextPaths {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
let mut offset_y = self.bounds.y();
|
||||
let mut paragraphs = paragraph_builder_group_from_text(&self.0, None, None, None);
|
||||
let mut paragraphs = paragraph_builder_group_from_text(&self.0, None);
|
||||
|
||||
for paragraphs in paragraphs.iter_mut() {
|
||||
for paragraph_builder in paragraphs.iter_mut() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use skia_safe::{self as skia, textlayout::ParagraphBuilder, ImageFilter, MaskFilter, Paint, Rect};
|
||||
use skia_safe::{self as skia, textlayout::ParagraphBuilder, Paint, Rect};
|
||||
|
||||
use crate::{
|
||||
render::filters::compose_filters,
|
||||
shapes::{merge_fills, set_paint_fill, Stroke, StrokeKind, TextContent},
|
||||
utils::{get_fallback_fonts, get_font_collection},
|
||||
};
|
||||
@@ -51,13 +50,11 @@ pub fn build_paragraphs_with_width(
|
||||
.collect()
|
||||
}
|
||||
|
||||
type ParagraphBuilderGroup = Vec<ParagraphBuilder>;
|
||||
pub type ParagraphBuilderGroup = Vec<ParagraphBuilder>;
|
||||
|
||||
pub fn paragraph_builder_group_from_text(
|
||||
text_content: &TextContent,
|
||||
blur: Option<&ImageFilter>,
|
||||
blur_mask: Option<&MaskFilter>,
|
||||
shadow: Option<&Paint>,
|
||||
use_shadow: Option<bool>,
|
||||
) -> Vec<ParagraphBuilderGroup> {
|
||||
let fonts = get_font_collection();
|
||||
let fallback_fonts = get_fallback_fonts();
|
||||
@@ -67,13 +64,8 @@ pub fn paragraph_builder_group_from_text(
|
||||
let paragraph_style = paragraph.paragraph_to_style();
|
||||
let mut builder = ParagraphBuilder::new(¶graph_style, fonts);
|
||||
for leaf in paragraph.children() {
|
||||
let text_style = leaf.to_style(
|
||||
&text_content.bounds(),
|
||||
fallback_fonts,
|
||||
blur,
|
||||
blur_mask,
|
||||
shadow,
|
||||
);
|
||||
let remove_alpha = use_shadow.unwrap_or(false) && !leaf.is_transparent();
|
||||
let text_style = leaf.to_style(&text_content.bounds(), fallback_fonts, remove_alpha);
|
||||
let text = leaf.apply_text_transform();
|
||||
builder.push_style(&text_style);
|
||||
builder.add_text(&text);
|
||||
@@ -88,43 +80,27 @@ pub fn stroke_paragraph_builder_group_from_text(
|
||||
text_content: &TextContent,
|
||||
stroke: &Stroke,
|
||||
bounds: &Rect,
|
||||
blur: Option<&ImageFilter>,
|
||||
blur_mask: Option<&MaskFilter>,
|
||||
shadow: Option<&Paint>,
|
||||
count_inner_strokes: usize,
|
||||
use_shadow: Option<bool>,
|
||||
) -> Vec<ParagraphBuilderGroup> {
|
||||
let fallback_fonts = get_fallback_fonts();
|
||||
let fonts = get_font_collection();
|
||||
let mut paragraph_group = Vec::new();
|
||||
let remove_stroke_alpha = use_shadow.unwrap_or(false) && !stroke.is_transparent();
|
||||
|
||||
for paragraph in text_content.paragraphs() {
|
||||
let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for leaf in paragraph.children().iter() {
|
||||
let mut text_paint = merge_fills(leaf.fills(), *bounds);
|
||||
if let Some(blur_mask) = blur_mask {
|
||||
text_paint.set_mask_filter(blur_mask.clone());
|
||||
}
|
||||
|
||||
let stroke_paints = if shadow.is_some() {
|
||||
get_text_stroke_paints_with_shadows(
|
||||
stroke,
|
||||
blur,
|
||||
blur_mask,
|
||||
shadow,
|
||||
leaf.is_transparent(),
|
||||
)
|
||||
} else {
|
||||
get_text_stroke_paints(
|
||||
stroke,
|
||||
bounds,
|
||||
&text_paint,
|
||||
blur,
|
||||
blur_mask,
|
||||
count_inner_strokes,
|
||||
)
|
||||
};
|
||||
let text_paint: skia_safe::Handle<_> = merge_fills(leaf.fills(), *bounds);
|
||||
let stroke_paints = get_text_stroke_paints(
|
||||
stroke,
|
||||
bounds,
|
||||
&text_paint,
|
||||
count_inner_strokes,
|
||||
remove_stroke_alpha,
|
||||
);
|
||||
|
||||
let text: String = leaf.apply_text_transform();
|
||||
|
||||
@@ -134,8 +110,9 @@ pub fn stroke_paragraph_builder_group_from_text(
|
||||
ParagraphBuilder::new(¶graph_style, fonts)
|
||||
});
|
||||
let stroke_paint = stroke_paint.clone();
|
||||
let remove_alpha = use_shadow.unwrap_or(false) && !leaf.is_transparent();
|
||||
let stroke_style =
|
||||
leaf.to_stroke_style(&stroke_paint, fallback_fonts, blur, blur_mask, None);
|
||||
leaf.to_stroke_style(&stroke_paint, fallback_fonts, remove_alpha);
|
||||
builder.push_style(&stroke_style);
|
||||
builder.add_text(&text);
|
||||
}
|
||||
@@ -158,101 +135,12 @@ fn get_built_paragraphs(
|
||||
build_paragraphs_with_width(paragraphs, width)
|
||||
}
|
||||
|
||||
fn get_text_stroke_paints_with_shadows(
|
||||
stroke: &Stroke,
|
||||
blur: Option<&ImageFilter>,
|
||||
blur_mask: Option<&MaskFilter>,
|
||||
shadow: Option<&Paint>,
|
||||
is_transparent: bool,
|
||||
) -> Vec<Paint> {
|
||||
let mut paints = Vec::new();
|
||||
|
||||
match stroke.kind {
|
||||
StrokeKind::Inner => {
|
||||
let mut paint = Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
paint.set_anti_alias(true);
|
||||
|
||||
if let Some(blur) = blur {
|
||||
paint.set_image_filter(blur.clone());
|
||||
}
|
||||
|
||||
if let Some(shadow) = shadow {
|
||||
paint.set_image_filter(shadow.image_filter());
|
||||
}
|
||||
|
||||
paints.push(paint.clone());
|
||||
|
||||
if is_transparent {
|
||||
let image_filter = skia_safe::image_filters::erode(
|
||||
(stroke.width, stroke.width),
|
||||
paint.image_filter(),
|
||||
None,
|
||||
);
|
||||
paint.set_image_filter(image_filter);
|
||||
paint.set_blend_mode(skia::BlendMode::DstOut);
|
||||
paints.push(paint.clone());
|
||||
}
|
||||
}
|
||||
StrokeKind::Center => {
|
||||
let mut paint = skia_safe::Paint::default();
|
||||
paint.set_anti_alias(true);
|
||||
paint.set_stroke_width(stroke.width);
|
||||
|
||||
if let Some(blur) = blur {
|
||||
paint.set_image_filter(blur.clone());
|
||||
}
|
||||
|
||||
if let Some(shadow) = shadow {
|
||||
paint.set_image_filter(shadow.image_filter());
|
||||
}
|
||||
|
||||
if is_transparent {
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
} else {
|
||||
paint.set_style(skia::PaintStyle::StrokeAndFill);
|
||||
}
|
||||
|
||||
paints.push(paint);
|
||||
}
|
||||
StrokeKind::Outer => {
|
||||
let mut paint = skia_safe::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::StrokeAndFill);
|
||||
paint.set_anti_alias(true);
|
||||
paint.set_stroke_width(stroke.width * 2.0);
|
||||
|
||||
if let Some(blur_mask) = blur_mask {
|
||||
paint.set_mask_filter(blur_mask.clone());
|
||||
}
|
||||
|
||||
if let Some(shadow) = shadow {
|
||||
paint.set_image_filter(shadow.image_filter());
|
||||
}
|
||||
|
||||
paints.push(paint.clone());
|
||||
|
||||
if is_transparent {
|
||||
let image_filter = skia_safe::image_filters::erode(
|
||||
(stroke.width, stroke.width),
|
||||
paint.image_filter(),
|
||||
None,
|
||||
);
|
||||
paint.set_image_filter(image_filter);
|
||||
paint.set_blend_mode(skia::BlendMode::DstOut);
|
||||
paints.push(paint.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
paints
|
||||
}
|
||||
|
||||
fn get_text_stroke_paints(
|
||||
stroke: &Stroke,
|
||||
bounds: &Rect,
|
||||
text_paint: &Paint,
|
||||
blur: Option<&ImageFilter>,
|
||||
blur_mask: Option<&MaskFilter>,
|
||||
count_inner_strokes: usize,
|
||||
remove_stroke_alpha: bool,
|
||||
) -> Vec<Paint> {
|
||||
let mut paints = Vec::new();
|
||||
|
||||
@@ -269,34 +157,37 @@ fn get_text_stroke_paints(
|
||||
let mut paint = text_paint.clone();
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
paint.set_anti_alias(true);
|
||||
if let Some(blur) = blur {
|
||||
paint.set_image_filter(blur.clone());
|
||||
}
|
||||
paints.push(paint);
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
paint.set_blend_mode(skia::BlendMode::SrcIn);
|
||||
paint.set_anti_alias(true);
|
||||
paint.set_stroke_width(stroke.width * 2.0);
|
||||
set_paint_fill(&mut paint, &stroke.fill, bounds);
|
||||
if let Some(blur) = blur {
|
||||
paint.set_image_filter(blur.clone());
|
||||
}
|
||||
set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha);
|
||||
paints.push(paint);
|
||||
} else {
|
||||
let mut paint = text_paint.clone();
|
||||
let mut paint = skia::Paint::default();
|
||||
if remove_stroke_alpha {
|
||||
paint.set_color(skia::Color::BLACK);
|
||||
paint.set_alpha(255);
|
||||
} else {
|
||||
paint = text_paint.clone();
|
||||
set_paint_fill(&mut paint, &stroke.fill, bounds, false);
|
||||
}
|
||||
|
||||
paint.set_style(skia::PaintStyle::Fill);
|
||||
paint.set_anti_alias(false);
|
||||
set_paint_fill(&mut paint, &stroke.fill, bounds);
|
||||
paints.push(paint);
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
let image_filter =
|
||||
skia_safe::image_filters::erode((stroke.width, stroke.width), None, None);
|
||||
|
||||
let filter = compose_filters(blur, image_filter.as_ref());
|
||||
paint.set_image_filter(filter);
|
||||
paint.set_image_filter(image_filter);
|
||||
paint.set_anti_alias(false);
|
||||
paint.set_color(skia::Color::BLACK);
|
||||
paint.set_alpha(255);
|
||||
paint.set_blend_mode(skia::BlendMode::DstOut);
|
||||
paints.push(paint);
|
||||
}
|
||||
@@ -306,12 +197,7 @@ fn get_text_stroke_paints(
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
paint.set_anti_alias(true);
|
||||
paint.set_stroke_width(stroke.width);
|
||||
|
||||
set_paint_fill(&mut paint, &stroke.fill, bounds);
|
||||
if let Some(blur) = blur {
|
||||
paint.set_image_filter(blur.clone());
|
||||
}
|
||||
|
||||
set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha);
|
||||
paints.push(paint);
|
||||
}
|
||||
StrokeKind::Outer => {
|
||||
@@ -320,10 +206,7 @@ fn get_text_stroke_paints(
|
||||
paint.set_blend_mode(skia::BlendMode::DstOver);
|
||||
paint.set_anti_alias(true);
|
||||
paint.set_stroke_width(stroke.width * 2.0);
|
||||
set_paint_fill(&mut paint, &stroke.fill, bounds);
|
||||
if let Some(blur_mask) = blur_mask {
|
||||
paint.set_mask_filter(blur_mask.clone());
|
||||
}
|
||||
set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha);
|
||||
paints.push(paint);
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
|
||||
@@ -46,7 +46,7 @@ pub extern "C" fn get_text_dimensions() -> *mut u8 {
|
||||
if let Type::Text(content) = &shape.shape_type {
|
||||
// 1. Reset Paragraphs
|
||||
let paragraph_width = content.width();
|
||||
let mut paragraphs = paragraph_builder_group_from_text(content, None, None, None);
|
||||
let mut paragraphs = paragraph_builder_group_from_text(content, None);
|
||||
let built_paragraphs = build_paragraphs_with_width(&mut paragraphs, paragraph_width);
|
||||
|
||||
// 2. Max Width Calculation
|
||||
@@ -58,14 +58,12 @@ pub extern "C" fn get_text_dimensions() -> *mut u8 {
|
||||
// 3. Width and Height Calculation
|
||||
match content.grow_type() {
|
||||
GrowType::AutoHeight => {
|
||||
let mut paragraph_height =
|
||||
paragraph_builder_group_from_text(content, None, None, None);
|
||||
let mut paragraph_height = paragraph_builder_group_from_text(content, None);
|
||||
height = auto_height(&mut paragraph_height, paragraph_width).ceil();
|
||||
}
|
||||
GrowType::AutoWidth => {
|
||||
width = paragraph_width;
|
||||
let mut paragraph_height =
|
||||
paragraph_builder_group_from_text(content, None, None, None);
|
||||
let mut paragraph_height = paragraph_builder_group_from_text(content, None);
|
||||
height = auto_height(&mut paragraph_height, paragraph_width).ceil();
|
||||
}
|
||||
GrowType::Fixed => {}
|
||||
|
||||