From 221ae0ea7c3dc83ee57b1bd5dc36559b25d56884 Mon Sep 17 00:00:00 2001 From: extio-de Date: Fri, 9 Oct 2026 19:13:56 +0200 Subject: [PATCH] fix(CustomSingleLineTextField, CustomMultiLineTextArea): account for trailing spaces when measuring caret and selection positions --- .../components/CustomMultiLineTextArea.java | 18 ++++++-- .../components/CustomSingleLineTextField.java | 44 ++++++++++++++++--- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomMultiLineTextArea.java b/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomMultiLineTextArea.java index 7287545..8c3b44f 100644 --- a/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomMultiLineTextArea.java +++ b/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomMultiLineTextArea.java @@ -668,6 +668,14 @@ public class CustomMultiLineTextArea extends Component { return this.cachedSpaceWidth; } + private int countTrailingSpaces(final String text) { + int count = 0; + for (int i = text.length() - 1; i >= 0 && text.charAt(i) == ' '; i--) { + count++; + } + return count; + } + private void updateCaretPosition(final int mouseX, final int mouseY) { final List wrappedLines = getWrappedLines(); final List rawLines = getLines(); @@ -696,7 +704,8 @@ public class CustomMultiLineTextArea extends Component { for (int j = 0; j <= clickedLine.length(); j++) { final String substr = clickedLine.substring(0, j); - final int textWidth = G2DDrawFont.getTextDimensions(substr, this.getGraphics(), this.fontSize, 1.0).getX(); + final int baseWidth = G2DDrawFont.getTextDimensions(substr, this.getGraphics(), this.fontSize, 1.0).getX(); + final int textWidth = baseWidth + this.countTrailingSpaces(substr) * this.getSpaceWidth(); final int dist = Math.abs(textWidth - (logicalMouseX - CONTENT_MARGIN)); if (dist < bestDist) { bestDist = dist; @@ -790,7 +799,8 @@ public class CustomMultiLineTextArea extends Component { if (text == null || text.isEmpty()) { return 0; } - return G2DDrawFont.getTextDimensions(text, g2d, this.fontSize, 1.0).getX(); + final int baseWidth = G2DDrawFont.getTextDimensions(text, g2d, this.fontSize, 1.0).getX(); + return baseWidth + this.countTrailingSpaces(text) * this.getSpaceWidth(); } private void renderTextSegment(final Graphics2D g2d, final Color color, final int x, final int y, final String text) { @@ -1074,8 +1084,8 @@ public class CustomMultiLineTextArea extends Component { ? caretWrappedLine.substring(0, caretPosInWrappedLine) : ""; final var textDim = beforeCaret.isEmpty() ? ImmutableCoordI2.create(0, 0) : G2DDrawFont.getTextDimensions(beforeCaret, g2d, this.fontSize, this.scaleFactor); - final int spaceWidth = beforeCaret.endsWith(" ") ? getSpaceWidth() : 0; - final int caretX = textDim.getX() + toScreenCoord(CONTENT_MARGIN) + (int) (spaceWidth * this.scaleFactor); + final int trailingSpaceWidth = this.countTrailingSpaces(beforeCaret) * (int) (this.getSpaceWidth() * this.scaleFactor); + final int caretX = textDim.getX() + toScreenCoord(CONTENT_MARGIN) + trailingSpaceWidth; final int caretY = (int) ((CONTENT_MARGIN + caretWrappedLineIndex * lineHeight - this.scrollOffsetY) * this.scaleFactor); final int caretHeight = (int) (rawFontHeight * this.scaleFactor); diff --git a/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomSingleLineTextField.java b/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomSingleLineTextField.java index e65edfe..fb2dbd9 100644 --- a/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomSingleLineTextField.java +++ b/src/main/java/de/extio/game_engine/renderer/g2d/control/components/CustomSingleLineTextField.java @@ -60,6 +60,10 @@ public class CustomSingleLineTextField extends Component { private int cachedRawFontHeight = 0; + private int cachedSpaceWidth = 0; + + private int cachedSpaceWidthFontSize = 0; + private final ThemeManager themeManager; private final Consumer onTextChanged; @@ -382,7 +386,8 @@ public class CustomSingleLineTextField extends Component { for (int j = 0; j <= this.text.length(); j++) { final String substr = this.text.substring(0, j); - final int textWidth = G2DDrawFont.getTextDimensions(substr, this.getGraphics(), this.fontSize, 1.0).getX(); + final int baseWidth = G2DDrawFont.getTextDimensions(substr, this.getGraphics(), this.fontSize, 1.0).getX(); + final int textWidth = baseWidth + this.countTrailingSpaces(substr) * this.getSpaceWidth(); final int dist = Math.abs(textWidth - textX); if (dist < bestDist) { bestDist = dist; @@ -400,7 +405,8 @@ public class CustomSingleLineTextField extends Component { } final String beforeCaret = this.text.substring(0, this.caretPosition); - final int caretX = G2DDrawFont.getTextDimensions(beforeCaret, this.getGraphics(), this.fontSize, 1.0).getX(); + final int baseCaretX = G2DDrawFont.getTextDimensions(beforeCaret, this.getGraphics(), this.fontSize, 1.0).getX(); + final int caretX = baseCaretX + this.countTrailingSpaces(beforeCaret) * this.getSpaceWidth(); final int visibleWidth = getVisibleWidth(); if (caretX < this.scrollOffsetX) { @@ -418,6 +424,30 @@ public class CustomSingleLineTextField extends Component { return (int) (this.getWidth() / this.scaleFactor); } + private int getSpaceWidth() { + if (this.cachedSpaceWidth > 0 && this.cachedSpaceWidthFontSize == this.fontSize) { + return this.cachedSpaceWidth; + } + + if (this.getGraphics() == null) { + return 10; + } + + final int widthWithoutSpace = G2DDrawFont.getTextDimensions("MM", this.getGraphics(), this.fontSize, 1.0).getX(); + final int widthWithSpace = G2DDrawFont.getTextDimensions("M M", this.getGraphics(), this.fontSize, 1.0).getX(); + this.cachedSpaceWidth = widthWithSpace - widthWithoutSpace; + this.cachedSpaceWidthFontSize = this.fontSize; + return this.cachedSpaceWidth; + } + + private int countTrailingSpaces(final String text) { + int count = 0; + for (int i = text.length() - 1; i >= 0 && text.charAt(i) == ' '; i--) { + count++; + } + return count; + } + private int toLogicalCoord(final int screenCoord) { return (int) (screenCoord / this.scaleFactor); } @@ -574,8 +604,11 @@ public class CustomSingleLineTextField extends Component { final String selected = this.text.substring(selStart, selEnd); final String afterSel = this.text.substring(selEnd); - final int beforeSelWidth = G2DDrawFont.getTextDimensions(beforeSel, g2d, this.fontSize, this.scaleFactor).getX(); - final int selWidth = G2DDrawFont.getTextDimensions(selected, g2d, this.fontSize, this.scaleFactor).getX(); + final int baseBeforeSelWidth = G2DDrawFont.getTextDimensions(beforeSel, g2d, this.fontSize, this.scaleFactor).getX(); + final int baseSelWidth = G2DDrawFont.getTextDimensions(selected, g2d, this.fontSize, this.scaleFactor).getX(); + final int spaceWidthScaled = (int) (this.getSpaceWidth() * this.scaleFactor); + final int beforeSelWidth = baseBeforeSelWidth + this.countTrailingSpaces(beforeSel) * spaceWidthScaled; + final int selWidth = baseSelWidth + this.countTrailingSpaces(selected) * spaceWidthScaled; final int drawOffsetX = TEXT_PADDING - (int) (this.scrollOffsetX * this.scaleFactor); if (!beforeSel.isEmpty()) { @@ -615,7 +648,8 @@ public class CustomSingleLineTextField extends Component { final var textDim = beforeCaret.isEmpty() ? ImmutableCoordI2.create(0, 0) : G2DDrawFont.getTextDimensions(beforeCaret, g2d, this.fontSize, this.scaleFactor); - final int caretX = textDim.getX() + TEXT_PADDING - (int) (this.scrollOffsetX * this.scaleFactor); + final int trailingSpaceWidth = this.countTrailingSpaces(beforeCaret) * (int) (this.getSpaceWidth() * this.scaleFactor); + final int caretX = textDim.getX() + trailingSpaceWidth + TEXT_PADDING - (int) (this.scrollOffsetX * this.scaleFactor); final int caretY = (int) (textY * this.scaleFactor); final int caretHeight = (int) (rawFontHeight * this.scaleFactor);