From bf8ce8be1eae48c8977d09b1d76cea10055e3a7b Mon Sep 17 00:00:00 2001 From: Mirko Pecora Date: Fri, 3 Jul 2026 09:56:56 +0200 Subject: [PATCH] fix(text): handle leading whitespace in word wrapping --- src/core/text-rendering/TextLayoutEngine.ts | 7 ++++++ .../tests/TextLayoutEngine.test.ts | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/core/text-rendering/TextLayoutEngine.ts b/src/core/text-rendering/TextLayoutEngine.ts index d64e15a..4f43f1a 100644 --- a/src/core/text-rendering/TextLayoutEngine.ts +++ b/src/core/text-rendering/TextLayoutEngine.ts @@ -368,6 +368,13 @@ export const wrapLine = ( ): WrappedLinesStruct => { const words = line.split(spaceRegex); const spaces = line.match(spaceRegex) || []; + // Leading whitespace splits into an empty token; fold it into the next + // word so it isn't dropped and re-applied after the first word instead. + if (words[0] === '' && words.length > 1) { + const leadingSpace = spaces.shift() ?? ''; + words.shift(); + words[0] = leadingSpace + (words[0] ?? ''); + } const wrappedLines: TextLineStruct[] = []; let currentLine = ''; let currentLineWidth = 0; diff --git a/src/core/text-rendering/tests/TextLayoutEngine.test.ts b/src/core/text-rendering/tests/TextLayoutEngine.test.ts index 8e19df1..41e9057 100644 --- a/src/core/text-rendering/tests/TextLayoutEngine.test.ts +++ b/src/core/text-rendering/tests/TextLayoutEngine.test.ts @@ -248,6 +248,28 @@ describe('SDF Text Utils', () => { expect(result[0][0]?.[2]).toBe(true); expect(result[1]).toBe(0); }); + + it('should keep leading whitespace attached to the first word, not the word after it', () => { + // Regression test: a line starting with padding spaces (used e.g. to + // reserve room for an inline badge before the first word) used to have + // those spaces silently reassigned as the gap AFTER the first word. + const result = wrapLine( + testMeasureText, + ' hello world', + 'Arial', + 1000, + 0, + 10, // spaceWidth + '', + 0, + 'break-word', + 10, + ); + + const [lines] = result; + expect(lines).toHaveLength(1); + expect(lines[0]?.[0]).toBe(' hello world'); + }); }); describe('wrapText', () => {