@solidtv/renderer 1.0.7 → 1.1.0

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.
@@ -227,7 +227,9 @@ export const wrapText = (
227
227
  : [[['', 0, false, 0, 0]], remainingLines, i < lines.length - 1];
228
228
 
229
229
  remainingLines--;
230
- wrappedLines.push(...wrappedLine);
230
+ for (let j = 0; j < wrappedLine.length; j++) {
231
+ wrappedLines.push(wrappedLine[j]!);
232
+ }
231
233
 
232
234
  if (hasMaxLines === true && remainingLines <= 0) {
233
235
  const lastLine = wrappedLines[wrappedLines.length - 1]!;
@@ -278,44 +280,75 @@ export const wrapLine = (
278
280
  let hasRemainingText = true;
279
281
 
280
282
  const wrapFn = getWrapStrategy(wordBreak);
281
- while (words.length > 0 && remainingLines > 0) {
282
- let word = words.shift()!;
283
- let wordWidth = measureText(word, fontFamily, letterSpacing);
283
+ let wordIdx = 0;
284
+ let spaceIdx = 0;
285
+ let pendingWord = '';
286
+
287
+ while (
288
+ (pendingWord.length > 0 || wordIdx < words.length) &&
289
+ remainingLines > 0
290
+ ) {
291
+ let word: string;
292
+ let wordWidth: number;
284
293
  let remainingWord = '';
285
294
 
295
+ if (pendingWord.length > 0) {
296
+ word = pendingWord;
297
+ pendingWord = '';
298
+ } else {
299
+ word = words[wordIdx++]!;
300
+ }
301
+ wordWidth = measureText(word, fontFamily, letterSpacing);
302
+
286
303
  //handle first word of new line separately to avoid empty line issues
287
304
  if (currentLineWidth === 0) {
288
305
  // Word doesn't fit on current line
289
306
  //if first word doesn't fit on empty line
290
307
  if (wordWidth > maxWidth) {
291
308
  remainingLines--;
309
+ const isLastLine = remainingLines === 0;
310
+ let lineTruncated = isLastLine;
292
311
  //truncate word to fit
293
- [word, remainingWord, wordWidth] =
294
- remainingLines === 0
295
- ? truncateWord(
296
- measureText,
297
- word,
298
- wordWidth,
299
- maxWidth,
300
- fontFamily,
301
- letterSpacing,
302
- overflowSuffix,
303
- overflowWidth,
304
- )
305
- : splitWord(
306
- measureText,
307
- word,
308
- wordWidth,
309
- maxWidth,
310
- fontFamily,
311
- letterSpacing,
312
- );
312
+ [word, remainingWord, wordWidth] = isLastLine
313
+ ? truncateWord(
314
+ measureText,
315
+ word,
316
+ wordWidth,
317
+ maxWidth,
318
+ fontFamily,
319
+ letterSpacing,
320
+ overflowSuffix,
321
+ overflowWidth,
322
+ )
323
+ : splitWord(
324
+ measureText,
325
+ word,
326
+ wordWidth,
327
+ maxWidth,
328
+ fontFamily,
329
+ letterSpacing,
330
+ );
313
331
 
314
332
  if (remainingWord.length > 0) {
315
- words.unshift(remainingWord);
333
+ if (word.length === 0) {
334
+ if (overflowSuffix.length > 0) {
335
+ word = overflowSuffix;
336
+ wordWidth = overflowWidth;
337
+ } else {
338
+ word = remainingWord.charAt(0);
339
+ if (word.length === 0) {
340
+ break;
341
+ }
342
+ wordWidth = measureText(word, fontFamily, letterSpacing);
343
+ }
344
+ remainingWord = '';
345
+ remainingLines = 0;
346
+ lineTruncated = true;
347
+ }
348
+ pendingWord = remainingWord;
316
349
  }
317
350
  // first word doesn't fit on an empty line
318
- wrappedLines.push([word, wordWidth, false, 0, 0]);
351
+ wrappedLines.push([word, wordWidth, lineTruncated, 0, 0]);
319
352
  } else if (wordWidth + spaceWidth >= maxWidth) {
320
353
  remainingLines--;
321
354
  // word with space doesn't fit, but word itself fits - put on new line
@@ -326,7 +359,7 @@ export const wrapLine = (
326
359
  }
327
360
  continue;
328
361
  }
329
- const space = spaces.shift() || '';
362
+ const space = spaces[spaceIdx++] || '';
330
363
  // For width calculation, treat ZWSP as having 0 width but regular space functionality
331
364
  const effectiveSpaceWidth = space === '\u200B' ? 0 : spaceWidth;
332
365
  const totalWidth = currentLineWidth + effectiveSpaceWidth + wordWidth;
@@ -367,7 +400,7 @@ export const wrapLine = (
367
400
  );
368
401
 
369
402
  if (remainingWord.length > 0) {
370
- words.unshift(remainingWord);
403
+ pendingWord = remainingWord;
371
404
  }
372
405
  }
373
406
 
@@ -228,6 +228,26 @@ describe('SDF Text Utils', () => {
228
228
  expect(result[0].length).toBeGreaterThan(1);
229
229
  expect(result[0][0]?.[0]).toBe('hello world test');
230
230
  });
231
+
232
+ it('should return non-empty output when maxWidth is smaller than glyph and suffix width', () => {
233
+ const result = wrapLine(
234
+ testMeasureText,
235
+ 'hello',
236
+ 'Arial',
237
+ 5,
238
+ 0,
239
+ 10,
240
+ '...',
241
+ 30,
242
+ 'break-word',
243
+ 1,
244
+ );
245
+
246
+ expect(result[0]).toHaveLength(1);
247
+ expect(result[0][0]?.[0]).toBe('...');
248
+ expect(result[0][0]?.[2]).toBe(true);
249
+ expect(result[1]).toBe(0);
250
+ });
231
251
  });
232
252
 
233
253
  describe('wrapText', () => {
@@ -680,8 +680,12 @@ export class RendererMain extends EventEmitter {
680
680
  */
681
681
  createNode<ShNode extends CoreShaderNode<any>>(
682
682
  props: Partial<INodeProps<ShNode>>,
683
+ resolved = false,
683
684
  ): INode<ShNode> {
684
- const node = this.stage.createNode(props as Partial<CoreNodeProps>);
685
+ const node = this.stage.createNode(
686
+ props as Partial<CoreNodeProps>,
687
+ resolved,
688
+ );
685
689
 
686
690
  if (ENABLE_INSPECTOR && this.inspector) {
687
691
  return this.inspector.createNode(node) as unknown as INode<ShNode>;
@@ -690,6 +694,20 @@ export class RendererMain extends EventEmitter {
690
694
  return node as unknown as INode<ShNode>;
691
695
  }
692
696
 
697
+ /**
698
+ * Allocate a fully-resolved CoreNodeProps bag — the same shape and
699
+ * defaults the renderer would otherwise build inside `createNode`.
700
+ *
701
+ * Frameworks (e.g. solid-tv) call this once per node at construction
702
+ * time, fill it in as user props flow in, then pass it back via
703
+ * `createNode(props, true)`. The renderer adopts the bag directly:
704
+ * one allocation instead of two, and a stable hidden class for the
705
+ * node's lifetime.
706
+ */
707
+ createNodeProps(initial?: Partial<CoreNodeProps>): CoreNodeProps {
708
+ return this.stage.createNodeProps(initial);
709
+ }
710
+
693
711
  /**
694
712
  * Create a new scene graph text node
695
713
  *
@@ -704,8 +722,11 @@ export class RendererMain extends EventEmitter {
704
722
  * @param props
705
723
  * @returns
706
724
  */
707
- createTextNode(props: Partial<ITextNodeProps>): ITextNode {
708
- const textNode = this.stage.createTextNode(props as CoreTextNodeProps);
725
+ createTextNode(props: Partial<ITextNodeProps>, resolved = false): ITextNode {
726
+ const textNode = this.stage.createTextNode(
727
+ props as CoreTextNodeProps,
728
+ resolved,
729
+ );
709
730
 
710
731
  if (ENABLE_INSPECTOR && this.inspector) {
711
732
  return this.inspector.createTextNode(textNode) as unknown as ITextNode;
@@ -714,6 +735,14 @@ export class RendererMain extends EventEmitter {
714
735
  return textNode as unknown as ITextNode;
715
736
  }
716
737
 
738
+ /**
739
+ * Allocate a fully-resolved CoreTextNodeProps bag. See
740
+ * {@link createNodeProps}.
741
+ */
742
+ createTextNodeProps(initial?: Partial<CoreTextNodeProps>): CoreTextNodeProps {
743
+ return this.stage.createTextNodeProps(initial);
744
+ }
745
+
717
746
  /**
718
747
  * Destroy a node
719
748
  *