uicore-ts 1.1.55 → 1.1.57
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 +65 -0
- package/compiledScripts/UITextMeasurement.js +268 -0
- package/compiledScripts/UITextMeasurement.js.map +7 -0
- package/compiledScripts/UITextView.d.ts +5 -0
- package/compiledScripts/UITextView.js +45 -0
- package/compiledScripts/UITextView.js.map +2 -2
- package/compiledScripts/UIView.d.ts +1 -1
- package/compiledScripts/UIView.js.map +2 -2
- package/package.json +1 -1
- package/scripts/UITextMeasurement.ts +399 -0
- package/scripts/UITextView.ts +74 -10
- package/scripts/UIView.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uicore-ts",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.57",
|
|
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",
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// UITextMeasurement.ts - Efficient text measurement without DOM reflows
|
|
2
|
+
|
|
3
|
+
export class UITextMeasurement {
|
|
4
|
+
|
|
5
|
+
private static canvas: HTMLCanvasElement | null = null;
|
|
6
|
+
private static context: CanvasRenderingContext2D | null = null;
|
|
7
|
+
|
|
8
|
+
// Cache for computed font strings to avoid repeated style parsing
|
|
9
|
+
private static fontCache = new Map<string, string>();
|
|
10
|
+
|
|
11
|
+
// Cache for line height calculations
|
|
12
|
+
private static lineHeightCache = new Map<string, number>();
|
|
13
|
+
|
|
14
|
+
// Temporary element for complex HTML measurements (reused to avoid allocations)
|
|
15
|
+
private static measurementElement: HTMLDivElement | null = null;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get or create the canvas context for text measurement
|
|
19
|
+
*/
|
|
20
|
+
private static getContext(): CanvasRenderingContext2D {
|
|
21
|
+
if (!this.context) {
|
|
22
|
+
this.canvas = document.createElement('canvas');
|
|
23
|
+
this.context = this.canvas.getContext('2d')!;
|
|
24
|
+
}
|
|
25
|
+
return this.context;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Detect if content is plain text or complex HTML
|
|
30
|
+
*/
|
|
31
|
+
private static isPlainText(content: string): boolean {
|
|
32
|
+
// Check for HTML tags (excluding simple formatting like <b>, <i>, <span>)
|
|
33
|
+
const hasComplexHTML = /<(?!\/?(b|i|em|strong|span|br)\b)[^>]+>/i.test(content);
|
|
34
|
+
return !hasComplexHTML;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if content has only simple inline formatting
|
|
39
|
+
*/
|
|
40
|
+
private static hasSimpleFormatting(content: string): boolean {
|
|
41
|
+
// Only <b>, <i>, <strong>, <em>, <span> with inline styles
|
|
42
|
+
const simpleTagPattern = /^[^<]*(?:<\/?(?:b|i|em|strong|span)(?:\s+style="[^"]*")?>[^<]*)*$/i;
|
|
43
|
+
return simpleTagPattern.test(content);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get or create measurement element for complex HTML
|
|
48
|
+
*/
|
|
49
|
+
private static getMeasurementElement(): HTMLDivElement {
|
|
50
|
+
if (!this.measurementElement) {
|
|
51
|
+
this.measurementElement = document.createElement('div');
|
|
52
|
+
this.measurementElement.style.cssText = `
|
|
53
|
+
position: absolute;
|
|
54
|
+
visibility: hidden;
|
|
55
|
+
pointer-events: none;
|
|
56
|
+
top: -9999px;
|
|
57
|
+
left: -9999px;
|
|
58
|
+
width: auto;
|
|
59
|
+
height: auto;
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
return this.measurementElement;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fast measurement using DOM (but optimized to minimize reflows)
|
|
67
|
+
*/
|
|
68
|
+
private static measureWithDOM(
|
|
69
|
+
element: HTMLElement,
|
|
70
|
+
content: string,
|
|
71
|
+
constrainingWidth?: number,
|
|
72
|
+
constrainingHeight?: number
|
|
73
|
+
): { width: number; height: number } {
|
|
74
|
+
const measureEl = this.getMeasurementElement();
|
|
75
|
+
const style = window.getComputedStyle(element);
|
|
76
|
+
|
|
77
|
+
// Copy relevant styles
|
|
78
|
+
measureEl.style.font = this.getFontString(element);
|
|
79
|
+
measureEl.style.lineHeight = style.lineHeight;
|
|
80
|
+
measureEl.style.whiteSpace = style.whiteSpace;
|
|
81
|
+
measureEl.style.wordBreak = style.wordBreak;
|
|
82
|
+
measureEl.style.wordWrap = style.wordWrap;
|
|
83
|
+
measureEl.style.padding = style.padding;
|
|
84
|
+
measureEl.style.border = style.border;
|
|
85
|
+
measureEl.style.boxSizing = style.boxSizing;
|
|
86
|
+
|
|
87
|
+
// Set constraints
|
|
88
|
+
if (constrainingWidth) {
|
|
89
|
+
measureEl.style.width = constrainingWidth + 'px';
|
|
90
|
+
measureEl.style.maxWidth = constrainingWidth + 'px';
|
|
91
|
+
} else {
|
|
92
|
+
measureEl.style.width = 'auto';
|
|
93
|
+
measureEl.style.maxWidth = 'none';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (constrainingHeight) {
|
|
97
|
+
measureEl.style.height = constrainingHeight + 'px';
|
|
98
|
+
measureEl.style.maxHeight = constrainingHeight + 'px';
|
|
99
|
+
} else {
|
|
100
|
+
measureEl.style.height = 'auto';
|
|
101
|
+
measureEl.style.maxHeight = 'none';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Set content
|
|
105
|
+
measureEl.innerHTML = content;
|
|
106
|
+
|
|
107
|
+
// Add to DOM only if not already there
|
|
108
|
+
if (!measureEl.parentElement) {
|
|
109
|
+
document.body.appendChild(measureEl);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Single reflow for both measurements
|
|
113
|
+
const rect = measureEl.getBoundingClientRect();
|
|
114
|
+
const result = {
|
|
115
|
+
width: rect.width || measureEl.scrollWidth,
|
|
116
|
+
height: rect.height || measureEl.scrollHeight
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Extract font properties from computed style and create font string
|
|
124
|
+
*/
|
|
125
|
+
private static getFontString(element: HTMLElement): string {
|
|
126
|
+
const cacheKey = element.className + element.id;
|
|
127
|
+
|
|
128
|
+
if (this.fontCache.has(cacheKey)) {
|
|
129
|
+
return this.fontCache.get(cacheKey)!;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const style = window.getComputedStyle(element);
|
|
133
|
+
const font = [
|
|
134
|
+
style.fontStyle,
|
|
135
|
+
style.fontVariant,
|
|
136
|
+
style.fontWeight,
|
|
137
|
+
style.fontSize,
|
|
138
|
+
style.fontFamily
|
|
139
|
+
].join(' ');
|
|
140
|
+
|
|
141
|
+
this.fontCache.set(cacheKey, font);
|
|
142
|
+
return font;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get line height in pixels
|
|
147
|
+
*/
|
|
148
|
+
private static getLineHeight(element: HTMLElement, fontSize: number): number {
|
|
149
|
+
const style = window.getComputedStyle(element);
|
|
150
|
+
const lineHeight = style.lineHeight;
|
|
151
|
+
|
|
152
|
+
if (lineHeight === 'normal') {
|
|
153
|
+
// Browsers typically use 1.2 as default line height
|
|
154
|
+
return fontSize * 1.2;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (lineHeight.endsWith('px')) {
|
|
158
|
+
return parseFloat(lineHeight);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// If it's a unitless number, multiply by font size
|
|
162
|
+
const numericLineHeight = parseFloat(lineHeight);
|
|
163
|
+
if (!isNaN(numericLineHeight)) {
|
|
164
|
+
return fontSize * numericLineHeight;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return fontSize * 1.2; // fallback
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Measure text width using Canvas API
|
|
172
|
+
*/
|
|
173
|
+
static measureTextWidth(text: string, font: string): number {
|
|
174
|
+
const ctx = this.getContext();
|
|
175
|
+
ctx.font = font;
|
|
176
|
+
return ctx.measureText(text).width;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Split text into lines based on width constraint
|
|
181
|
+
*/
|
|
182
|
+
private static wrapText(
|
|
183
|
+
text: string,
|
|
184
|
+
maxWidth: number,
|
|
185
|
+
font: string,
|
|
186
|
+
whiteSpace: string
|
|
187
|
+
): string[] {
|
|
188
|
+
// No wrapping needed
|
|
189
|
+
if (whiteSpace === 'nowrap' || whiteSpace === 'pre') {
|
|
190
|
+
return [text];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const ctx = this.getContext();
|
|
194
|
+
ctx.font = font;
|
|
195
|
+
|
|
196
|
+
const lines: string[] = [];
|
|
197
|
+
const paragraphs = text.split('\n');
|
|
198
|
+
|
|
199
|
+
for (const paragraph of paragraphs) {
|
|
200
|
+
if (whiteSpace === 'pre-wrap') {
|
|
201
|
+
// Preserve whitespace but wrap at maxWidth
|
|
202
|
+
lines.push(...this.wrapPreservingWhitespace(paragraph, maxWidth, ctx));
|
|
203
|
+
} else {
|
|
204
|
+
// Normal wrapping (collapse whitespace)
|
|
205
|
+
lines.push(...this.wrapNormal(paragraph, maxWidth, ctx));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return lines;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private static wrapNormal(
|
|
213
|
+
text: string,
|
|
214
|
+
maxWidth: number,
|
|
215
|
+
ctx: CanvasRenderingContext2D
|
|
216
|
+
): string[] {
|
|
217
|
+
const words = text.split(/\s+/).filter(w => w.length > 0);
|
|
218
|
+
const lines: string[] = [];
|
|
219
|
+
let currentLine = '';
|
|
220
|
+
|
|
221
|
+
for (const word of words) {
|
|
222
|
+
const testLine = currentLine ? `${currentLine} ${word}` : word;
|
|
223
|
+
const metrics = ctx.measureText(testLine);
|
|
224
|
+
|
|
225
|
+
if (metrics.width > maxWidth && currentLine) {
|
|
226
|
+
lines.push(currentLine);
|
|
227
|
+
currentLine = word;
|
|
228
|
+
} else {
|
|
229
|
+
currentLine = testLine;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (currentLine) {
|
|
234
|
+
lines.push(currentLine);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return lines.length > 0 ? lines : [''];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private static wrapPreservingWhitespace(
|
|
241
|
+
text: string,
|
|
242
|
+
maxWidth: number,
|
|
243
|
+
ctx: CanvasRenderingContext2D
|
|
244
|
+
): string[] {
|
|
245
|
+
const lines: string[] = [];
|
|
246
|
+
let currentLine = '';
|
|
247
|
+
|
|
248
|
+
for (let i = 0; i < text.length; i++) {
|
|
249
|
+
const char = text[i];
|
|
250
|
+
const testLine = currentLine + char;
|
|
251
|
+
const metrics = ctx.measureText(testLine);
|
|
252
|
+
|
|
253
|
+
if (metrics.width > maxWidth && currentLine) {
|
|
254
|
+
lines.push(currentLine);
|
|
255
|
+
currentLine = char;
|
|
256
|
+
} else {
|
|
257
|
+
currentLine = testLine;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (currentLine) {
|
|
262
|
+
lines.push(currentLine);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return lines.length > 0 ? lines : [''];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Calculate intrinsic content size for text - SMART METHOD
|
|
270
|
+
* Automatically chooses the most efficient measurement technique
|
|
271
|
+
*/
|
|
272
|
+
static calculateTextSize(
|
|
273
|
+
element: HTMLElement,
|
|
274
|
+
content: string,
|
|
275
|
+
constrainingWidth?: number,
|
|
276
|
+
constrainingHeight?: number
|
|
277
|
+
): { width: number; height: number } {
|
|
278
|
+
// Empty content
|
|
279
|
+
if (!content || content.length === 0) {
|
|
280
|
+
const style = window.getComputedStyle(element);
|
|
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
|
+
|
|
286
|
+
return {
|
|
287
|
+
width: paddingLeft + paddingRight,
|
|
288
|
+
height: paddingTop + paddingBottom
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Check complexity of content
|
|
293
|
+
const isPlain = this.isPlainText(content);
|
|
294
|
+
const hasSimple = this.hasSimpleFormatting(content);
|
|
295
|
+
|
|
296
|
+
// Strategy 1: Pure canvas for plain text (fastest)
|
|
297
|
+
if (isPlain) {
|
|
298
|
+
return this.calculatePlainTextSize(
|
|
299
|
+
element,
|
|
300
|
+
content,
|
|
301
|
+
constrainingWidth,
|
|
302
|
+
constrainingHeight
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Strategy 2: Optimized DOM for simple formatting (fast)
|
|
307
|
+
if (hasSimple) {
|
|
308
|
+
// For simple formatting, we can still use canvas but need to strip tags
|
|
309
|
+
const plainText = content.replace(/<[^>]+>/g, '');
|
|
310
|
+
return this.calculatePlainTextSize(
|
|
311
|
+
element,
|
|
312
|
+
plainText,
|
|
313
|
+
constrainingWidth,
|
|
314
|
+
constrainingHeight
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Strategy 3: DOM measurement for complex HTML (slower but accurate)
|
|
319
|
+
return this.measureWithDOM(
|
|
320
|
+
element,
|
|
321
|
+
content,
|
|
322
|
+
constrainingWidth,
|
|
323
|
+
constrainingHeight
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Calculate size for plain text using canvas (no HTML)
|
|
329
|
+
*/
|
|
330
|
+
private static calculatePlainTextSize(
|
|
331
|
+
element: HTMLElement,
|
|
332
|
+
text: string,
|
|
333
|
+
constrainingWidth?: number,
|
|
334
|
+
constrainingHeight?: number
|
|
335
|
+
): { width: number; height: number } {
|
|
336
|
+
const font = this.getFontString(element);
|
|
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;
|
|
349
|
+
|
|
350
|
+
// Adjust constraining width for padding
|
|
351
|
+
const availableWidth = constrainingWidth
|
|
352
|
+
? constrainingWidth - paddingLeft - paddingRight
|
|
353
|
+
: Infinity;
|
|
354
|
+
|
|
355
|
+
// Calculate dimensions
|
|
356
|
+
let width: number;
|
|
357
|
+
let height: number;
|
|
358
|
+
|
|
359
|
+
if (whiteSpace === 'nowrap' || whiteSpace === 'pre' || !constrainingWidth) {
|
|
360
|
+
// Single line or no width constraint
|
|
361
|
+
width = this.measureTextWidth(text, font) + paddingLeft + paddingRight;
|
|
362
|
+
height = lineHeight + paddingTop + paddingBottom;
|
|
363
|
+
} else {
|
|
364
|
+
// Multi-line text
|
|
365
|
+
const lines = this.wrapText(text, availableWidth, font, whiteSpace);
|
|
366
|
+
|
|
367
|
+
// Find the widest line
|
|
368
|
+
width = Math.max(
|
|
369
|
+
...lines.map(line => this.measureTextWidth(line, font))
|
|
370
|
+
) + paddingLeft + paddingRight;
|
|
371
|
+
|
|
372
|
+
height = (lines.length * lineHeight) + paddingTop + paddingBottom;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return { width, height };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Clear all caches (call when fonts change or for cleanup)
|
|
380
|
+
*/
|
|
381
|
+
static clearCaches(): void {
|
|
382
|
+
this.fontCache.clear();
|
|
383
|
+
this.lineHeightCache.clear();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Clean up measurement element (call on app cleanup)
|
|
388
|
+
*/
|
|
389
|
+
static cleanup(): void {
|
|
390
|
+
if (this.measurementElement && this.measurementElement.parentElement) {
|
|
391
|
+
document.body.removeChild(this.measurementElement);
|
|
392
|
+
}
|
|
393
|
+
this.measurementElement = null;
|
|
394
|
+
this.canvas = null;
|
|
395
|
+
this.context = null;
|
|
396
|
+
this.clearCaches();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
package/scripts/UITextView.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { UIColor } from "./UIColor"
|
|
2
2
|
import { UILocalizedTextObject } from "./UIInterfaces"
|
|
3
|
-
import { FIRST, IS_LIKE_NULL, nil, NO, UIObject, YES } from "./UIObject"
|
|
3
|
+
import { FIRST, IS, IS_LIKE_NULL, nil, NO, UIObject, YES } from "./UIObject"
|
|
4
4
|
import { UIRectangle } from "./UIRectangle"
|
|
5
5
|
import type { ValueOf } from "./UIObject"
|
|
6
|
+
import { UITextMeasurement } from "./UITextMeasurement"
|
|
6
7
|
import { UIView, UIViewBroadcastEvent } from "./UIView"
|
|
7
8
|
|
|
8
9
|
|
|
@@ -40,6 +41,8 @@ export class UITextView extends UIView {
|
|
|
40
41
|
static _pxToPt: number
|
|
41
42
|
_text?: string
|
|
42
43
|
|
|
44
|
+
private _useFastMeasurement: boolean | undefined;
|
|
45
|
+
|
|
43
46
|
|
|
44
47
|
constructor(
|
|
45
48
|
elementID?: string,
|
|
@@ -204,30 +207,24 @@ export class UITextView extends UIView {
|
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
set text(text) {
|
|
207
|
-
|
|
208
210
|
this._text = text
|
|
209
|
-
|
|
210
211
|
var notificationText = ""
|
|
211
|
-
|
|
212
212
|
if (this.notificationAmount) {
|
|
213
|
-
|
|
214
213
|
notificationText = "<span style=\"color: " + UITextView.notificationTextColor.stringValue + ";\">" +
|
|
215
214
|
(" (" + this.notificationAmount + ")").bold() + "</span>"
|
|
216
|
-
|
|
217
215
|
}
|
|
218
216
|
|
|
219
217
|
if (this.viewHTMLElement.innerHTML != this.textPrefix + text + this.textSuffix + notificationText) {
|
|
220
|
-
|
|
221
218
|
this.viewHTMLElement.innerHTML = this.textPrefix + FIRST(text, "") + this.textSuffix + notificationText
|
|
222
|
-
|
|
223
219
|
}
|
|
224
220
|
|
|
225
221
|
if (this.changesOften) {
|
|
226
|
-
|
|
227
222
|
this._intrinsicHeightCache = new UIObject() as any
|
|
228
223
|
this._intrinsicWidthCache = new UIObject() as any
|
|
229
|
-
|
|
230
224
|
}
|
|
225
|
+
// Invalidate measurement strategy when text changes significantly
|
|
226
|
+
this._useFastMeasurement = undefined;
|
|
227
|
+
this._intrinsicSizesCache = {};
|
|
231
228
|
|
|
232
229
|
this.setNeedsLayout()
|
|
233
230
|
|
|
@@ -452,6 +449,73 @@ export class UITextView extends UIView {
|
|
|
452
449
|
}
|
|
453
450
|
|
|
454
451
|
|
|
452
|
+
override intrinsicContentSizeWithConstraints(
|
|
453
|
+
constrainingHeight: number = 0,
|
|
454
|
+
constrainingWidth: number = 0
|
|
455
|
+
): UIRectangle {
|
|
456
|
+
const cacheKey = "h_" + constrainingHeight + "__w_" + constrainingWidth;
|
|
457
|
+
const cachedResult = this._intrinsicSizesCache[cacheKey];
|
|
458
|
+
if (cachedResult) {
|
|
459
|
+
return cachedResult;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Determine measurement strategy
|
|
463
|
+
const shouldUseFastPath = this._useFastMeasurement ?? this._shouldUseFastMeasurement();
|
|
464
|
+
|
|
465
|
+
let result: UIRectangle;
|
|
466
|
+
|
|
467
|
+
if (shouldUseFastPath) {
|
|
468
|
+
// Fast path: canvas-based measurement
|
|
469
|
+
const size = UITextMeasurement.calculateTextSize(
|
|
470
|
+
this.viewHTMLElement,
|
|
471
|
+
this.text || this.innerHTML,
|
|
472
|
+
constrainingWidth || undefined,
|
|
473
|
+
constrainingHeight || undefined
|
|
474
|
+
);
|
|
475
|
+
result = new UIRectangle(0, 0, size.height, size.width);
|
|
476
|
+
} else {
|
|
477
|
+
// Fallback: original DOM-based measurement for complex content
|
|
478
|
+
result = super.intrinsicContentSizeWithConstraints(constrainingHeight, constrainingWidth);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
this._intrinsicSizesCache[cacheKey] = result.copy();
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Helper to determine if we can use fast measurement
|
|
486
|
+
private _shouldUseFastMeasurement(): boolean {
|
|
487
|
+
const content = this.text || this.innerHTML;
|
|
488
|
+
|
|
489
|
+
// If using dynamic innerHTML with parameters, use DOM measurement
|
|
490
|
+
if (IS(this._innerHTMLKey) || IS(this._localizedTextObject)) {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Check for notification badges
|
|
495
|
+
if (this.notificationAmount > 0) {
|
|
496
|
+
return false; // Has span with colored text
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Check content complexity
|
|
500
|
+
const hasComplexHTML = /<(?!\/?(b|i|em|strong|span|br)\b)[^>]+>/i.test(content);
|
|
501
|
+
|
|
502
|
+
return !hasComplexHTML;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Optional: Allow manual override for specific instances
|
|
506
|
+
setUseFastMeasurement(useFast: boolean): void {
|
|
507
|
+
this._useFastMeasurement = useFast;
|
|
508
|
+
this._intrinsicSizesCache = {};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Optional: Force re-evaluation of measurement strategy
|
|
512
|
+
invalidateMeasurementStrategy(): void {
|
|
513
|
+
this._useFastMeasurement = undefined;
|
|
514
|
+
this._intrinsicSizesCache = {};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
|
|
455
519
|
override intrinsicContentSize() {
|
|
456
520
|
|
|
457
521
|
// This works but is slow
|
package/scripts/UIView.ts
CHANGED
|
@@ -223,7 +223,7 @@ export class UIView extends UIObject {
|
|
|
223
223
|
static _onWindowMouseMove: (event: MouseEvent) => void = nil
|
|
224
224
|
static _onWindowMouseup: (event: MouseEvent) => void = nil
|
|
225
225
|
private _resizeObserverEntry?: ResizeObserverEntry
|
|
226
|
-
|
|
226
|
+
protected _intrinsicSizesCache: Record<string, UIRectangle> = {}
|
|
227
227
|
|
|
228
228
|
|
|
229
229
|
constructor(
|