🐛 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:
Belén Albeza 2026-07-01 20:15:20 +02:00 committed by GitHub
parent 64ba70e6f3
commit 6d458c80a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 900 additions and 54 deletions

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}

View 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");
});
});

View File

@ -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 ""

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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));

View File

@ -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