From 8db5b6e07f3fa95139155806a1f6da8ba91ec424 Mon Sep 17 00:00:00 2001 From: Martin Guillon Date: Thu, 8 Oct 2026 23:20:28 +0200 Subject: [PATCH] fix(android): don't copy cached typeface when cloning a Font FontBase.cloneOrDirty() used Object.assign to copy the source font into the clone, which also copied the Android cached `_typeface`. Clones returned by withFontWeight/withFontFamily/withFontStyle/withFontSize/... then kept returning the stale typeface from getAndroidTypeface(), so the new property was ignored once the source font had been used. Reset `_typeface` for clones too, not only for dirtied fonts. Co-Authored-By: Claude Opus 5.5 --- packages/core/ui/styling/font-common.ts | 5 ++-- packages/core/ui/styling/font.spec.ts | 39 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 packages/core/ui/styling/font.spec.ts diff --git a/packages/core/ui/styling/font-common.ts b/packages/core/ui/styling/font-common.ts index e949128ecf..7e440585ce 100644 --- a/packages/core/ui/styling/font-common.ts +++ b/packages/core/ui/styling/font-common.ts @@ -20,9 +20,10 @@ export abstract class FontBase implements Font { Object.assign(clone, this); } else { clone.isDirty = true; - // clear android cached typeface. need to find a better way - clone['_typeface'] = null; } + // clear android cached typeface (Object.assign copies it to clones too) + // so it is recreated from the updated font properties + clone['_typeface'] = null; return clone; } diff --git a/packages/core/ui/styling/font.spec.ts b/packages/core/ui/styling/font.spec.ts new file mode 100644 index 0000000000..7372f447ca --- /dev/null +++ b/packages/core/ui/styling/font.spec.ts @@ -0,0 +1,39 @@ +import { Font } from './font'; + +describe('Font', () => { + describe('cloneOrDirty', () => { + // simulate the android cached typeface set by Font.getAndroidTypeface() + const fontWithCachedTypeface = () => { + const font = new Font('serif', 12, 'normal', 'normal'); + font['_typeface'] = { cached: true }; + return font; + }; + + it('clones do not carry the cached typeface', () => { + const font = fontWithCachedTypeface(); + const clones = [font.withFontWeight('700'), font.withFontFamily('monospace'), font.withFontStyle('italic'), font.withFontSize(20), font.withFontScale(2), font.withFontVariationSettings([{ axis: 'wght', value: 700 }])]; + for (const clone of clones) { + expect(clone).not.toBe(font); + expect(clone['_typeface']).toBeNull(); + } + // source font keeps its own cache + expect(font['_typeface']).toEqual({ cached: true }); + }); + + it('clones keep the font descriptor and apply the new property', () => { + const clone = fontWithCachedTypeface().withFontWeight('700'); + expect(clone.fontFamily).toBe('serif'); + expect(clone.fontSize).toBe(12); + expect(clone.fontStyle).toBe('normal'); + expect(clone.fontWeight).toBe('700'); + }); + + it('dirtied (non cloned) font clears the cached typeface', () => { + const font = fontWithCachedTypeface(); + const result = font.withFontWeight('700', false); + expect(result).toBe(font); + expect(result.isDirty).toBe(true); + expect(result['_typeface']).toBeNull(); + }); + }); +});