mirror of
https://github.com/penpot/penpot.git
synced 2026-07-01 20:05:26 +00:00
🐛 Fix font and variant dropdowns on mixed text styles (#10520)
* 🐛 Fix dropdown shown Mixed Font Families for same family with different variant * 🐛 Fix variants dropdown appearing blank on mixed variants but same family * ✨ Add playwright test for mixed font families/variants
This commit is contained in:
parent
64ba70e6f3
commit
6d458c80a1
@ -0,0 +1,372 @@
|
||||
{
|
||||
"~: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",
|
||||
"text-editor-wasm/v1"
|
||||
]
|
||||
},
|
||||
"~: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": "simple-text",
|
||||
"~: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": [
|
||||
"~u7274a6af-66db-8009-8006-52c837bed25d"
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u7274a6af-66db-8009-8006-52c837bed25d": {
|
||||
"~#shape": {
|
||||
"~:y": 368.000005463652,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:auto-width",
|
||||
"~:content": {
|
||||
"~:type": "root",
|
||||
"~:key": "13hr3ftth2o",
|
||||
"~: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": "1qm8gi1rphc",
|
||||
"~:font-size": "48",
|
||||
"~: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": "Hello "
|
||||
},
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:font-id": "gfont-sora",
|
||||
"~:key": "2qm8gi1rphd",
|
||||
"~:font-size": "48",
|
||||
"~: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": "Sora",
|
||||
"~:text": "World"
|
||||
}
|
||||
],
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "r8gahivbg7",
|
||||
"~: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": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"~:vertical-align": "top"
|
||||
},
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "this is a text",
|
||||
"~:width": 237.0000390021974,
|
||||
"~:type": "~:text",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 414.9999714372273,
|
||||
"~:y": 368.000005463652
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 652.0000104394247,
|
||||
"~:y": 368.000005463652
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 652.0000104394247,
|
||||
"~:y": 426.0000039162686
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 414.9999714372273,
|
||||
"~:y": 426.0000039162686
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:layout-item-h-sizing": "~:fix",
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:layout-item-v-sizing": "~:fix",
|
||||
"~:id": "~u7274a6af-66db-8009-8006-52c837bed25d",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:x": 414.9999714372274,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 414.9999714372274,
|
||||
"~:y": 368.000005463652,
|
||||
"~:width": 237.0000390021974,
|
||||
"~:height": 57.99999845261664,
|
||||
"~:x1": 414.9999714372274,
|
||||
"~:y1": 368.000005463652,
|
||||
"~:x2": 652.0000104394248,
|
||||
"~:y2": 426.0000039162686
|
||||
}
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 57.99999845261664,
|
||||
"~:flip-y": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c73",
|
||||
"~:name": "Page 1"
|
||||
}
|
||||
},
|
||||
"~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c72",
|
||||
"~:options": {
|
||||
"~:components-v2": true,
|
||||
"~:base-font-size": "16px"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,372 @@
|
||||
{
|
||||
"~: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",
|
||||
"text-editor-wasm/v1"
|
||||
]
|
||||
},
|
||||
"~: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": "simple-text",
|
||||
"~: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": [
|
||||
"~u7274a6af-66db-8009-8006-52c837bed25d"
|
||||
]
|
||||
}
|
||||
},
|
||||
"~u7274a6af-66db-8009-8006-52c837bed25d": {
|
||||
"~#shape": {
|
||||
"~:y": 368.000005463652,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:auto-width",
|
||||
"~:content": {
|
||||
"~:type": "root",
|
||||
"~:key": "13hr3ftth2o",
|
||||
"~: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": "1qm8gi1rphc",
|
||||
"~:font-size": "48",
|
||||
"~: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": "Hello "
|
||||
},
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "italic",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "2qm8gi1rphd",
|
||||
"~:font-size": "48",
|
||||
"~:font-weight": "600",
|
||||
"~:typography-ref-file": null,
|
||||
"~:font-variant-id": "600italic",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "World"
|
||||
}
|
||||
],
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "r8gahivbg7",
|
||||
"~: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": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"~:vertical-align": "top"
|
||||
},
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "this is a text",
|
||||
"~:width": 237.0000390021974,
|
||||
"~:type": "~:text",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 414.9999714372273,
|
||||
"~:y": 368.000005463652
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 652.0000104394247,
|
||||
"~:y": 368.000005463652
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 652.0000104394247,
|
||||
"~:y": 426.0000039162686
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 414.9999714372273,
|
||||
"~:y": 426.0000039162686
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:layout-item-h-sizing": "~:fix",
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:layout-item-v-sizing": "~:fix",
|
||||
"~:id": "~u7274a6af-66db-8009-8006-52c837bed25d",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:x": 414.9999714372274,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 414.9999714372274,
|
||||
"~:y": 368.000005463652,
|
||||
"~:width": 237.0000390021974,
|
||||
"~:height": 57.99999845261664,
|
||||
"~:x1": 414.9999714372274,
|
||||
"~:y1": 368.000005463652,
|
||||
"~:x2": 652.0000104394248,
|
||||
"~:y2": 426.0000039162686
|
||||
}
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 57.99999845261664,
|
||||
"~:flip-y": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c73",
|
||||
"~:name": "Page 1"
|
||||
}
|
||||
},
|
||||
"~:id": "~u3b0d758a-8c9d-8013-8006-52c8337e5c72",
|
||||
"~:options": {
|
||||
"~:components-v2": true,
|
||||
"~:base-font-size": "16px"
|
||||
}
|
||||
}
|
||||
}
|
||||
69
frontend/playwright/ui/specs/text-editor-v3.spec.js
Normal file
69
frontend/playwright/ui/specs/text-editor-v3.spec.js
Normal file
@ -0,0 +1,69 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
|
||||
|
||||
const FILE = {
|
||||
id: "3b0d758a-8c9d-8013-8006-52c8337e5c72",
|
||||
pageId: "3b0d758a-8c9d-8013-8006-52c8337e5c73",
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WasmWorkspacePage.init(page);
|
||||
// WASM_FLAGS already enables render-wasm; add the WASM text editor on top.
|
||||
await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-wasm"]);
|
||||
});
|
||||
|
||||
async function openEditorAndSelectAll(workspace) {
|
||||
await workspace.clickLeafLayer("this is a text");
|
||||
// Enter edit mode (waits until the typography controls are ready) and then
|
||||
// select every character so the sidebar reflects the combined styles of the
|
||||
// whole text via the WASM editor path.
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.page.keyboard.press("ControlOrMeta+a");
|
||||
}
|
||||
|
||||
test.describe("BUG 10502 - Mixed families and variants", () => {
|
||||
test("Multiple variants of the same font family", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page, { textEditor: true });
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-10502-mixed-variants.json");
|
||||
|
||||
await workspace.goToWorkspace(FILE);
|
||||
await workspace.waitForFirstRender();
|
||||
|
||||
await openEditorAndSelectAll(workspace);
|
||||
|
||||
// The whole selection shares a single font family, so it must be shown even
|
||||
// though the variants differ.
|
||||
const fontFamily = workspace.rightSidebar.getByTitle("Font Family");
|
||||
await expect(fontFamily).toContainText("Source Sans Pro");
|
||||
|
||||
// The variants differ across the selection, so the variant dropdown shows the
|
||||
// "mixed" placeholder.
|
||||
const fontVariant = workspace.rightSidebar
|
||||
.getByTitle("Font Style")
|
||||
.getByRole("combobox");
|
||||
await expect(fontVariant).toHaveText("--");
|
||||
});
|
||||
|
||||
test("Mixed font families appear as such in the dropdown", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page, { textEditor: true });
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-10502-mixed-families.json");
|
||||
// Serve a stand-in TTF for Sora so the render doesn't wait on a real fetch.
|
||||
// Glyphs are irrelevant here: the assertion only inspects the sidebar.
|
||||
await workspace.mockGoogleFont("sora", "render-wasm/assets/ebgaramond.ttf");
|
||||
|
||||
await workspace.goToWorkspace(FILE);
|
||||
await workspace.waitForFirstRender();
|
||||
|
||||
await openEditorAndSelectAll(workspace);
|
||||
|
||||
// The selection mixes two different font families (Source Sans Pro and Sora),
|
||||
// so the font family dropdown reports it as mixed.
|
||||
const fontFamily = workspace.rightSidebar.getByTitle("Font Family");
|
||||
await expect(fontFamily).toContainText("Mixed Font Families");
|
||||
});
|
||||
});
|
||||
|
||||
@ -360,6 +360,9 @@
|
||||
{:value (:id variant)
|
||||
:key (pr-str variant)
|
||||
:label (:name variant)})))
|
||||
;; When the selection mixes variants we prepend a "--" entry: it is
|
||||
;; shown as the collapsed value (nothing single is selected) while
|
||||
;; the real variants of the resolved font are still listed below it.
|
||||
variant-options (if (or (= font-variant-id :multiple) (= font-variant-id "mixed"))
|
||||
(conj basic-variant-options
|
||||
{:value ""
|
||||
|
||||
@ -286,6 +286,13 @@
|
||||
default (unchecked-get values "parent")]
|
||||
(d/nilv (unchecked-get values (d/name kind)) default)))
|
||||
|
||||
|
||||
(defn translate-multiple-state
|
||||
[multiple-state]
|
||||
(let [values (unchecked-get wasm/serializers "multiple-state")
|
||||
default (unchecked-get values "undefined")]
|
||||
(d/nilv (unchecked-get values (d/name multiple-state)) default)))
|
||||
|
||||
;; --- Guides
|
||||
|
||||
;; Each guide is serialized as 5 x 32-bit words:
|
||||
|
||||
@ -13,8 +13,11 @@
|
||||
[app.render-wasm.api.fonts :as fonts]
|
||||
[app.render-wasm.helpers :as h]
|
||||
[app.render-wasm.mem :as mem]
|
||||
[app.render-wasm.serializers :as sr]
|
||||
[app.render-wasm.wasm :as wasm]))
|
||||
|
||||
(def multiple-state-multiple (sr/translate-multiple-state :multiple))
|
||||
|
||||
(def ^:const TEXT_EDITOR_STYLES_METADATA_SIZE (* 31 4))
|
||||
(def ^:const TEXT_EDITOR_STYLES_FILL_SOLID 0)
|
||||
(def ^:const TEXT_EDITOR_STYLES_FILL_LINEAR_GRADIENT 1)
|
||||
@ -260,33 +263,41 @@
|
||||
text-direction-state (aget heap-u32 (+ u32-offset 2))
|
||||
text-decoration-state (aget heap-u32 (+ u32-offset 3))
|
||||
text-transform-state (aget heap-u32 (+ u32-offset 4))
|
||||
font-family-state (aget heap-u32 (+ u32-offset 5))
|
||||
font-family-id-state (aget heap-u32 (+ u32-offset 5))
|
||||
font-size-state (aget heap-u32 (+ u32-offset 6))
|
||||
font-weight-state (aget heap-u32 (+ u32-offset 7))
|
||||
font-variant-id-state (aget heap-u32 (+ u32-offset 8))
|
||||
;; Unused: the variant id is stored as a zero uuid for every span
|
||||
_font-variant-id-state (aget heap-u32 (+ u32-offset 8))
|
||||
line-height-state (aget heap-u32 (+ u32-offset 9))
|
||||
letter-spacing-state (aget heap-u32 (+ u32-offset 10))
|
||||
num-fills (aget heap-u32 (+ u32-offset 11))
|
||||
multiple-fills (aget heap-u32 (+ u32-offset 12))
|
||||
font-style-state (aget heap-u32 (+ u32-offset 11))
|
||||
num-fills (aget heap-u32 (+ u32-offset 12))
|
||||
multiple-fills (aget heap-u32 (+ u32-offset 13))
|
||||
|
||||
text-align-value (aget heap-u32 (+ u32-offset 13))
|
||||
text-direction-value (aget heap-u32 (+ u32-offset 14))
|
||||
text-decoration-value (aget heap-u32 (+ u32-offset 15))
|
||||
text-transform-value (aget heap-u32 (+ u32-offset 16))
|
||||
font-family-id-a (aget heap-u32 (+ u32-offset 17))
|
||||
font-family-id-b (aget heap-u32 (+ u32-offset 18))
|
||||
font-family-id-c (aget heap-u32 (+ u32-offset 19))
|
||||
font-family-id-d (aget heap-u32 (+ u32-offset 20))
|
||||
text-align-value (aget heap-u32 (+ u32-offset 14))
|
||||
text-direction-value (aget heap-u32 (+ u32-offset 15))
|
||||
text-decoration-value (aget heap-u32 (+ u32-offset 16))
|
||||
text-transform-value (aget heap-u32 (+ u32-offset 17))
|
||||
font-family-id-a (aget heap-u32 (+ u32-offset 18))
|
||||
font-family-id-b (aget heap-u32 (+ u32-offset 19))
|
||||
font-family-id-c (aget heap-u32 (+ u32-offset 20))
|
||||
font-family-id-d (aget heap-u32 (+ u32-offset 21))
|
||||
font-family-id-value (uuid/from-unsigned-parts font-family-id-a font-family-id-b font-family-id-c font-family-id-d)
|
||||
font-family-style-value (aget heap-u32 (+ u32-offset 21))
|
||||
_font-family-weight-value (aget heap-u32 (+ u32-offset 22))
|
||||
font-style-raw-value (aget heap-u32 (+ u32-offset 22))
|
||||
font-size-value (aget heap-f32 (+ u32-offset 23))
|
||||
font-weight-value (aget heap-i32 (+ u32-offset 24))
|
||||
line-height-value (aget heap-f32 (+ u32-offset 29))
|
||||
letter-spacing-value (aget heap-f32 (+ u32-offset 30))
|
||||
font-id (fonts/uuid->font-id font-family-id-value)
|
||||
font-style-value (text-editor-translate-font-style (text-editor-get-style-property font-family-state font-family-style-value))
|
||||
font-style-value (text-editor-translate-font-style (text-editor-get-style-property font-style-state font-style-raw-value))
|
||||
font-variant-id-computed (text-editor-compute-font-variant-id font-id font-weight-value font-style-value)
|
||||
;; A font variant is defined by its family + weight + style, so it
|
||||
;; is "mixed" when any of those is mixed. When the family itself is
|
||||
;; mixed there is no single font to resolve variants against, so we
|
||||
;; also report the variant as mixed.
|
||||
font-variant-multiple? (or (= font-family-id-state multiple-state-multiple)
|
||||
(= font-weight-state multiple-state-multiple)
|
||||
(= font-style-state multiple-state-multiple))
|
||||
|
||||
fills (->> (range num-fills)
|
||||
(map (fn [idx]
|
||||
@ -313,9 +324,9 @@
|
||||
:font-size (text-editor-get-style-property font-size-state font-size-value)
|
||||
:font-weight (text-editor-get-style-property font-weight-state font-weight-value)
|
||||
:font-style font-style-value
|
||||
:font-family (text-editor-get-style-property font-family-state font-id)
|
||||
:font-id (text-editor-get-style-property font-family-state font-id)
|
||||
:font-variant-id (text-editor-get-style-property font-variant-id-state font-variant-id-computed)
|
||||
:font-family (text-editor-get-style-property font-family-id-state font-id)
|
||||
:font-id (text-editor-get-style-property font-family-id-state font-id)
|
||||
:font-variant-id (if font-variant-multiple? :multiple font-variant-id-computed)
|
||||
:typography-ref-file nil
|
||||
:typography-ref-id nil
|
||||
:selected-colors selected-colors
|
||||
|
||||
@ -75,6 +75,7 @@
|
||||
:text-direction shared/RawTextDirection
|
||||
:text-decoration shared/RawTextDecoration
|
||||
:text-transform shared/RawTextTransform
|
||||
:multiple-state shared/MultipleState
|
||||
:transform-entry-kind shared/RawTransformEntryKind
|
||||
:segment-data shared/RawSegmentData
|
||||
:stroke-linecap shared/RawStrokeLineCap
|
||||
|
||||
@ -38,9 +38,12 @@ impl FontFamily {
|
||||
pub fn id(&self) -> Uuid {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn style(&self) -> FontStyle {
|
||||
self.style
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn weight(&self) -> u32 {
|
||||
self.weight
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
use macros::ToJs;
|
||||
|
||||
use crate::shapes::{
|
||||
Fill, FontFamily, TextAlign, TextContent, TextDecoration, TextDirection,
|
||||
Fill, FontStyle, TextAlign, TextContent, TextDecoration, TextDirection,
|
||||
TextPositionWithAffinity, TextTransform, VerticalAlign,
|
||||
};
|
||||
use crate::uuid::Uuid;
|
||||
@ -88,6 +88,7 @@ impl TextSelection {
|
||||
}
|
||||
|
||||
/// Events that the text editor can emit for frontend synchronization
|
||||
/// FIXME: the serialization should be in the wasm module
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, ToJs)]
|
||||
pub enum TextEditorEvent {
|
||||
@ -111,7 +112,12 @@ pub struct TextEditorStyles {
|
||||
pub text_direction: Multiple<TextDirection>, // Multiple
|
||||
pub text_decoration: Multiple<TextDecoration>,
|
||||
pub text_transform: Multiple<TextTransform>,
|
||||
pub font_family: Multiple<FontFamily>,
|
||||
// The font family is decomposed into its independent parts so the family
|
||||
// dropdown shows a single family even when the selection mixes variants of
|
||||
// the same family (e.g. regular + bold italic). The id identifies the
|
||||
// family; weight is tracked by `font_weight`; only the style is left here.
|
||||
pub font_family_id: Multiple<Uuid>,
|
||||
pub font_style: Multiple<FontStyle>,
|
||||
pub font_size: Multiple<f32>,
|
||||
pub font_weight: Multiple<i32>,
|
||||
pub font_variant_id: Multiple<Uuid>,
|
||||
@ -121,7 +127,8 @@ pub struct TextEditorStyles {
|
||||
pub fills: Vec<Fill>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
// FIXME: the serialization should be in the wasm module
|
||||
#[derive(Debug, Clone, Copy, PartialEq, ToJs)]
|
||||
#[repr(u8)]
|
||||
pub enum MultipleState {
|
||||
Undefined = 0,
|
||||
@ -228,7 +235,8 @@ impl TextEditorStyles {
|
||||
text_direction: Multiple::empty(),
|
||||
text_decoration: Multiple::empty(),
|
||||
text_transform: Multiple::empty(),
|
||||
font_family: Multiple::empty(),
|
||||
font_family_id: Multiple::empty(),
|
||||
font_style: Multiple::empty(),
|
||||
font_size: Multiple::empty(),
|
||||
font_weight: Multiple::empty(),
|
||||
font_variant_id: Multiple::empty(),
|
||||
@ -244,7 +252,8 @@ impl TextEditorStyles {
|
||||
self.text_direction.reset();
|
||||
self.text_decoration.reset();
|
||||
self.text_transform.reset();
|
||||
self.font_family.reset();
|
||||
self.font_family_id.reset();
|
||||
self.font_style.reset();
|
||||
self.font_size.reset();
|
||||
self.font_weight.reset();
|
||||
self.font_variant_id.reset();
|
||||
@ -614,8 +623,11 @@ impl TextEditorState {
|
||||
.text_transform
|
||||
.merge(span.text_transform);
|
||||
self.current_styles
|
||||
.font_family
|
||||
.merge(Some(span.font_family));
|
||||
.font_family_id
|
||||
.merge(Some(span.font_family.id()));
|
||||
self.current_styles
|
||||
.font_style
|
||||
.merge(Some(span.font_family.style()));
|
||||
self.current_styles.font_size.merge(Some(span.font_size));
|
||||
self.current_styles
|
||||
.font_weight
|
||||
@ -667,8 +679,11 @@ impl TextEditorState {
|
||||
.text_transform
|
||||
.set_single(text_span.text_transform);
|
||||
self.current_styles
|
||||
.font_family
|
||||
.set_single(Some(text_span.font_family));
|
||||
.font_family_id
|
||||
.set_single(Some(text_span.font_family.id()));
|
||||
self.current_styles
|
||||
.font_style
|
||||
.set_single(Some(text_span.font_family.style()));
|
||||
self.current_styles
|
||||
.font_size
|
||||
.set_single(Some(text_span.font_size));
|
||||
|
||||
@ -722,27 +722,20 @@ pub extern "C" fn text_editor_get_current_styles() -> *mut u8 {
|
||||
.unwrap_or(RawTextTransform::None as u32);
|
||||
|
||||
let font_family_id = styles
|
||||
.font_family
|
||||
.font_family_id
|
||||
.value()
|
||||
.as_ref()
|
||||
.map(|value| {
|
||||
let (a, b, c, d) = uuid_to_u32_quartet(&value.id());
|
||||
let (a, b, c, d) = uuid_to_u32_quartet(value);
|
||||
[a, b, c, d]
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let font_family_weight = styles
|
||||
.font_family
|
||||
let font_style = styles
|
||||
.font_style
|
||||
.value()
|
||||
.as_ref()
|
||||
.map(|value| value.weight())
|
||||
.unwrap_or_default();
|
||||
|
||||
let font_family_style = styles
|
||||
.font_family
|
||||
.value()
|
||||
.as_ref()
|
||||
.map(|value| value.style() as u32)
|
||||
.map(|value| *value as u32)
|
||||
.unwrap_or_default();
|
||||
|
||||
let font_size = styles.font_size.value().unwrap_or(0.0);
|
||||
@ -767,35 +760,35 @@ pub extern "C" fn text_editor_get_current_styles() -> *mut u8 {
|
||||
}
|
||||
}
|
||||
|
||||
// Layout: 48-byte fixed header + fixed values + serialized fills.
|
||||
// Layout: 56-byte fixed header + fixed values + serialized fills.
|
||||
let mut bytes = Vec::with_capacity(132 + fill_bytes.len());
|
||||
|
||||
// Header data // offset // index
|
||||
// Header data (multiple-states) // offset // index
|
||||
bytes.extend_from_slice(&vertical_align.to_le_bytes()); // 0 // 0
|
||||
bytes.extend_from_slice(&(*styles.text_align.state() as u32).to_le_bytes()); // 4 // 1
|
||||
bytes.extend_from_slice(&(*styles.text_direction.state() as u32).to_le_bytes()); // 8 // 2
|
||||
bytes.extend_from_slice(&(*styles.text_decoration.state() as u32).to_le_bytes()); // 12 // 3
|
||||
bytes.extend_from_slice(&(*styles.text_transform.state() as u32).to_le_bytes()); // 16 // 4
|
||||
bytes.extend_from_slice(&(*styles.font_family.state() as u32).to_le_bytes()); // 20 // 5
|
||||
bytes.extend_from_slice(&(*styles.font_family_id.state() as u32).to_le_bytes()); // 20 // 5
|
||||
bytes.extend_from_slice(&(*styles.font_size.state() as u32).to_le_bytes()); // 24 // 6
|
||||
bytes.extend_from_slice(&(*styles.font_weight.state() as u32).to_le_bytes()); // 28 // 7
|
||||
bytes.extend_from_slice(&(*styles.font_variant_id.state() as u32).to_le_bytes()); // 32 // 8
|
||||
bytes.extend_from_slice(&(*styles.line_height.state() as u32).to_le_bytes()); // 36 // 9
|
||||
bytes.extend_from_slice(&(*styles.letter_spacing.state() as u32).to_le_bytes()); // 40 // 10
|
||||
bytes.extend_from_slice(&fill_count.to_le_bytes()); // 44 // 11
|
||||
bytes.extend_from_slice(&(fill_multiple as u32).to_le_bytes()); // 48 // 12
|
||||
bytes.extend_from_slice(&(*styles.font_style.state() as u32).to_le_bytes()); // 44 // 11
|
||||
bytes.extend_from_slice(&fill_count.to_le_bytes()); // 48 // 12
|
||||
bytes.extend_from_slice(&(fill_multiple as u32).to_le_bytes()); // 52 // 13
|
||||
|
||||
// Value section.
|
||||
bytes.extend_from_slice(&text_align.to_le_bytes()); // 52 // 13
|
||||
bytes.extend_from_slice(&text_direction.to_le_bytes()); // 56 // 14
|
||||
bytes.extend_from_slice(&text_decoration.to_le_bytes()); // 60 // 15
|
||||
bytes.extend_from_slice(&text_transform.to_le_bytes()); // 64 // 16
|
||||
bytes.extend_from_slice(&font_family_id[0].to_le_bytes()); // 68 // 17
|
||||
bytes.extend_from_slice(&font_family_id[1].to_le_bytes()); // 72 // 18
|
||||
bytes.extend_from_slice(&font_family_id[2].to_le_bytes()); // 76 // 19
|
||||
bytes.extend_from_slice(&font_family_id[3].to_le_bytes()); // 80 // 20
|
||||
bytes.extend_from_slice(&font_family_style.to_le_bytes()); // 84 // 21
|
||||
bytes.extend_from_slice(&font_family_weight.to_le_bytes()); // 88 // 22
|
||||
bytes.extend_from_slice(&text_align.to_le_bytes()); // 56 // 14
|
||||
bytes.extend_from_slice(&text_direction.to_le_bytes()); // 60 // 15
|
||||
bytes.extend_from_slice(&text_decoration.to_le_bytes()); // 64 // 16
|
||||
bytes.extend_from_slice(&text_transform.to_le_bytes()); // 68 // 17
|
||||
bytes.extend_from_slice(&font_family_id[0].to_le_bytes()); // 72 // 18
|
||||
bytes.extend_from_slice(&font_family_id[1].to_le_bytes()); // 76 // 19
|
||||
bytes.extend_from_slice(&font_family_id[2].to_le_bytes()); // 80 // 20
|
||||
bytes.extend_from_slice(&font_family_id[3].to_le_bytes()); // 84 // 21
|
||||
bytes.extend_from_slice(&font_style.to_le_bytes()); // 88 // 22
|
||||
bytes.extend_from_slice(&font_size.to_le_bytes()); // 92 // 23
|
||||
bytes.extend_from_slice(&font_weight.to_le_bytes()); // 96 // 24
|
||||
bytes.extend_from_slice(&font_variant_id[0].to_le_bytes()); // 100 // 25
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user