From a1322cb474fef6ce3540cea953dbebe4c0968172 Mon Sep 17 00:00:00 2001 From: Joe Williamson Date: Wed, 5 Aug 2026 09:01:15 -0700 Subject: [PATCH 1/4] fixes word wrap when text length is <2 Fixes highlighting --- h2d/Text.hx | 2 ++ h2d/TextInput.hx | 12 ++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/h2d/Text.hx b/h2d/Text.hx index a043958b97..1e853881f6 100644 --- a/h2d/Text.hx +++ b/h2d/Text.hx @@ -340,6 +340,8 @@ class Text extends Drawable { } else { maxWidth -= afterData; } + if (maxWidth <= 2) + return text; if ( font == null ) font = this.font; var lines = []; var x = leftMargin; diff --git a/h2d/TextInput.hx b/h2d/TextInput.hx index 9dd410c15a..0ec6e92406 100644 --- a/h2d/TextInput.hx +++ b/h2d/TextInput.hx @@ -286,11 +286,11 @@ class TextInput extends Text { return; case K.C if (K.isDown(K.CTRL)): if( text != "" && selectionRange != null ) { - hxd.System.setClipboardText(text.substr(selectionRange.start, selectionRange.length)); + hxd.System.setClipboardText(getSelectedText()); } case K.X if (K.isDown(K.CTRL)): if( text != "" && selectionRange != null ) { - if(hxd.System.setClipboardText(text.substr(selectionRange.start, selectionRange.length))) { + if(hxd.System.setClipboardText(getSelectedText())) { if( !canEdit ) return; beforeChange(); cutSelection(); @@ -432,9 +432,13 @@ class TextInput extends Text { if (cursorIndex >= len) { return len; } + // ret walks the text itself, so it stops at the end of the TEXT: getTextLength + // is the expanded length, longer than the text by one per inserted line break, + // and bounding a text position with it runs past the end. + var max = text.length; var ret = getTextPos(cursorIndex); - while (ret < len && isWordLimit(ret)) ret++; - while (ret < len && !isWordLimit(ret)) ret++; + while (ret < max && isWordLimit(ret)) ret++; + while (ret < max && !isWordLimit(ret)) ret++; return getCursorPos(ret); } From 1691cde933e6dd5d1c30aaa25d3e6888ce18ff76 Mon Sep 17 00:00:00 2001 From: Joe Williamson <2074385+hoseyjoe@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:17:57 -0700 Subject: [PATCH 2/4] TextInput: copy and cut the text the selection actually covers cursorIndex and selectionRange count the EXPANDED text - the text with the line breaks word wrapping inserted, as getTextPos's documentation says - and every operation on the text itself converts through getTextPos first. Ctrl+C and Ctrl+X did not: they took `text.substr(selectionRange.start, selectionRange.length)` straight from the text, so on any wrapped line the clipboard was given a copy shifted by one character per wrap above the selection, while cutSelection removed the correct characters. Cutting a word and pasting it back therefore lost a letter. getSelectedText already does the conversion, so both now use it. getWordEnd had the mirror of the same confusion: `ret` is a position in the text, but it was bounded by getTextLength(), which is the expanded length and so longer than the text by one per inserted break. A double-click on a word at the end of a wrapped text selected one character past the end of it. --- h2d/TextInput.hx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/h2d/TextInput.hx b/h2d/TextInput.hx index 316a1d35b5..1b8127b955 100644 --- a/h2d/TextInput.hx +++ b/h2d/TextInput.hx @@ -289,11 +289,11 @@ class TextInput extends Text { return; case K.C if (K.isDown(K.CTRL)): if( text != "" && selectionRange != null ) { - hxd.System.setClipboardText(text.substr(selectionRange.start, selectionRange.length)); + hxd.System.setClipboardText(getSelectedText()); } case K.X if (K.isDown(K.CTRL)): if( text != "" && selectionRange != null ) { - if(hxd.System.setClipboardText(text.substr(selectionRange.start, selectionRange.length))) { + if(hxd.System.setClipboardText(getSelectedText())) { if( !canEdit ) return; beforeChange(); cutSelection(); @@ -435,9 +435,13 @@ class TextInput extends Text { if (cursorIndex >= len) { return len; } + // ret walks the text itself, so it stops at the end of the TEXT: getTextLength + // is the expanded length, longer than the text by one per inserted line break, + // and bounding a text position with it runs past the end. + var max = text.length; var ret = getTextPos(cursorIndex); - while (ret < len && isWordLimit(ret)) ret++; - while (ret < len && !isWordLimit(ret)) ret++; + while (ret < max && isWordLimit(ret)) ret++; + while (ret < max && !isWordLimit(ret)) ret++; return getCursorPos(ret); } From 8d486c4c564e683375153cbb36106bb350c77ba4 Mon Sep 17 00:00:00 2001 From: Joe Williamson Date: Sat, 3 Oct 2026 08:10:36 -0700 Subject: [PATCH 3/4] Text: fix splitRawText infinite loop when wrap width is below one glyph When a break char already overflows maxWidth, lastBreak is set to i - 1, which can point before the start of the current line (lastPos - 1). The rewind branch then flushes an empty line without advancing i or lastPos, and the loop repeats forever. This happens e.g. with maxWidth = 0 (layouts that measure at zero width first) and CJK text, or TextInput with trimTrailingSpaces = false. Only rewind to a break point inside the current line, so every rewind consumes at least one character. lastPos is never negative, so this still implies the old lastBreak >= 0 check. Fixes #1347 Co-Authored-By: Claude Opus 5.5 --- h2d/Text.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/h2d/Text.hx b/h2d/Text.hx index 34e1c42310..4aa8098360 100644 --- a/h2d/Text.hx +++ b/h2d/Text.hx @@ -385,7 +385,7 @@ class Text extends Drawable { x += esize; prevChar = cc; if( lineBreak ) { - if( x > maxWidth && lastBreak >= 0 && (!trimTrailingSpaces || !font.charset.isSpace(cc) || startX > maxWidth) ) { + if( x > maxWidth && lastBreak >= lastPos && (!trimTrailingSpaces || !font.charset.isSpace(cc) || startX > maxWidth) ) { i = lastBreak; x = lastBreakX; flushLine(i + 1); From 9e8e703d42c2eae1c22c1e3d2e9d9f64794c6b1b Mon Sep 17 00:00:00 2001 From: Joe Williamson Date: Mon, 5 Oct 2026 23:04:52 -0700 Subject: [PATCH 4/4] RenderContext: render zones inside a filter start at the filter texture's origin Object.drawFilters pushes the filter texture with the (xMin, yMin) of its bounds as the target's start, but setRZ mapped a zone through the filter matrix (relative to the filtered object's origin, already in texture pixels) and then through the scene viewport. The zone was therefore offset by (xMin, yMin) - e.g. 2px up and left under a Blur/Glow with radius 1 - and scaled a second time when the viewport is scaled. Visible as a single-line TextInput, drawn inside any filtered parent, having the descenders of its text clipped off. Co-Authored-By: Claude Opus 5.5 --- h2d/RenderContext.hx | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/h2d/RenderContext.hx b/h2d/RenderContext.hx index 86a1413bef..8389b4832d 100644 --- a/h2d/RenderContext.hx +++ b/h2d/RenderContext.hx @@ -544,10 +544,22 @@ class RenderContext extends h3d.impl.RenderContext { var ry1 = x * fb.x + y * fb.y + fb.z; var rx2 = x2 * fa.x + y2 * fa.y + fa.z; var ry2 = x2 * fb.x + y2 * fb.y + fb.z; - x = rx1; - y = ry1; - w = rx2 - rx1; - h = ry2 - ry1; + // The filter matrix lands in the filter texture's own pixels (it already + // carries the filter's resolution), but measured from the filtered + // object's origin, while the texture starts at the (xMin, yMin) it was + // pushed with (Object.drawFilters) - which pushTarget folded into + // viewX/viewY. Measured from the scene viewport instead, the zone sat + // that far off (a blur's boundsExtend, 2px up and left) and scaled + // a second time by the viewport. + var startX = -(viewX + 1) / viewA; + var startY = -(viewY + 1) / viewD; + engine.setRenderZone( + Std.int(rx1 - startX + 1e-10), + Std.int(ry1 - startY + 1e-10), + Std.int(rx2 - rx1 + 1e-10), + Std.int(ry2 - ry1 + 1e-10) + ); + return; } engine.setRenderZone(