From cecd467dd2e2cbdb90a92ce29b76f5ee271fcdef Mon Sep 17 00:00:00 2001 From: Thomas Weber Date: Sat, 31 Oct 2026 15:31:42 -0500 Subject: [PATCH] Wait for custom fonts to load before continuing (#401) --- src/engine/tw-font-manager.js | 16 +++++++--- test/integration/tw_font_manager.js | 46 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/engine/tw-font-manager.js b/src/engine/tw-font-manager.js index f617a67f..54b98b0f 100644 --- a/src/engine/tw-font-manager.js +++ b/src/engine/tw-font-manager.js @@ -160,6 +160,7 @@ class FontManager extends EventEmitter { * @param {string} family * @param {string} fallback * @param {Asset} asset scratch-storage asset + * @returns {Promise} Resolves when the font is ready. */ addCustomFont (family, fallback, asset) { if (!this.isValidCustomFont(family)) { @@ -171,8 +172,9 @@ class FontManager extends EventEmitter { fallback, asset }); - this.updateRenderer(); + const promise = this.updateRenderer(); this.changed(); + return promise; } /** @@ -208,9 +210,12 @@ class FontManager extends EventEmitter { this.changed(); } + /** + * @returns {Promise} Resolves when fonts are ready. + */ updateRenderer () { if (!this.runtime.renderer || !this.runtime.renderer.setCustomFonts) { - return; + return Promise.resolve(); } const fontfaces = {}; @@ -222,7 +227,7 @@ class FontManager extends EventEmitter { fontfaces[family] = fontface; } } - this.runtime.renderer.setCustomFonts(fontfaces); + return this.runtime.renderer.setCustomFonts(fontfaces); } /** @@ -273,6 +278,7 @@ class FontManager extends EventEmitter { return; } + const fontPromises = []; for (const font of json) { if (!font || typeof font !== 'object') { continue; @@ -305,12 +311,14 @@ class FontManager extends EventEmitter { this.runtime.storage.AssetType.Font, md5ext ); - this.addCustomFont(family, fallback, asset); + fontPromises.push(this.addCustomFont(family, fallback, asset)); } } catch (e) { log.error('could not add font', e); } } + + await Promise.all(fontPromises); } } diff --git a/test/integration/tw_font_manager.js b/test/integration/tw_font_manager.js index 742799ce..834221e7 100644 --- a/test/integration/tw_font_manager.js +++ b/test/integration/tw_font_manager.js @@ -5,6 +5,7 @@ const assert = require('node:assert'); const _makeTestStorage = require('../fixtures/make-test-storage'); const Runtime = require('../../src/engine/runtime'); const VirtualMachine = require('../../src/virtual-machine'); +const JSZip = require('@turbowarp/jszip'); const emptyProjectFixture = path.join(__dirname, '..', 'fixtures', 'tw-empty-project.sb3'); @@ -731,3 +732,48 @@ test('overriding existing fonts', () => { assert.strictEqual(setCustomFontsCalls, 3); assert.deepEqual(fontManager.getFonts().map(i => i.name), ['TESTFONT']); }); + +test('deserialize waits for renderer to load fonts', async () => { + const zip = new JSZip(); + zip.file('8e0b5e4ad4e3b2b9a8b4e9bd0ec4de2c.ttf', new Uint8Array([1, 2, 3])); + + let resolveSetCustomFontsCalled; + const setCustomFontsCalled = new Promise(resolve => { + resolveSetCustomFontsCalled = resolve; + }); + let resolveFontsLoaded; + const mockRenderer = { + setLayerGroupOrdering: () => {}, + setCustomFonts: () => { + resolveSetCustomFontsCalled(); + return new Promise(resolve => { + resolveFontsLoaded = resolve; + }); + } + }; + + const rt = new Runtime(); + rt.attachRenderer(mockRenderer); + rt.attachStorage(makeTestStorage()); + + let deserialized = false; + const promise = rt.fontManager.deserialize([ + { + system: false, + family: 'TestFont', + fallback: 'sans-serif', + md5ext: '8e0b5e4ad4e3b2b9a8b4e9bd0ec4de2c.ttf' + } + ], zip, false).then(() => { + deserialized = true; + }); + + await setCustomFontsCalled; + await new Promise(resolve => setTimeout(resolve)); + assert.strictEqual(deserialized, false, 'still waiting for fonts'); + + resolveFontsLoaded(); + await promise; + assert.strictEqual(deserialized, true); + assert.deepEqual(rt.fontManager.getFonts().map(i => i.name), ['TestFont']); +});