uicore-ts 1.1.212 → 1.1.216

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -59,6 +59,7 @@ export declare class UITextMeasurement {
59
59
  /**
60
60
  * Measure text width using Canvas API
61
61
  */
62
+ private static _fontsLoadingSet;
62
63
  static measureTextWidth(text: string, font: string, letterSpacing?: number): number;
63
64
  /**
64
65
  * Split text into lines based on width constraint
@@ -189,6 +189,15 @@ class UITextMeasurement {
189
189
  static measureTextWidth(text, font, letterSpacing = 0) {
190
190
  const ctx = this.getContext();
191
191
  ctx.font = font;
192
+ if (!ctx.font.includes(font.split(",")[0].split(" ").pop().trim())) {
193
+ if (!this._fontsLoadingSet.has(font)) {
194
+ this._fontsLoadingSet.add(font);
195
+ document.fonts.load(font).then(() => {
196
+ this._fontsLoadingSet.delete(font);
197
+ });
198
+ }
199
+ return NaN;
200
+ }
192
201
  const baseWidth = ctx.measureText(text).width;
193
202
  return baseWidth + letterSpacing * text.length;
194
203
  }
@@ -198,6 +207,9 @@ class UITextMeasurement {
198
207
  }
199
208
  const ctx = this.getContext();
200
209
  ctx.font = font;
210
+ if (!ctx.font.includes(font.split(",")[0].split(" ").pop().trim())) {
211
+ return null;
212
+ }
201
213
  const lines = [];
202
214
  const paragraphs = text.split("\n");
203
215
  for (const paragraph of paragraphs) {
@@ -295,6 +307,9 @@ class UITextMeasurement {
295
307
  height = styles.lineHeight + styles.paddingTop + styles.paddingBottom;
296
308
  } else {
297
309
  const lines = this.wrapText(transformedText, availableWidth, styles.font, styles.whiteSpace);
310
+ if (!lines) {
311
+ return { width: NaN, height: NaN };
312
+ }
298
313
  width = Math.max(
299
314
  ...lines.map((line) => this.measureTextWidth(line, styles.font, styles.letterSpacing))
300
315
  ) + styles.paddingLeft + styles.paddingRight;
@@ -342,6 +357,7 @@ UITextMeasurement.context = null;
342
357
  UITextMeasurement.globalStyleCache = /* @__PURE__ */ new Map();
343
358
  UITextMeasurement.elementToCacheKey = /* @__PURE__ */ new WeakMap();
344
359
  UITextMeasurement.measurementElement = null;
360
+ UITextMeasurement._fontsLoadingSet = /* @__PURE__ */ new Set();
345
361
  // Annotate the CommonJS export names for ESM import in node:
346
362
  0 && (module.exports = {
347
363
  UITextMeasurement
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../scripts/UITextMeasurement.ts"],
4
- "sourcesContent": ["// UITextMeasurement.ts - Efficient text measurement without DOM reflows\n\nexport interface TextMeasurementStyle {\n font: string;\n fontSize: number;\n lineHeight: number;\n whiteSpace: string;\n paddingLeft: number;\n paddingRight: number;\n paddingTop: number;\n paddingBottom: number;\n letterSpacing: number;\n textTransform: string;\n}\n\nexport class UITextMeasurement {\n private static canvas: HTMLCanvasElement | null = null;\n private static context: CanvasRenderingContext2D | null = null;\n \n // Global cache for style objects using semantic cache key\n private static globalStyleCache = new Map<string, TextMeasurementStyle>();\n \n // Per-element cache to map element -> cache key\n private static elementToCacheKey = new WeakMap<HTMLElement, string>();\n \n // Temporary element for complex HTML measurements (reused to avoid allocations)\n private static measurementElement: HTMLDivElement | null = null;\n \n /**\n * Generate a cache key based only on styles that affect text measurement\n * Ignores position, color, transform, etc.\n */\n private static generateStyleCacheKey(computed: CSSStyleDeclaration): string {\n // Only include properties that affect text layout\n const relevantProps = [\n computed.fontFamily,\n computed.fontSize,\n computed.fontWeight,\n computed.fontStyle,\n computed.fontVariant,\n computed.lineHeight,\n computed.letterSpacing,\n computed.wordSpacing,\n computed.textTransform,\n computed.whiteSpace,\n computed.wordBreak,\n computed.wordWrap,\n computed.paddingLeft,\n computed.paddingRight,\n computed.paddingTop,\n computed.paddingBottom,\n computed.borderLeftWidth,\n computed.borderRightWidth,\n computed.borderTopWidth,\n computed.borderBottomWidth,\n computed.boxSizing\n ];\n \n // Create a hash-like key from relevant properties\n return relevantProps.join('|');\n }\n \n /**\n * Extract cache key from element's classList\n * Elements with same classes likely have same text measurement styles\n */\n private static getSemanticCacheKey(element: HTMLElement): string {\n // Check if we already computed a cache key for this element\n const existingKey = this.elementToCacheKey.get(element);\n if (existingKey) {\n return existingKey;\n }\n \n // Try to use class-based caching first (fastest)\n const classList = Array.from(element.classList).sort().join(' ');\n const tagName = element.tagName.toLowerCase();\n \n // Semantic key based on tag + classes\n const semanticKey = `${tagName}::${classList}`;\n \n // Check if we have styles for this semantic key\n if (this.globalStyleCache.has(semanticKey)) {\n this.elementToCacheKey.set(element, semanticKey);\n return semanticKey;\n }\n \n // If not cached, compute and use style-based key\n const computed = window.getComputedStyle(element);\n const styleCacheKey = this.generateStyleCacheKey(computed);\n \n // Use the style-based key\n this.elementToCacheKey.set(element, styleCacheKey);\n \n return styleCacheKey;\n }\n \n /**\n * Get or create the canvas context for text measurement\n */\n private static getContext(): CanvasRenderingContext2D {\n if (!this.context) {\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d')!;\n }\n return this.context;\n }\n \n /**\n * Detect if content is plain text or complex HTML\n */\n private static isPlainText(content: string): boolean {\n // Check for HTML tags (excluding simple formatting like <b>, <i>, <span>)\n const hasComplexHTML = /<(?!\\/?(b|i|em|strong|span|br)\\b)[^>]+>/i.test(content);\n return !hasComplexHTML;\n }\n \n /**\n * Check if content has only simple inline formatting\n */\n private static hasSimpleFormatting(content: string): boolean {\n // Only <b>, <i>, <strong>, <em>, <span> with inline styles\n const simpleTagPattern = /^[^<]*(?:<\\/?(?:b|i|em|strong|span)(?:\\s+style=\"[^\"]*\")?>[^<]*)*$/i;\n return simpleTagPattern.test(content);\n }\n \n /**\n * Get or create measurement element for complex HTML\n */\n private static getMeasurementElement(): HTMLDivElement {\n if (!this.measurementElement) {\n this.measurementElement = document.createElement('div');\n this.measurementElement.style.cssText = `\n position: absolute;\n visibility: hidden;\n pointer-events: none;\n top: -9999px;\n left: -9999px;\n width: auto;\n height: auto;\n `;\n }\n return this.measurementElement;\n }\n \n /**\n * Fast measurement using DOM (but optimized to minimize reflows)\n */\n private static measureWithDOM(\n element: HTMLElement,\n content: string,\n constrainingWidth?: number,\n constrainingHeight?: number,\n providedStyles?: TextMeasurementStyle\n ): { width: number; height: number } {\n const measureEl = this.getMeasurementElement();\n const styles = this.getElementStyles(element, providedStyles);\n \n // Copy relevant styles\n measureEl.style.font = styles.font;\n measureEl.style.lineHeight = styles.lineHeight + 'px';\n measureEl.style.whiteSpace = styles.whiteSpace;\n measureEl.style.padding = `${styles.paddingTop}px ${styles.paddingRight}px ${styles.paddingBottom}px ${styles.paddingLeft}px`;\n measureEl.style.letterSpacing = styles.letterSpacing ? styles.letterSpacing + 'px' : '';\n measureEl.style.textTransform = styles.textTransform || '';\n \n // Set constraints\n if (constrainingWidth) {\n measureEl.style.width = constrainingWidth + 'px';\n measureEl.style.maxWidth = constrainingWidth + 'px';\n } else {\n measureEl.style.width = 'auto';\n measureEl.style.maxWidth = 'none';\n }\n \n if (constrainingHeight) {\n measureEl.style.height = constrainingHeight + 'px';\n measureEl.style.maxHeight = constrainingHeight + 'px';\n } else {\n measureEl.style.height = 'auto';\n measureEl.style.maxHeight = 'none';\n }\n \n // Set content\n measureEl.innerHTML = content;\n \n // Add to DOM only if not already there\n if (!measureEl.parentElement) {\n document.body.appendChild(measureEl);\n }\n \n // Single reflow for both measurements\n const rect = measureEl.getBoundingClientRect();\n const result = {\n width: rect.width || measureEl.scrollWidth,\n height: rect.height || measureEl.scrollHeight\n };\n \n return result;\n }\n \n /**\n * Get or extract styles from element (with smart global caching)\n * Returns cached styles if available, otherwise computes once and caches globally\n */\n private static getElementStyles(element: HTMLElement, providedStyles?: TextMeasurementStyle): TextMeasurementStyle {\n // Use provided styles if available (avoids getComputedStyle entirely)\n if (providedStyles) {\n return providedStyles;\n }\n \n // Get semantic cache key\n const cacheKey = this.getSemanticCacheKey(element);\n \n // Check global cache\n const cached = this.globalStyleCache.get(cacheKey);\n if (cached) {\n return cached;\n }\n \n // Compute once and cache globally (this is the only getComputedStyle call)\n const computed = window.getComputedStyle(element);\n const fontSize = parseFloat(computed.fontSize);\n \n const styles: TextMeasurementStyle = {\n font: [\n computed.fontStyle,\n computed.fontVariant,\n computed.fontWeight,\n computed.fontSize,\n computed.fontFamily\n ].join(' '),\n fontSize: fontSize,\n lineHeight: this.parseLineHeight(computed.lineHeight, fontSize),\n whiteSpace: computed.whiteSpace,\n paddingLeft: parseFloat(computed.paddingLeft) || 0,\n paddingRight: parseFloat(computed.paddingRight) || 0,\n paddingTop: parseFloat(computed.paddingTop) || 0,\n paddingBottom: parseFloat(computed.paddingBottom) || 0,\n letterSpacing: parseFloat(computed.letterSpacing) || 0,\n textTransform: computed.textTransform || 'none'\n };\n \n this.globalStyleCache.set(cacheKey, styles);\n return styles;\n }\n \n /**\n * Parse line height from computed style\n */\n private static applyTextTransform(text: string, transform: string): string {\n switch (transform) {\n case 'uppercase': return text.toUpperCase();\n case 'lowercase': return text.toLowerCase();\n case 'capitalize': return text.replace(/\\b\\w/g, c => c.toUpperCase());\n default: return text;\n }\n }\n \n private static parseLineHeight(lineHeight: string, fontSize: number): number {\n if (lineHeight === 'normal') {\n return fontSize * 1.2;\n }\n \n if (lineHeight.endsWith('px')) {\n return parseFloat(lineHeight);\n }\n \n const numericLineHeight = parseFloat(lineHeight);\n if (!isNaN(numericLineHeight)) {\n return fontSize * numericLineHeight;\n }\n \n return fontSize * 1.2;\n }\n \n \n /**\n * Measure text width using Canvas API\n */\n static measureTextWidth(text: string, font: string, letterSpacing: number = 0): number {\n const ctx = this.getContext();\n ctx.font = font;\n const baseWidth = ctx.measureText(text).width;\n // Canvas measureText does not apply letter-spacing; add it manually.\n // letter-spacing applies between characters, so multiply by text length\n // (not text.length - 1) to match browser rendering which also adds it\n // after the last character in most implementations.\n return baseWidth + letterSpacing * text.length;\n }\n \n /**\n * Split text into lines based on width constraint\n */\n private static wrapText(\n text: string,\n maxWidth: number,\n font: string,\n whiteSpace: string\n ): string[] {\n // No wrapping needed\n if (whiteSpace === 'nowrap' || whiteSpace === 'pre') {\n return [text];\n }\n \n const ctx = this.getContext();\n ctx.font = font;\n \n const lines: string[] = [];\n const paragraphs = text.split('\\n');\n \n for (const paragraph of paragraphs) {\n if (whiteSpace === 'pre-wrap') {\n // Preserve whitespace but wrap at maxWidth\n lines.push(...this.wrapPreservingWhitespace(paragraph, maxWidth, ctx));\n } else {\n // Normal wrapping (collapse whitespace)\n lines.push(...this.wrapNormal(paragraph, maxWidth, ctx));\n }\n }\n \n return lines;\n }\n \n private static wrapNormal(\n text: string,\n maxWidth: number,\n ctx: CanvasRenderingContext2D\n ): string[] {\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const lines: string[] = [];\n let currentLine = '';\n \n for (const word of words) {\n const testLine = currentLine ? `${currentLine} ${word}` : word;\n const metrics = ctx.measureText(testLine);\n \n if (metrics.width > maxWidth && currentLine) {\n lines.push(currentLine);\n currentLine = word;\n } else {\n currentLine = testLine;\n }\n }\n \n if (currentLine) {\n lines.push(currentLine);\n }\n \n return lines.length > 0 ? lines : [''];\n }\n \n private static wrapPreservingWhitespace(\n text: string,\n maxWidth: number,\n ctx: CanvasRenderingContext2D\n ): string[] {\n const lines: string[] = [];\n let currentLine = '';\n \n for (let i = 0; i < text.length; i++) {\n const char = text[i];\n const testLine = currentLine + char;\n const metrics = ctx.measureText(testLine);\n \n if (metrics.width > maxWidth && currentLine) {\n lines.push(currentLine);\n currentLine = char;\n } else {\n currentLine = testLine;\n }\n }\n \n if (currentLine) {\n lines.push(currentLine);\n }\n \n return lines.length > 0 ? lines : [''];\n }\n \n /**\n * Calculate intrinsic content size for text - SMART METHOD\n * Automatically chooses the most efficient measurement technique\n */\n static calculateTextSize(\n element: HTMLElement,\n content: string,\n constrainingWidth?: number,\n constrainingHeight?: number,\n providedStyles?: TextMeasurementStyle\n ): { width: number; height: number } {\n // Empty content\n if (!content || content.length === 0) {\n const styles = this.getElementStyles(element, providedStyles);\n return {\n width: styles.paddingLeft + styles.paddingRight,\n height: styles.paddingTop + styles.paddingBottom\n };\n }\n \n // Check complexity of content\n const isPlain = this.isPlainText(content);\n const hasSimple = this.hasSimpleFormatting(content);\n \n // Strategy 1: Pure canvas for plain text (fastest)\n if (isPlain) {\n return this.calculatePlainTextSize(\n element,\n content,\n constrainingWidth,\n constrainingHeight,\n providedStyles\n );\n }\n \n // Strategy 2: Optimized DOM for simple formatting (fast)\n if (hasSimple) {\n // For simple formatting, we can still use canvas but need to strip tags\n const plainText = content.replace(/<[^>]+>/g, '');\n return this.calculatePlainTextSize(\n element,\n plainText,\n constrainingWidth,\n constrainingHeight,\n providedStyles\n );\n }\n \n // Strategy 3: DOM measurement for complex HTML (slower but accurate)\n return this.measureWithDOM(\n element,\n content,\n constrainingWidth,\n constrainingHeight,\n providedStyles\n );\n }\n \n /**\n * Calculate size for plain text using canvas (no HTML)\n */\n private static calculatePlainTextSize(\n element: HTMLElement,\n text: string,\n constrainingWidth?: number,\n constrainingHeight?: number,\n providedStyles?: TextMeasurementStyle\n ): { width: number; height: number } {\n const styles = this.getElementStyles(element, providedStyles);\n \n // Adjust constraining width for padding\n const availableWidth = constrainingWidth\n ? constrainingWidth - styles.paddingLeft - styles.paddingRight\n : Infinity;\n \n // Calculate dimensions\n let width: number;\n let height: number;\n \n // Apply text-transform before measuring, matching what the browser renders\n const transformedText = this.applyTextTransform(text, styles.textTransform);\n \n if (styles.whiteSpace === 'nowrap' || styles.whiteSpace === 'pre' || !constrainingWidth) {\n // Single line or no width constraint\n width = this.measureTextWidth(transformedText, styles.font, styles.letterSpacing) + styles.paddingLeft + styles.paddingRight;\n height = styles.lineHeight + styles.paddingTop + styles.paddingBottom;\n } else {\n // Multi-line text\n const lines = this.wrapText(transformedText, availableWidth, styles.font, styles.whiteSpace);\n \n // Find the widest line\n width = Math.max(\n ...lines.map(line => this.measureTextWidth(line, styles.font, styles.letterSpacing))\n ) + styles.paddingLeft + styles.paddingRight;\n \n height = (lines.length * styles.lineHeight) + styles.paddingTop + styles.paddingBottom;\n }\n \n return { width, height };\n }\n \n /**\n * Clear all caches (call when fonts change or for cleanup).\n * Also resets the canvas context so the next measureText call forces the\n * browser to re-resolve the font string against the now-loaded FontFace set.\n * Without this, ctx.font may silently fall back to the system font even\n * though getComputedStyle already reports the correct custom font.\n */\n static clearCaches(): void {\n this.globalStyleCache.clear();\n this.elementToCacheKey = new WeakMap();\n this.context = null;\n this.canvas = null;\n }\n \n /**\n * Invalidate cached styles for a specific element\n */\n static invalidateElement(element: HTMLElement): void {\n const cacheKey = this.elementToCacheKey.get(element);\n if (cacheKey) {\n this.globalStyleCache.delete(cacheKey);\n this.elementToCacheKey.delete(element);\n }\n }\n \n /**\n * Invalidate cache for elements with specific class\n * Useful when you change a CSS class definition\n */\n static invalidateClass(className: string): void {\n // Clear all cache keys that contain this class\n for (const [key] of this.globalStyleCache.entries()) {\n if (key.includes(className)) {\n this.globalStyleCache.delete(key);\n }\n }\n }\n \n /**\n * Pre-warm the cache by measuring a representative element\n * Useful at app startup to avoid first-paint delays\n */\n static prewarmCache(elements: HTMLElement[]): void {\n elements.forEach(el => {\n this.getElementStyles(el);\n });\n }\n \n /**\n * Clean up measurement element (call on app cleanup)\n */\n static cleanup(): void {\n if (this.measurementElement && this.measurementElement.parentElement) {\n document.body.removeChild(this.measurementElement);\n }\n this.measurementElement = null;\n this.canvas = null;\n this.context = null;\n this.clearCaches();\n }\n}\n\n// Extension methods to add to UITextView\nexport interface UITextViewMeasurementMethods {\n intrinsicContentSizeEfficient(\n constrainingWidth?: number,\n constrainingHeight?: number\n ): { width: number; height: number };\n}\n\n// ==================== INTEGRATION CODE ====================\n// Add these methods to UITextView class:\n\n/*\n // In UITextView class:\n \n // Add this property to track content complexity and cached styles\n private _useFastMeasurement: boolean | undefined;\n private _cachedMeasurementStyles: TextMeasurementStyle | undefined;\n \n // Call this when styles change (fontSize, padding, etc.)\n private _invalidateMeasurementStyles(): void {\n this._cachedMeasurementStyles = undefined;\n UITextMeasurement.invalidateElement(this.viewHTMLElement);\n this._intrinsicSizesCache = {};\n }\n \n // Extract styles ONCE and cache them (avoids getComputedStyle)\n private _getMeasurementStyles(): TextMeasurementStyle {\n if (this._cachedMeasurementStyles) {\n return this._cachedMeasurementStyles;\n }\n \n // Only call getComputedStyle once and cache the result\n const computed = window.getComputedStyle(this.viewHTMLElement);\n const fontSize = parseFloat(computed.fontSize);\n \n this._cachedMeasurementStyles = {\n font: [\n computed.fontStyle,\n computed.fontVariant,\n computed.fontWeight,\n computed.fontSize,\n computed.fontFamily\n ].join(' '),\n fontSize: fontSize,\n lineHeight: this._parseLineHeight(computed.lineHeight, fontSize),\n whiteSpace: computed.whiteSpace,\n paddingLeft: parseFloat(computed.paddingLeft) || 0,\n paddingRight: parseFloat(computed.paddingRight) || 0,\n paddingTop: parseFloat(computed.paddingTop) || 0,\n paddingBottom: parseFloat(computed.paddingBottom) || 0\n };\n \n return this._cachedMeasurementStyles;\n }\n \n private _parseLineHeight(lineHeight: string, fontSize: number): number {\n if (lineHeight === 'normal') {\n return fontSize * 1.2;\n }\n if (lineHeight.endsWith('px')) {\n return parseFloat(lineHeight);\n }\n const numericLineHeight = parseFloat(lineHeight);\n if (!isNaN(numericLineHeight)) {\n return fontSize * numericLineHeight;\n }\n return fontSize * 1.2;\n }\n \n // Override the intrinsic size method\n override intrinsicContentSizeWithConstraints(\n constrainingHeight: number = 0,\n constrainingWidth: number = 0\n ): UIRectangle {\n const cacheKey = \"h_\" + constrainingHeight + \"__w_\" + constrainingWidth;\n const cachedResult = this._intrinsicSizesCache[cacheKey];\n if (cachedResult) {\n return cachedResult;\n }\n \n // Determine measurement strategy\n const shouldUseFastPath = this._useFastMeasurement ?? this._shouldUseFastMeasurement();\n \n let result: UIRectangle;\n \n if (shouldUseFastPath) {\n // Fast path: canvas-based measurement with pre-extracted styles\n const styles = this._getMeasurementStyles();\n const size = UITextMeasurement.calculateTextSize(\n this.viewHTMLElement,\n this.text || this.innerHTML,\n constrainingWidth || undefined,\n constrainingHeight || undefined,\n styles // Pass pre-computed styles to avoid getComputedStyle!\n );\n result = new UIRectangle(0, 0, size.height, size.width);\n } else {\n // Fallback: original DOM-based measurement for complex content\n result = super.intrinsicContentSizeWithConstraints(constrainingHeight, constrainingWidth);\n }\n \n this._intrinsicSizesCache[cacheKey] = result.copy();\n return result;\n }\n \n // Helper to determine if we can use fast measurement\n private _shouldUseFastMeasurement(): boolean {\n const content = this.text || this.innerHTML;\n \n // If using dynamic innerHTML with parameters, use DOM measurement\n if (this._innerHTMLKey || this._localizedTextObject) {\n return false;\n }\n \n // Check for notification badges\n if (this.notificationAmount > 0) {\n return false; // Has span with colored text\n }\n \n // Check content complexity\n const hasComplexHTML = /<(?!\\/?(b|i|em|strong|span|br)\\b)[^>]+>/i.test(content);\n \n return !hasComplexHTML;\n }\n \n // Optional: Allow manual override for specific instances\n setUseFastMeasurement(useFast: boolean): void {\n this._useFastMeasurement = useFast;\n this._intrinsicSizesCache = {};\n }\n \n // Optional: Force re-evaluation of measurement strategy\n invalidateMeasurementStrategy(): void {\n this._useFastMeasurement = undefined;\n this._invalidateMeasurementStyles();\n }\n \n // Update fontSize setter to invalidate cached styles\n override set fontSize(fontSize: number) {\n this.style.fontSize = \"\" + fontSize + \"pt\";\n this._intrinsicHeightCache = new UIObject() as any;\n this._intrinsicWidthCache = new UIObject() as any;\n this._invalidateMeasurementStyles(); // Invalidate when font changes\n }\n \n // Update the text setter to invalidate measurement strategy\n override set text(text: string) {\n this._text = text;\n \n var notificationText = \"\";\n \n if (this.notificationAmount) {\n notificationText = \"<span style=\\\"color: \" + UITextView.notificationTextColor.stringValue + \";\\\">\" +\n (\" (\" + this.notificationAmount + \")\").bold() + \"</span>\";\n }\n \n if (this.viewHTMLElement.innerHTML != this.textPrefix + text + this.textSuffix + notificationText) {\n this.viewHTMLElement.innerHTML = this.textPrefix + FIRST(text, \"\") + this.textSuffix + notificationText;\n }\n \n if (this.changesOften) {\n this._intrinsicHeightCache = new UIObject() as any;\n this._intrinsicWidthCache = new UIObject() as any;\n }\n \n // Invalidate measurement strategy when text changes significantly\n this._useFastMeasurement = undefined;\n this._intrinsicSizesCache = {};\n \n this.setNeedsLayout();\n }\n */\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// UITextMeasurement.ts - Efficient text measurement without DOM reflows\n\nexport interface TextMeasurementStyle {\n font: string;\n fontSize: number;\n lineHeight: number;\n whiteSpace: string;\n paddingLeft: number;\n paddingRight: number;\n paddingTop: number;\n paddingBottom: number;\n letterSpacing: number;\n textTransform: string;\n}\n\nexport class UITextMeasurement {\n private static canvas: HTMLCanvasElement | null = null;\n private static context: CanvasRenderingContext2D | null = null;\n \n // Global cache for style objects using semantic cache key\n private static globalStyleCache = new Map<string, TextMeasurementStyle>();\n \n // Per-element cache to map element -> cache key\n private static elementToCacheKey = new WeakMap<HTMLElement, string>();\n \n // Temporary element for complex HTML measurements (reused to avoid allocations)\n private static measurementElement: HTMLDivElement | null = null;\n \n /**\n * Generate a cache key based only on styles that affect text measurement\n * Ignores position, color, transform, etc.\n */\n private static generateStyleCacheKey(computed: CSSStyleDeclaration): string {\n // Only include properties that affect text layout\n const relevantProps = [\n computed.fontFamily,\n computed.fontSize,\n computed.fontWeight,\n computed.fontStyle,\n computed.fontVariant,\n computed.lineHeight,\n computed.letterSpacing,\n computed.wordSpacing,\n computed.textTransform,\n computed.whiteSpace,\n computed.wordBreak,\n computed.wordWrap,\n computed.paddingLeft,\n computed.paddingRight,\n computed.paddingTop,\n computed.paddingBottom,\n computed.borderLeftWidth,\n computed.borderRightWidth,\n computed.borderTopWidth,\n computed.borderBottomWidth,\n computed.boxSizing\n ];\n \n // Create a hash-like key from relevant properties\n return relevantProps.join('|');\n }\n \n /**\n * Extract cache key from element's classList\n * Elements with same classes likely have same text measurement styles\n */\n private static getSemanticCacheKey(element: HTMLElement): string {\n // Check if we already computed a cache key for this element\n const existingKey = this.elementToCacheKey.get(element);\n if (existingKey) {\n return existingKey;\n }\n \n // Try to use class-based caching first (fastest)\n const classList = Array.from(element.classList).sort().join(' ');\n const tagName = element.tagName.toLowerCase();\n \n // Semantic key based on tag + classes\n const semanticKey = `${tagName}::${classList}`;\n \n // Check if we have styles for this semantic key\n if (this.globalStyleCache.has(semanticKey)) {\n this.elementToCacheKey.set(element, semanticKey);\n return semanticKey;\n }\n \n // If not cached, compute and use style-based key\n const computed = window.getComputedStyle(element);\n const styleCacheKey = this.generateStyleCacheKey(computed);\n \n // Use the style-based key\n this.elementToCacheKey.set(element, styleCacheKey);\n \n return styleCacheKey;\n }\n \n /**\n * Get or create the canvas context for text measurement\n */\n private static getContext(): CanvasRenderingContext2D {\n if (!this.context) {\n this.canvas = document.createElement('canvas');\n this.context = this.canvas.getContext('2d')!;\n }\n return this.context;\n }\n \n /**\n * Detect if content is plain text or complex HTML\n */\n private static isPlainText(content: string): boolean {\n // Check for HTML tags (excluding simple formatting like <b>, <i>, <span>)\n const hasComplexHTML = /<(?!\\/?(b|i|em|strong|span|br)\\b)[^>]+>/i.test(content);\n return !hasComplexHTML;\n }\n \n /**\n * Check if content has only simple inline formatting\n */\n private static hasSimpleFormatting(content: string): boolean {\n // Only <b>, <i>, <strong>, <em>, <span> with inline styles\n const simpleTagPattern = /^[^<]*(?:<\\/?(?:b|i|em|strong|span)(?:\\s+style=\"[^\"]*\")?>[^<]*)*$/i;\n return simpleTagPattern.test(content);\n }\n \n /**\n * Get or create measurement element for complex HTML\n */\n private static getMeasurementElement(): HTMLDivElement {\n if (!this.measurementElement) {\n this.measurementElement = document.createElement('div');\n this.measurementElement.style.cssText = `\n position: absolute;\n visibility: hidden;\n pointer-events: none;\n top: -9999px;\n left: -9999px;\n width: auto;\n height: auto;\n `;\n }\n return this.measurementElement;\n }\n \n /**\n * Fast measurement using DOM (but optimized to minimize reflows)\n */\n private static measureWithDOM(\n element: HTMLElement,\n content: string,\n constrainingWidth?: number,\n constrainingHeight?: number,\n providedStyles?: TextMeasurementStyle\n ): { width: number; height: number } {\n const measureEl = this.getMeasurementElement();\n const styles = this.getElementStyles(element, providedStyles);\n \n // Copy relevant styles\n measureEl.style.font = styles.font;\n measureEl.style.lineHeight = styles.lineHeight + 'px';\n measureEl.style.whiteSpace = styles.whiteSpace;\n measureEl.style.padding = `${styles.paddingTop}px ${styles.paddingRight}px ${styles.paddingBottom}px ${styles.paddingLeft}px`;\n measureEl.style.letterSpacing = styles.letterSpacing ? styles.letterSpacing + 'px' : '';\n measureEl.style.textTransform = styles.textTransform || '';\n \n // Set constraints\n if (constrainingWidth) {\n measureEl.style.width = constrainingWidth + 'px';\n measureEl.style.maxWidth = constrainingWidth + 'px';\n } else {\n measureEl.style.width = 'auto';\n measureEl.style.maxWidth = 'none';\n }\n \n if (constrainingHeight) {\n measureEl.style.height = constrainingHeight + 'px';\n measureEl.style.maxHeight = constrainingHeight + 'px';\n } else {\n measureEl.style.height = 'auto';\n measureEl.style.maxHeight = 'none';\n }\n \n // Set content\n measureEl.innerHTML = content;\n \n // Add to DOM only if not already there\n if (!measureEl.parentElement) {\n document.body.appendChild(measureEl);\n }\n \n // Single reflow for both measurements\n const rect = measureEl.getBoundingClientRect();\n const result = {\n width: rect.width || measureEl.scrollWidth,\n height: rect.height || measureEl.scrollHeight\n };\n \n return result;\n }\n \n /**\n * Get or extract styles from element (with smart global caching)\n * Returns cached styles if available, otherwise computes once and caches globally\n */\n private static getElementStyles(element: HTMLElement, providedStyles?: TextMeasurementStyle): TextMeasurementStyle {\n // Use provided styles if available (avoids getComputedStyle entirely)\n if (providedStyles) {\n return providedStyles;\n }\n \n // Get semantic cache key\n const cacheKey = this.getSemanticCacheKey(element);\n \n // Check global cache\n const cached = this.globalStyleCache.get(cacheKey);\n if (cached) {\n return cached;\n }\n \n // Compute once and cache globally (this is the only getComputedStyle call)\n const computed = window.getComputedStyle(element);\n const fontSize = parseFloat(computed.fontSize);\n \n const styles: TextMeasurementStyle = {\n font: [\n computed.fontStyle,\n computed.fontVariant,\n computed.fontWeight,\n computed.fontSize,\n computed.fontFamily\n ].join(' '),\n fontSize: fontSize,\n lineHeight: this.parseLineHeight(computed.lineHeight, fontSize),\n whiteSpace: computed.whiteSpace,\n paddingLeft: parseFloat(computed.paddingLeft) || 0,\n paddingRight: parseFloat(computed.paddingRight) || 0,\n paddingTop: parseFloat(computed.paddingTop) || 0,\n paddingBottom: parseFloat(computed.paddingBottom) || 0,\n letterSpacing: parseFloat(computed.letterSpacing) || 0,\n textTransform: computed.textTransform || 'none'\n };\n \n this.globalStyleCache.set(cacheKey, styles);\n return styles;\n }\n \n /**\n * Parse line height from computed style\n */\n private static applyTextTransform(text: string, transform: string): string {\n switch (transform) {\n case 'uppercase': return text.toUpperCase();\n case 'lowercase': return text.toLowerCase();\n case 'capitalize': return text.replace(/\\b\\w/g, c => c.toUpperCase());\n default: return text;\n }\n }\n \n private static parseLineHeight(lineHeight: string, fontSize: number): number {\n if (lineHeight === 'normal') {\n return fontSize * 1.2;\n }\n \n if (lineHeight.endsWith('px')) {\n return parseFloat(lineHeight);\n }\n \n const numericLineHeight = parseFloat(lineHeight);\n if (!isNaN(numericLineHeight)) {\n return fontSize * numericLineHeight;\n }\n \n return fontSize * 1.2;\n }\n \n /**\n * Measure text width using Canvas API\n */\n // Tracks fonts we have already requested loading for, to avoid redundant calls.\n private static _fontsLoadingSet = new Set<string>()\n \n static measureTextWidth(text: string, font: string, letterSpacing: number = 0): number {\n const ctx = this.getContext();\n ctx.font = font;\n // After assigning ctx.font, the browser normalises it. If the result\n // doesn't contain the requested family the font hasn't been loaded into\n // the canvas font system yet and measureText will use the fallback.\n // Request an explicit load so the documentFontsDidLoad re-layout will\n // have the correct font, and return NaN to signal the caller to fall\n // back to a layout-triggered retry rather than caching a wrong value.\n if (!ctx.font.includes(font.split(\",\")[0].split(\" \").pop()!.trim())) {\n if (!this._fontsLoadingSet.has(font)) {\n this._fontsLoadingSet.add(font)\n document.fonts.load(font).then(() => {\n this._fontsLoadingSet.delete(font)\n })\n }\n return NaN;\n }\n const baseWidth = ctx.measureText(text).width;\n // Canvas measureText does not apply letter-spacing; add it manually.\n return baseWidth + letterSpacing * text.length;\n }\n \n /**\n * Split text into lines based on width constraint\n */\n private static wrapText(\n text: string,\n maxWidth: number,\n font: string,\n whiteSpace: string\n ): string[] | null {\n // No wrapping needed\n if (whiteSpace === 'nowrap' || whiteSpace === 'pre') {\n return [text];\n }\n \n const ctx = this.getContext();\n ctx.font = font;\n // If the font fell back, signal the caller to not cache the result.\n if (!ctx.font.includes(font.split(\",\")[0].split(\" \").pop()!.trim())) {\n return null;\n }\n \n const lines: string[] = [];\n const paragraphs = text.split('\\n');\n \n for (const paragraph of paragraphs) {\n if (whiteSpace === 'pre-wrap') {\n // Preserve whitespace but wrap at maxWidth\n lines.push(...this.wrapPreservingWhitespace(paragraph, maxWidth, ctx));\n } else {\n // Normal wrapping (collapse whitespace)\n lines.push(...this.wrapNormal(paragraph, maxWidth, ctx));\n }\n }\n \n return lines;\n }\n \n private static wrapNormal(\n text: string,\n maxWidth: number,\n ctx: CanvasRenderingContext2D\n ): string[] {\n const words = text.split(/\\s+/).filter(w => w.length > 0);\n const lines: string[] = [];\n let currentLine = '';\n \n for (const word of words) {\n const testLine = currentLine ? `${currentLine} ${word}` : word;\n const metrics = ctx.measureText(testLine);\n \n if (metrics.width > maxWidth && currentLine) {\n lines.push(currentLine);\n currentLine = word;\n } else {\n currentLine = testLine;\n }\n }\n \n if (currentLine) {\n lines.push(currentLine);\n }\n \n return lines.length > 0 ? lines : [''];\n }\n \n private static wrapPreservingWhitespace(\n text: string,\n maxWidth: number,\n ctx: CanvasRenderingContext2D\n ): string[] {\n const lines: string[] = [];\n let currentLine = '';\n \n for (let i = 0; i < text.length; i++) {\n const char = text[i];\n const testLine = currentLine + char;\n const metrics = ctx.measureText(testLine);\n \n if (metrics.width > maxWidth && currentLine) {\n lines.push(currentLine);\n currentLine = char;\n } else {\n currentLine = testLine;\n }\n }\n \n if (currentLine) {\n lines.push(currentLine);\n }\n \n return lines.length > 0 ? lines : [''];\n }\n \n /**\n * Calculate intrinsic content size for text - SMART METHOD\n * Automatically chooses the most efficient measurement technique\n */\n static calculateTextSize(\n element: HTMLElement,\n content: string,\n constrainingWidth?: number,\n constrainingHeight?: number,\n providedStyles?: TextMeasurementStyle\n ): { width: number; height: number } {\n // Empty content\n if (!content || content.length === 0) {\n const styles = this.getElementStyles(element, providedStyles);\n return {\n width: styles.paddingLeft + styles.paddingRight,\n height: styles.paddingTop + styles.paddingBottom\n };\n }\n \n // Check complexity of content\n const isPlain = this.isPlainText(content);\n const hasSimple = this.hasSimpleFormatting(content);\n \n // Strategy 1: Pure canvas for plain text (fastest)\n if (isPlain) {\n return this.calculatePlainTextSize(\n element,\n content,\n constrainingWidth,\n constrainingHeight,\n providedStyles\n );\n }\n \n // Strategy 2: Optimized DOM for simple formatting (fast)\n if (hasSimple) {\n // For simple formatting, we can still use canvas but need to strip tags\n const plainText = content.replace(/<[^>]+>/g, '');\n return this.calculatePlainTextSize(\n element,\n plainText,\n constrainingWidth,\n constrainingHeight,\n providedStyles\n );\n }\n \n // Strategy 3: DOM measurement for complex HTML (slower but accurate)\n return this.measureWithDOM(\n element,\n content,\n constrainingWidth,\n constrainingHeight,\n providedStyles\n );\n }\n \n /**\n * Calculate size for plain text using canvas (no HTML)\n */\n private static calculatePlainTextSize(\n element: HTMLElement,\n text: string,\n constrainingWidth?: number,\n constrainingHeight?: number,\n providedStyles?: TextMeasurementStyle\n ): { width: number; height: number } {\n const styles = this.getElementStyles(element, providedStyles);\n \n // Adjust constraining width for padding\n const availableWidth = constrainingWidth\n ? constrainingWidth - styles.paddingLeft - styles.paddingRight\n : Infinity;\n \n // Calculate dimensions\n let width: number;\n let height: number;\n \n // Apply text-transform before measuring, matching what the browser renders\n const transformedText = this.applyTextTransform(text, styles.textTransform);\n \n if (styles.whiteSpace === 'nowrap' || styles.whiteSpace === 'pre' || !constrainingWidth) {\n // Single line or no width constraint\n width = this.measureTextWidth(transformedText, styles.font, styles.letterSpacing) + styles.paddingLeft + styles.paddingRight;\n height = styles.lineHeight + styles.paddingTop + styles.paddingBottom;\n } else {\n // Multi-line text\n const lines = this.wrapText(transformedText, availableWidth, styles.font, styles.whiteSpace);\n \n // null means the font wasn't loaded into the canvas yet\n if (!lines) {\n return { width: NaN, height: NaN };\n }\n \n // Find the widest line\n width = Math.max(\n ...lines.map(line => this.measureTextWidth(line, styles.font, styles.letterSpacing))\n ) + styles.paddingLeft + styles.paddingRight;\n \n height = (lines.length * styles.lineHeight) + styles.paddingTop + styles.paddingBottom;\n }\n \n // NaN means the canvas font wasn't loaded yet. Propagate NaN upward so\n // the caller knows not to cache this result. The documentFontsDidLoad\n // hook will trigger a re-layout once the font is available.\n return { width, height };\n }\n \n /**\n * Clear all caches (call when fonts change or for cleanup).\n * Also resets the canvas context so the next measureText call forces the\n * browser to re-resolve the font string against the now-loaded FontFace set.\n * Without this, ctx.font may silently fall back to the system font even\n * though getComputedStyle already reports the correct custom font.\n */\n static clearCaches(): void {\n this.globalStyleCache.clear();\n this.elementToCacheKey = new WeakMap();\n this.context = null;\n this.canvas = null;\n }\n \n /**\n * Invalidate cached styles for a specific element\n */\n static invalidateElement(element: HTMLElement): void {\n const cacheKey = this.elementToCacheKey.get(element);\n if (cacheKey) {\n this.globalStyleCache.delete(cacheKey);\n this.elementToCacheKey.delete(element);\n }\n }\n \n /**\n * Invalidate cache for elements with specific class\n * Useful when you change a CSS class definition\n */\n static invalidateClass(className: string): void {\n // Clear all cache keys that contain this class\n for (const [key] of this.globalStyleCache.entries()) {\n if (key.includes(className)) {\n this.globalStyleCache.delete(key);\n }\n }\n }\n \n /**\n * Pre-warm the cache by measuring a representative element\n * Useful at app startup to avoid first-paint delays\n */\n static prewarmCache(elements: HTMLElement[]): void {\n elements.forEach(el => {\n this.getElementStyles(el);\n });\n }\n \n /**\n * Clean up measurement element (call on app cleanup)\n */\n static cleanup(): void {\n if (this.measurementElement && this.measurementElement.parentElement) {\n document.body.removeChild(this.measurementElement);\n }\n this.measurementElement = null;\n this.canvas = null;\n this.context = null;\n this.clearCaches();\n }\n}\n\n// Extension methods to add to UITextView\nexport interface UITextViewMeasurementMethods {\n intrinsicContentSizeEfficient(\n constrainingWidth?: number,\n constrainingHeight?: number\n ): { width: number; height: number };\n}\n\n// ==================== INTEGRATION CODE ====================\n// Add these methods to UITextView class:\n\n/*\n // In UITextView class:\n \n // Add this property to track content complexity and cached styles\n private _useFastMeasurement: boolean | undefined;\n private _cachedMeasurementStyles: TextMeasurementStyle | undefined;\n \n // Call this when styles change (fontSize, padding, etc.)\n private _invalidateMeasurementStyles(): void {\n this._cachedMeasurementStyles = undefined;\n UITextMeasurement.invalidateElement(this.viewHTMLElement);\n this._intrinsicSizesCache = {};\n }\n \n // Extract styles ONCE and cache them (avoids getComputedStyle)\n private _getMeasurementStyles(): TextMeasurementStyle {\n if (this._cachedMeasurementStyles) {\n return this._cachedMeasurementStyles;\n }\n \n // Only call getComputedStyle once and cache the result\n const computed = window.getComputedStyle(this.viewHTMLElement);\n const fontSize = parseFloat(computed.fontSize);\n \n this._cachedMeasurementStyles = {\n font: [\n computed.fontStyle,\n computed.fontVariant,\n computed.fontWeight,\n computed.fontSize,\n computed.fontFamily\n ].join(' '),\n fontSize: fontSize,\n lineHeight: this._parseLineHeight(computed.lineHeight, fontSize),\n whiteSpace: computed.whiteSpace,\n paddingLeft: parseFloat(computed.paddingLeft) || 0,\n paddingRight: parseFloat(computed.paddingRight) || 0,\n paddingTop: parseFloat(computed.paddingTop) || 0,\n paddingBottom: parseFloat(computed.paddingBottom) || 0\n };\n \n return this._cachedMeasurementStyles;\n }\n \n private _parseLineHeight(lineHeight: string, fontSize: number): number {\n if (lineHeight === 'normal') {\n return fontSize * 1.2;\n }\n if (lineHeight.endsWith('px')) {\n return parseFloat(lineHeight);\n }\n const numericLineHeight = parseFloat(lineHeight);\n if (!isNaN(numericLineHeight)) {\n return fontSize * numericLineHeight;\n }\n return fontSize * 1.2;\n }\n \n // Override the intrinsic size method\n override intrinsicContentSizeWithConstraints(\n constrainingHeight: number = 0,\n constrainingWidth: number = 0\n ): UIRectangle {\n const cacheKey = \"h_\" + constrainingHeight + \"__w_\" + constrainingWidth;\n const cachedResult = this._intrinsicSizesCache[cacheKey];\n if (cachedResult) {\n return cachedResult;\n }\n \n // Determine measurement strategy\n const shouldUseFastPath = this._useFastMeasurement ?? this._shouldUseFastMeasurement();\n \n let result: UIRectangle;\n \n if (shouldUseFastPath) {\n // Fast path: canvas-based measurement with pre-extracted styles\n const styles = this._getMeasurementStyles();\n const size = UITextMeasurement.calculateTextSize(\n this.viewHTMLElement,\n this.text || this.innerHTML,\n constrainingWidth || undefined,\n constrainingHeight || undefined,\n styles // Pass pre-computed styles to avoid getComputedStyle!\n );\n result = new UIRectangle(0, 0, size.height, size.width);\n } else {\n // Fallback: original DOM-based measurement for complex content\n result = super.intrinsicContentSizeWithConstraints(constrainingHeight, constrainingWidth);\n }\n \n this._intrinsicSizesCache[cacheKey] = result.copy();\n return result;\n }\n \n // Helper to determine if we can use fast measurement\n private _shouldUseFastMeasurement(): boolean {\n const content = this.text || this.innerHTML;\n \n // If using dynamic innerHTML with parameters, use DOM measurement\n if (this._innerHTMLKey || this._localizedTextObject) {\n return false;\n }\n \n // Check for notification badges\n if (this.notificationAmount > 0) {\n return false; // Has span with colored text\n }\n \n // Check content complexity\n const hasComplexHTML = /<(?!\\/?(b|i|em|strong|span|br)\\b)[^>]+>/i.test(content);\n \n return !hasComplexHTML;\n }\n \n // Optional: Allow manual override for specific instances\n setUseFastMeasurement(useFast: boolean): void {\n this._useFastMeasurement = useFast;\n this._intrinsicSizesCache = {};\n }\n \n // Optional: Force re-evaluation of measurement strategy\n invalidateMeasurementStrategy(): void {\n this._useFastMeasurement = undefined;\n this._invalidateMeasurementStyles();\n }\n \n // Update fontSize setter to invalidate cached styles\n override set fontSize(fontSize: number) {\n this.style.fontSize = \"\" + fontSize + \"pt\";\n this._intrinsicHeightCache = new UIObject() as any;\n this._intrinsicWidthCache = new UIObject() as any;\n this._invalidateMeasurementStyles(); // Invalidate when font changes\n }\n \n // Update the text setter to invalidate measurement strategy\n override set text(text: string) {\n this._text = text;\n \n var notificationText = \"\";\n \n if (this.notificationAmount) {\n notificationText = \"<span style=\\\"color: \" + UITextView.notificationTextColor.stringValue + \";\\\">\" +\n (\" (\" + this.notificationAmount + \")\").bold() + \"</span>\";\n }\n \n if (this.viewHTMLElement.innerHTML != this.textPrefix + text + this.textSuffix + notificationText) {\n this.viewHTMLElement.innerHTML = this.textPrefix + FIRST(text, \"\") + this.textSuffix + notificationText;\n }\n \n if (this.changesOften) {\n this._intrinsicHeightCache = new UIObject() as any;\n this._intrinsicWidthCache = new UIObject() as any;\n }\n \n // Invalidate measurement strategy when text changes significantly\n this._useFastMeasurement = undefined;\n this._intrinsicSizesCache = {};\n \n this.setNeedsLayout();\n }\n */\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uicore-ts",
3
- "version": "1.1.212",
3
+ "version": "1.1.216",
4
4
  "description": "UICore is a library to build native-like user interfaces using pure Typescript. No HTML is needed at all. Components are described as TS classes and all user interactions are handled explicitly. This library is strongly inspired by the UIKit framework that is used in IOS. In addition, UICore has tools to handle URL based routing, array sorting and filtering and adds a number of other utilities for convenience.",
5
5
  "main": "compiledScripts/index.js",
6
6
  "types": "compiledScripts/index.d.ts",
@@ -273,18 +273,32 @@ export class UITextMeasurement {
273
273
  return fontSize * 1.2;
274
274
  }
275
275
 
276
-
277
276
  /**
278
277
  * Measure text width using Canvas API
279
278
  */
279
+ // Tracks fonts we have already requested loading for, to avoid redundant calls.
280
+ private static _fontsLoadingSet = new Set<string>()
281
+
280
282
  static measureTextWidth(text: string, font: string, letterSpacing: number = 0): number {
281
283
  const ctx = this.getContext();
282
284
  ctx.font = font;
285
+ // After assigning ctx.font, the browser normalises it. If the result
286
+ // doesn't contain the requested family the font hasn't been loaded into
287
+ // the canvas font system yet and measureText will use the fallback.
288
+ // Request an explicit load so the documentFontsDidLoad re-layout will
289
+ // have the correct font, and return NaN to signal the caller to fall
290
+ // back to a layout-triggered retry rather than caching a wrong value.
291
+ if (!ctx.font.includes(font.split(",")[0].split(" ").pop()!.trim())) {
292
+ if (!this._fontsLoadingSet.has(font)) {
293
+ this._fontsLoadingSet.add(font)
294
+ document.fonts.load(font).then(() => {
295
+ this._fontsLoadingSet.delete(font)
296
+ })
297
+ }
298
+ return NaN;
299
+ }
283
300
  const baseWidth = ctx.measureText(text).width;
284
301
  // Canvas measureText does not apply letter-spacing; add it manually.
285
- // letter-spacing applies between characters, so multiply by text length
286
- // (not text.length - 1) to match browser rendering which also adds it
287
- // after the last character in most implementations.
288
302
  return baseWidth + letterSpacing * text.length;
289
303
  }
290
304
 
@@ -296,7 +310,7 @@ export class UITextMeasurement {
296
310
  maxWidth: number,
297
311
  font: string,
298
312
  whiteSpace: string
299
- ): string[] {
313
+ ): string[] | null {
300
314
  // No wrapping needed
301
315
  if (whiteSpace === 'nowrap' || whiteSpace === 'pre') {
302
316
  return [text];
@@ -304,6 +318,10 @@ export class UITextMeasurement {
304
318
 
305
319
  const ctx = this.getContext();
306
320
  ctx.font = font;
321
+ // If the font fell back, signal the caller to not cache the result.
322
+ if (!ctx.font.includes(font.split(",")[0].split(" ").pop()!.trim())) {
323
+ return null;
324
+ }
307
325
 
308
326
  const lines: string[] = [];
309
327
  const paragraphs = text.split('\n');
@@ -467,6 +485,11 @@ export class UITextMeasurement {
467
485
  // Multi-line text
468
486
  const lines = this.wrapText(transformedText, availableWidth, styles.font, styles.whiteSpace);
469
487
 
488
+ // null means the font wasn't loaded into the canvas yet
489
+ if (!lines) {
490
+ return { width: NaN, height: NaN };
491
+ }
492
+
470
493
  // Find the widest line
471
494
  width = Math.max(
472
495
  ...lines.map(line => this.measureTextWidth(line, styles.font, styles.letterSpacing))
@@ -475,6 +498,9 @@ export class UITextMeasurement {
475
498
  height = (lines.length * styles.lineHeight) + styles.paddingTop + styles.paddingBottom;
476
499
  }
477
500
 
501
+ // NaN means the canvas font wasn't loaded yet. Propagate NaN upward so
502
+ // the caller knows not to cache this result. The documentFontsDidLoad
503
+ // hook will trigger a re-layout once the font is available.
478
504
  return { width, height };
479
505
  }
480
506