uicore-ts 1.1.57 → 1.1.59
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.
- package/compiledScripts/UITextMeasurement.d.ts +48 -7
- package/compiledScripts/UITextMeasurement.js +121 -60
- package/compiledScripts/UITextMeasurement.js.map +2 -2
- package/compiledScripts/UITextView.d.ts +4 -0
- package/compiledScripts/UITextView.js +59 -7
- package/compiledScripts/UITextView.js.map +2 -2
- package/compiledScripts/UIView.d.ts +1 -0
- package/compiledScripts/UIView.js +4 -0
- package/compiledScripts/UIView.js.map +2 -2
- package/package.json +1 -1
- package/scripts/UITextMeasurement.ts +360 -75
- package/scripts/UITextView.ts +79 -31
- package/scripts/UIView.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uicore-ts",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.59",
|
|
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",
|
|
@@ -1,19 +1,97 @@
|
|
|
1
1
|
// UITextMeasurement.ts - Efficient text measurement without DOM reflows
|
|
2
2
|
|
|
3
|
+
export interface TextMeasurementStyle {
|
|
4
|
+
font: string;
|
|
5
|
+
fontSize: number;
|
|
6
|
+
lineHeight: number;
|
|
7
|
+
whiteSpace: string;
|
|
8
|
+
paddingLeft: number;
|
|
9
|
+
paddingRight: number;
|
|
10
|
+
paddingTop: number;
|
|
11
|
+
paddingBottom: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
export class UITextMeasurement {
|
|
4
|
-
|
|
5
15
|
private static canvas: HTMLCanvasElement | null = null;
|
|
6
16
|
private static context: CanvasRenderingContext2D | null = null;
|
|
7
17
|
|
|
8
|
-
//
|
|
9
|
-
private static
|
|
18
|
+
// Global cache for style objects using semantic cache key
|
|
19
|
+
private static globalStyleCache = new Map<string, TextMeasurementStyle>();
|
|
10
20
|
|
|
11
|
-
//
|
|
12
|
-
private static
|
|
21
|
+
// Per-element cache to map element -> cache key
|
|
22
|
+
private static elementToCacheKey = new WeakMap<HTMLElement, string>();
|
|
13
23
|
|
|
14
24
|
// Temporary element for complex HTML measurements (reused to avoid allocations)
|
|
15
25
|
private static measurementElement: HTMLDivElement | null = null;
|
|
16
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Generate a cache key based only on styles that affect text measurement
|
|
29
|
+
* Ignores position, color, transform, etc.
|
|
30
|
+
*/
|
|
31
|
+
private static generateStyleCacheKey(computed: CSSStyleDeclaration): string {
|
|
32
|
+
// Only include properties that affect text layout
|
|
33
|
+
const relevantProps = [
|
|
34
|
+
computed.fontFamily,
|
|
35
|
+
computed.fontSize,
|
|
36
|
+
computed.fontWeight,
|
|
37
|
+
computed.fontStyle,
|
|
38
|
+
computed.fontVariant,
|
|
39
|
+
computed.lineHeight,
|
|
40
|
+
computed.letterSpacing,
|
|
41
|
+
computed.wordSpacing,
|
|
42
|
+
computed.textTransform,
|
|
43
|
+
computed.whiteSpace,
|
|
44
|
+
computed.wordBreak,
|
|
45
|
+
computed.wordWrap,
|
|
46
|
+
computed.paddingLeft,
|
|
47
|
+
computed.paddingRight,
|
|
48
|
+
computed.paddingTop,
|
|
49
|
+
computed.paddingBottom,
|
|
50
|
+
computed.borderLeftWidth,
|
|
51
|
+
computed.borderRightWidth,
|
|
52
|
+
computed.borderTopWidth,
|
|
53
|
+
computed.borderBottomWidth,
|
|
54
|
+
computed.boxSizing
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// Create a hash-like key from relevant properties
|
|
58
|
+
return relevantProps.join('|');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Extract cache key from element's classList
|
|
63
|
+
* Elements with same classes likely have same text measurement styles
|
|
64
|
+
*/
|
|
65
|
+
private static getSemanticCacheKey(element: HTMLElement): string {
|
|
66
|
+
// Check if we already computed a cache key for this element
|
|
67
|
+
const existingKey = this.elementToCacheKey.get(element);
|
|
68
|
+
if (existingKey) {
|
|
69
|
+
return existingKey;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Try to use class-based caching first (fastest)
|
|
73
|
+
const classList = Array.from(element.classList).sort().join(' ');
|
|
74
|
+
const tagName = element.tagName.toLowerCase();
|
|
75
|
+
|
|
76
|
+
// Semantic key based on tag + classes
|
|
77
|
+
const semanticKey = `${tagName}::${classList}`;
|
|
78
|
+
|
|
79
|
+
// Check if we have styles for this semantic key
|
|
80
|
+
if (this.globalStyleCache.has(semanticKey)) {
|
|
81
|
+
this.elementToCacheKey.set(element, semanticKey);
|
|
82
|
+
return semanticKey;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// If not cached, compute and use style-based key
|
|
86
|
+
const computed = window.getComputedStyle(element);
|
|
87
|
+
const styleCacheKey = this.generateStyleCacheKey(computed);
|
|
88
|
+
|
|
89
|
+
// Use the style-based key
|
|
90
|
+
this.elementToCacheKey.set(element, styleCacheKey);
|
|
91
|
+
|
|
92
|
+
return styleCacheKey;
|
|
93
|
+
}
|
|
94
|
+
|
|
17
95
|
/**
|
|
18
96
|
* Get or create the canvas context for text measurement
|
|
19
97
|
*/
|
|
@@ -69,20 +147,17 @@ export class UITextMeasurement {
|
|
|
69
147
|
element: HTMLElement,
|
|
70
148
|
content: string,
|
|
71
149
|
constrainingWidth?: number,
|
|
72
|
-
constrainingHeight?: number
|
|
150
|
+
constrainingHeight?: number,
|
|
151
|
+
providedStyles?: TextMeasurementStyle
|
|
73
152
|
): { width: number; height: number } {
|
|
74
153
|
const measureEl = this.getMeasurementElement();
|
|
75
|
-
const
|
|
154
|
+
const styles = this.getElementStyles(element, providedStyles);
|
|
76
155
|
|
|
77
156
|
// Copy relevant styles
|
|
78
|
-
measureEl.style.font =
|
|
79
|
-
measureEl.style.lineHeight =
|
|
80
|
-
measureEl.style.whiteSpace =
|
|
81
|
-
measureEl.style.
|
|
82
|
-
measureEl.style.wordWrap = style.wordWrap;
|
|
83
|
-
measureEl.style.padding = style.padding;
|
|
84
|
-
measureEl.style.border = style.border;
|
|
85
|
-
measureEl.style.boxSizing = style.boxSizing;
|
|
157
|
+
measureEl.style.font = styles.font;
|
|
158
|
+
measureEl.style.lineHeight = styles.lineHeight + 'px';
|
|
159
|
+
measureEl.style.whiteSpace = styles.whiteSpace;
|
|
160
|
+
measureEl.style.padding = `${styles.paddingTop}px ${styles.paddingRight}px ${styles.paddingBottom}px ${styles.paddingLeft}px`;
|
|
86
161
|
|
|
87
162
|
// Set constraints
|
|
88
163
|
if (constrainingWidth) {
|
|
@@ -120,37 +195,54 @@ export class UITextMeasurement {
|
|
|
120
195
|
}
|
|
121
196
|
|
|
122
197
|
/**
|
|
123
|
-
*
|
|
198
|
+
* Get or extract styles from element (with smart global caching)
|
|
199
|
+
* Returns cached styles if available, otherwise computes once and caches globally
|
|
124
200
|
*/
|
|
125
|
-
private static
|
|
126
|
-
|
|
201
|
+
private static getElementStyles(element: HTMLElement, providedStyles?: TextMeasurementStyle): TextMeasurementStyle {
|
|
202
|
+
// Use provided styles if available (avoids getComputedStyle entirely)
|
|
203
|
+
if (providedStyles) {
|
|
204
|
+
return providedStyles;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Get semantic cache key
|
|
208
|
+
const cacheKey = this.getSemanticCacheKey(element);
|
|
127
209
|
|
|
128
|
-
|
|
129
|
-
|
|
210
|
+
// Check global cache
|
|
211
|
+
const cached = this.globalStyleCache.get(cacheKey);
|
|
212
|
+
if (cached) {
|
|
213
|
+
return cached;
|
|
130
214
|
}
|
|
131
215
|
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
216
|
+
// Compute once and cache globally (this is the only getComputedStyle call)
|
|
217
|
+
const computed = window.getComputedStyle(element);
|
|
218
|
+
const fontSize = parseFloat(computed.fontSize);
|
|
219
|
+
|
|
220
|
+
const styles: TextMeasurementStyle = {
|
|
221
|
+
font: [
|
|
222
|
+
computed.fontStyle,
|
|
223
|
+
computed.fontVariant,
|
|
224
|
+
computed.fontWeight,
|
|
225
|
+
computed.fontSize,
|
|
226
|
+
computed.fontFamily
|
|
227
|
+
].join(' '),
|
|
228
|
+
fontSize: fontSize,
|
|
229
|
+
lineHeight: this.parseLineHeight(computed.lineHeight, fontSize),
|
|
230
|
+
whiteSpace: computed.whiteSpace,
|
|
231
|
+
paddingLeft: parseFloat(computed.paddingLeft) || 0,
|
|
232
|
+
paddingRight: parseFloat(computed.paddingRight) || 0,
|
|
233
|
+
paddingTop: parseFloat(computed.paddingTop) || 0,
|
|
234
|
+
paddingBottom: parseFloat(computed.paddingBottom) || 0
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
this.globalStyleCache.set(cacheKey, styles);
|
|
238
|
+
return styles;
|
|
143
239
|
}
|
|
144
240
|
|
|
145
241
|
/**
|
|
146
|
-
*
|
|
242
|
+
* Parse line height from computed style
|
|
147
243
|
*/
|
|
148
|
-
private static
|
|
149
|
-
const style = window.getComputedStyle(element);
|
|
150
|
-
const lineHeight = style.lineHeight;
|
|
151
|
-
|
|
244
|
+
private static parseLineHeight(lineHeight: string, fontSize: number): number {
|
|
152
245
|
if (lineHeight === 'normal') {
|
|
153
|
-
// Browsers typically use 1.2 as default line height
|
|
154
246
|
return fontSize * 1.2;
|
|
155
247
|
}
|
|
156
248
|
|
|
@@ -158,13 +250,12 @@ export class UITextMeasurement {
|
|
|
158
250
|
return parseFloat(lineHeight);
|
|
159
251
|
}
|
|
160
252
|
|
|
161
|
-
// If it's a unitless number, multiply by font size
|
|
162
253
|
const numericLineHeight = parseFloat(lineHeight);
|
|
163
254
|
if (!isNaN(numericLineHeight)) {
|
|
164
255
|
return fontSize * numericLineHeight;
|
|
165
256
|
}
|
|
166
257
|
|
|
167
|
-
return fontSize * 1.2;
|
|
258
|
+
return fontSize * 1.2;
|
|
168
259
|
}
|
|
169
260
|
|
|
170
261
|
/**
|
|
@@ -273,19 +364,15 @@ export class UITextMeasurement {
|
|
|
273
364
|
element: HTMLElement,
|
|
274
365
|
content: string,
|
|
275
366
|
constrainingWidth?: number,
|
|
276
|
-
constrainingHeight?: number
|
|
367
|
+
constrainingHeight?: number,
|
|
368
|
+
providedStyles?: TextMeasurementStyle
|
|
277
369
|
): { width: number; height: number } {
|
|
278
370
|
// Empty content
|
|
279
371
|
if (!content || content.length === 0) {
|
|
280
|
-
const
|
|
281
|
-
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
282
|
-
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
|
283
|
-
const paddingLeft = parseFloat(style.paddingLeft) || 0;
|
|
284
|
-
const paddingRight = parseFloat(style.paddingRight) || 0;
|
|
285
|
-
|
|
372
|
+
const styles = this.getElementStyles(element, providedStyles);
|
|
286
373
|
return {
|
|
287
|
-
width: paddingLeft + paddingRight,
|
|
288
|
-
height: paddingTop + paddingBottom
|
|
374
|
+
width: styles.paddingLeft + styles.paddingRight,
|
|
375
|
+
height: styles.paddingTop + styles.paddingBottom
|
|
289
376
|
};
|
|
290
377
|
}
|
|
291
378
|
|
|
@@ -299,7 +386,8 @@ export class UITextMeasurement {
|
|
|
299
386
|
element,
|
|
300
387
|
content,
|
|
301
388
|
constrainingWidth,
|
|
302
|
-
constrainingHeight
|
|
389
|
+
constrainingHeight,
|
|
390
|
+
providedStyles
|
|
303
391
|
);
|
|
304
392
|
}
|
|
305
393
|
|
|
@@ -311,7 +399,8 @@ export class UITextMeasurement {
|
|
|
311
399
|
element,
|
|
312
400
|
plainText,
|
|
313
401
|
constrainingWidth,
|
|
314
|
-
constrainingHeight
|
|
402
|
+
constrainingHeight,
|
|
403
|
+
providedStyles
|
|
315
404
|
);
|
|
316
405
|
}
|
|
317
406
|
|
|
@@ -320,7 +409,8 @@ export class UITextMeasurement {
|
|
|
320
409
|
element,
|
|
321
410
|
content,
|
|
322
411
|
constrainingWidth,
|
|
323
|
-
constrainingHeight
|
|
412
|
+
constrainingHeight,
|
|
413
|
+
providedStyles
|
|
324
414
|
);
|
|
325
415
|
}
|
|
326
416
|
|
|
@@ -331,45 +421,34 @@ export class UITextMeasurement {
|
|
|
331
421
|
element: HTMLElement,
|
|
332
422
|
text: string,
|
|
333
423
|
constrainingWidth?: number,
|
|
334
|
-
constrainingHeight?: number
|
|
424
|
+
constrainingHeight?: number,
|
|
425
|
+
providedStyles?: TextMeasurementStyle
|
|
335
426
|
): { width: number; height: number } {
|
|
336
|
-
const
|
|
337
|
-
const style = window.getComputedStyle(element);
|
|
338
|
-
const whiteSpace = style.whiteSpace;
|
|
339
|
-
|
|
340
|
-
// Get font size in pixels
|
|
341
|
-
const fontSize = parseFloat(style.fontSize);
|
|
342
|
-
const lineHeight = this.getLineHeight(element, fontSize);
|
|
343
|
-
|
|
344
|
-
// Get padding
|
|
345
|
-
const paddingLeft = parseFloat(style.paddingLeft) || 0;
|
|
346
|
-
const paddingRight = parseFloat(style.paddingRight) || 0;
|
|
347
|
-
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
348
|
-
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
|
427
|
+
const styles = this.getElementStyles(element, providedStyles);
|
|
349
428
|
|
|
350
429
|
// Adjust constraining width for padding
|
|
351
430
|
const availableWidth = constrainingWidth
|
|
352
|
-
? constrainingWidth - paddingLeft - paddingRight
|
|
431
|
+
? constrainingWidth - styles.paddingLeft - styles.paddingRight
|
|
353
432
|
: Infinity;
|
|
354
433
|
|
|
355
434
|
// Calculate dimensions
|
|
356
435
|
let width: number;
|
|
357
436
|
let height: number;
|
|
358
437
|
|
|
359
|
-
if (whiteSpace === 'nowrap' || whiteSpace === 'pre' || !constrainingWidth) {
|
|
438
|
+
if (styles.whiteSpace === 'nowrap' || styles.whiteSpace === 'pre' || !constrainingWidth) {
|
|
360
439
|
// Single line or no width constraint
|
|
361
|
-
width = this.measureTextWidth(text, font) + paddingLeft + paddingRight;
|
|
362
|
-
height = lineHeight + paddingTop + paddingBottom;
|
|
440
|
+
width = this.measureTextWidth(text, styles.font) + styles.paddingLeft + styles.paddingRight;
|
|
441
|
+
height = styles.lineHeight + styles.paddingTop + styles.paddingBottom;
|
|
363
442
|
} else {
|
|
364
443
|
// Multi-line text
|
|
365
|
-
const lines = this.wrapText(text, availableWidth, font, whiteSpace);
|
|
444
|
+
const lines = this.wrapText(text, availableWidth, styles.font, styles.whiteSpace);
|
|
366
445
|
|
|
367
446
|
// Find the widest line
|
|
368
447
|
width = Math.max(
|
|
369
|
-
...lines.map(line => this.measureTextWidth(line, font))
|
|
370
|
-
) + paddingLeft + paddingRight;
|
|
448
|
+
...lines.map(line => this.measureTextWidth(line, styles.font))
|
|
449
|
+
) + styles.paddingLeft + styles.paddingRight;
|
|
371
450
|
|
|
372
|
-
height = (lines.length * lineHeight) + paddingTop + paddingBottom;
|
|
451
|
+
height = (lines.length * styles.lineHeight) + styles.paddingTop + styles.paddingBottom;
|
|
373
452
|
}
|
|
374
453
|
|
|
375
454
|
return { width, height };
|
|
@@ -379,8 +458,42 @@ export class UITextMeasurement {
|
|
|
379
458
|
* Clear all caches (call when fonts change or for cleanup)
|
|
380
459
|
*/
|
|
381
460
|
static clearCaches(): void {
|
|
382
|
-
this.
|
|
383
|
-
this.
|
|
461
|
+
this.globalStyleCache.clear();
|
|
462
|
+
this.elementToCacheKey = new WeakMap();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Invalidate cached styles for a specific element
|
|
467
|
+
*/
|
|
468
|
+
static invalidateElement(element: HTMLElement): void {
|
|
469
|
+
const cacheKey = this.elementToCacheKey.get(element);
|
|
470
|
+
if (cacheKey) {
|
|
471
|
+
this.globalStyleCache.delete(cacheKey);
|
|
472
|
+
this.elementToCacheKey.delete(element);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Invalidate cache for elements with specific class
|
|
478
|
+
* Useful when you change a CSS class definition
|
|
479
|
+
*/
|
|
480
|
+
static invalidateClass(className: string): void {
|
|
481
|
+
// Clear all cache keys that contain this class
|
|
482
|
+
for (const [key] of this.globalStyleCache.entries()) {
|
|
483
|
+
if (key.includes(className)) {
|
|
484
|
+
this.globalStyleCache.delete(key);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Pre-warm the cache by measuring a representative element
|
|
491
|
+
* Useful at app startup to avoid first-paint delays
|
|
492
|
+
*/
|
|
493
|
+
static prewarmCache(elements: HTMLElement[]): void {
|
|
494
|
+
elements.forEach(el => {
|
|
495
|
+
this.getElementStyles(el);
|
|
496
|
+
});
|
|
384
497
|
}
|
|
385
498
|
|
|
386
499
|
/**
|
|
@@ -397,3 +510,175 @@ export class UITextMeasurement {
|
|
|
397
510
|
}
|
|
398
511
|
}
|
|
399
512
|
|
|
513
|
+
// Extension methods to add to UITextView
|
|
514
|
+
export interface UITextViewMeasurementMethods {
|
|
515
|
+
intrinsicContentSizeEfficient(
|
|
516
|
+
constrainingWidth?: number,
|
|
517
|
+
constrainingHeight?: number
|
|
518
|
+
): { width: number; height: number };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ==================== INTEGRATION CODE ====================
|
|
522
|
+
// Add these methods to UITextView class:
|
|
523
|
+
|
|
524
|
+
/*
|
|
525
|
+
// In UITextView class:
|
|
526
|
+
|
|
527
|
+
// Add this property to track content complexity and cached styles
|
|
528
|
+
private _useFastMeasurement: boolean | undefined;
|
|
529
|
+
private _cachedMeasurementStyles: TextMeasurementStyle | undefined;
|
|
530
|
+
|
|
531
|
+
// Call this when styles change (fontSize, padding, etc.)
|
|
532
|
+
private _invalidateMeasurementStyles(): void {
|
|
533
|
+
this._cachedMeasurementStyles = undefined;
|
|
534
|
+
UITextMeasurement.invalidateElement(this.viewHTMLElement);
|
|
535
|
+
this._intrinsicSizesCache = {};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Extract styles ONCE and cache them (avoids getComputedStyle)
|
|
539
|
+
private _getMeasurementStyles(): TextMeasurementStyle {
|
|
540
|
+
if (this._cachedMeasurementStyles) {
|
|
541
|
+
return this._cachedMeasurementStyles;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Only call getComputedStyle once and cache the result
|
|
545
|
+
const computed = window.getComputedStyle(this.viewHTMLElement);
|
|
546
|
+
const fontSize = parseFloat(computed.fontSize);
|
|
547
|
+
|
|
548
|
+
this._cachedMeasurementStyles = {
|
|
549
|
+
font: [
|
|
550
|
+
computed.fontStyle,
|
|
551
|
+
computed.fontVariant,
|
|
552
|
+
computed.fontWeight,
|
|
553
|
+
computed.fontSize,
|
|
554
|
+
computed.fontFamily
|
|
555
|
+
].join(' '),
|
|
556
|
+
fontSize: fontSize,
|
|
557
|
+
lineHeight: this._parseLineHeight(computed.lineHeight, fontSize),
|
|
558
|
+
whiteSpace: computed.whiteSpace,
|
|
559
|
+
paddingLeft: parseFloat(computed.paddingLeft) || 0,
|
|
560
|
+
paddingRight: parseFloat(computed.paddingRight) || 0,
|
|
561
|
+
paddingTop: parseFloat(computed.paddingTop) || 0,
|
|
562
|
+
paddingBottom: parseFloat(computed.paddingBottom) || 0
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
return this._cachedMeasurementStyles;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private _parseLineHeight(lineHeight: string, fontSize: number): number {
|
|
569
|
+
if (lineHeight === 'normal') {
|
|
570
|
+
return fontSize * 1.2;
|
|
571
|
+
}
|
|
572
|
+
if (lineHeight.endsWith('px')) {
|
|
573
|
+
return parseFloat(lineHeight);
|
|
574
|
+
}
|
|
575
|
+
const numericLineHeight = parseFloat(lineHeight);
|
|
576
|
+
if (!isNaN(numericLineHeight)) {
|
|
577
|
+
return fontSize * numericLineHeight;
|
|
578
|
+
}
|
|
579
|
+
return fontSize * 1.2;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Override the intrinsic size method
|
|
583
|
+
override intrinsicContentSizeWithConstraints(
|
|
584
|
+
constrainingHeight: number = 0,
|
|
585
|
+
constrainingWidth: number = 0
|
|
586
|
+
): UIRectangle {
|
|
587
|
+
const cacheKey = "h_" + constrainingHeight + "__w_" + constrainingWidth;
|
|
588
|
+
const cachedResult = this._intrinsicSizesCache[cacheKey];
|
|
589
|
+
if (cachedResult) {
|
|
590
|
+
return cachedResult;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Determine measurement strategy
|
|
594
|
+
const shouldUseFastPath = this._useFastMeasurement ?? this._shouldUseFastMeasurement();
|
|
595
|
+
|
|
596
|
+
let result: UIRectangle;
|
|
597
|
+
|
|
598
|
+
if (shouldUseFastPath) {
|
|
599
|
+
// Fast path: canvas-based measurement with pre-extracted styles
|
|
600
|
+
const styles = this._getMeasurementStyles();
|
|
601
|
+
const size = UITextMeasurement.calculateTextSize(
|
|
602
|
+
this.viewHTMLElement,
|
|
603
|
+
this.text || this.innerHTML,
|
|
604
|
+
constrainingWidth || undefined,
|
|
605
|
+
constrainingHeight || undefined,
|
|
606
|
+
styles // Pass pre-computed styles to avoid getComputedStyle!
|
|
607
|
+
);
|
|
608
|
+
result = new UIRectangle(0, 0, size.height, size.width);
|
|
609
|
+
} else {
|
|
610
|
+
// Fallback: original DOM-based measurement for complex content
|
|
611
|
+
result = super.intrinsicContentSizeWithConstraints(constrainingHeight, constrainingWidth);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
this._intrinsicSizesCache[cacheKey] = result.copy();
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Helper to determine if we can use fast measurement
|
|
619
|
+
private _shouldUseFastMeasurement(): boolean {
|
|
620
|
+
const content = this.text || this.innerHTML;
|
|
621
|
+
|
|
622
|
+
// If using dynamic innerHTML with parameters, use DOM measurement
|
|
623
|
+
if (this._innerHTMLKey || this._localizedTextObject) {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Check for notification badges
|
|
628
|
+
if (this.notificationAmount > 0) {
|
|
629
|
+
return false; // Has span with colored text
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Check content complexity
|
|
633
|
+
const hasComplexHTML = /<(?!\/?(b|i|em|strong|span|br)\b)[^>]+>/i.test(content);
|
|
634
|
+
|
|
635
|
+
return !hasComplexHTML;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Optional: Allow manual override for specific instances
|
|
639
|
+
setUseFastMeasurement(useFast: boolean): void {
|
|
640
|
+
this._useFastMeasurement = useFast;
|
|
641
|
+
this._intrinsicSizesCache = {};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Optional: Force re-evaluation of measurement strategy
|
|
645
|
+
invalidateMeasurementStrategy(): void {
|
|
646
|
+
this._useFastMeasurement = undefined;
|
|
647
|
+
this._invalidateMeasurementStyles();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Update fontSize setter to invalidate cached styles
|
|
651
|
+
override set fontSize(fontSize: number) {
|
|
652
|
+
this.style.fontSize = "" + fontSize + "pt";
|
|
653
|
+
this._intrinsicHeightCache = new UIObject() as any;
|
|
654
|
+
this._intrinsicWidthCache = new UIObject() as any;
|
|
655
|
+
this._invalidateMeasurementStyles(); // Invalidate when font changes
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Update the text setter to invalidate measurement strategy
|
|
659
|
+
override set text(text: string) {
|
|
660
|
+
this._text = text;
|
|
661
|
+
|
|
662
|
+
var notificationText = "";
|
|
663
|
+
|
|
664
|
+
if (this.notificationAmount) {
|
|
665
|
+
notificationText = "<span style=\"color: " + UITextView.notificationTextColor.stringValue + ";\">" +
|
|
666
|
+
(" (" + this.notificationAmount + ")").bold() + "</span>";
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (this.viewHTMLElement.innerHTML != this.textPrefix + text + this.textSuffix + notificationText) {
|
|
670
|
+
this.viewHTMLElement.innerHTML = this.textPrefix + FIRST(text, "") + this.textSuffix + notificationText;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (this.changesOften) {
|
|
674
|
+
this._intrinsicHeightCache = new UIObject() as any;
|
|
675
|
+
this._intrinsicWidthCache = new UIObject() as any;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Invalidate measurement strategy when text changes significantly
|
|
679
|
+
this._useFastMeasurement = undefined;
|
|
680
|
+
this._intrinsicSizesCache = {};
|
|
681
|
+
|
|
682
|
+
this.setNeedsLayout();
|
|
683
|
+
}
|
|
684
|
+
*/
|