fix(CustomSingleLineTextField): stable character positioning while selected and advance-based caret
Selection rendering now draws the full text in a single layout and overlays the selection rectangle (mirroring CustomMultiLineTextArea) instead of positioning split segments by ink width, which shifted characters when a selection was active. Caret, click mapping and scroll-ensure now use the new G2DDrawFont.getTextAdvance() (pen advance incl. spaces) instead of ink bounds plus trailing-space fudge; removed the now-unnecessary space-width helpers.
This commit is contained in:
@@ -259,6 +259,15 @@ public class G2DDrawFont extends G2DAbstractRenderingBo implements DrawFontRende
|
|||||||
return MutableCoordI2.create(pixBounds.x + pixBounds.width, pixBounds.height - (int) pixBounds.getMaxY());
|
return MutableCoordI2.create(pixBounds.x + pixBounds.width, pixBounds.height - (int) pixBounds.getMaxY());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int getTextAdvance(final String text, final Graphics graphics, final int size_, final double scaleFactor) {
|
||||||
|
if (text == null || text.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final var textLayout = createTextLayout(text, graphics, size_, scaleFactor);
|
||||||
|
return (int) Math.round(textLayout.getAdvance());
|
||||||
|
}
|
||||||
|
|
||||||
private static TextLayout createTextLayout(final String text, final Graphics graphics, final int size_, final double scaleFactor) {
|
private static TextLayout createTextLayout(final String text, final Graphics graphics, final int size_, final double scaleFactor) {
|
||||||
final var cacheKey = new TextLayoutParameters(text, size_, scaleFactor);
|
final var cacheKey = new TextLayoutParameters(text, size_, scaleFactor);
|
||||||
return textLayoutCache.get(cacheKey, key -> {
|
return textLayoutCache.get(cacheKey, key -> {
|
||||||
|
|||||||
@@ -60,10 +60,6 @@ public class CustomSingleLineTextField extends Component {
|
|||||||
|
|
||||||
private int cachedRawFontHeight = 0;
|
private int cachedRawFontHeight = 0;
|
||||||
|
|
||||||
private int cachedSpaceWidth = 0;
|
|
||||||
|
|
||||||
private int cachedSpaceWidthFontSize = 0;
|
|
||||||
|
|
||||||
private final ThemeManager themeManager;
|
private final ThemeManager themeManager;
|
||||||
|
|
||||||
private final Consumer<String> onTextChanged;
|
private final Consumer<String> onTextChanged;
|
||||||
@@ -386,8 +382,7 @@ public class CustomSingleLineTextField extends Component {
|
|||||||
|
|
||||||
for (int j = 0; j <= this.text.length(); j++) {
|
for (int j = 0; j <= this.text.length(); j++) {
|
||||||
final String substr = this.text.substring(0, j);
|
final String substr = this.text.substring(0, j);
|
||||||
final int baseWidth = G2DDrawFont.getTextDimensions(substr, this.getGraphics(), this.fontSize, 1.0).getX();
|
final int textWidth = G2DDrawFont.getTextAdvance(substr, this.getGraphics(), this.fontSize, 1.0);
|
||||||
final int textWidth = baseWidth + this.countTrailingSpaces(substr) * this.getSpaceWidth();
|
|
||||||
final int dist = Math.abs(textWidth - textX);
|
final int dist = Math.abs(textWidth - textX);
|
||||||
if (dist < bestDist) {
|
if (dist < bestDist) {
|
||||||
bestDist = dist;
|
bestDist = dist;
|
||||||
@@ -405,8 +400,7 @@ public class CustomSingleLineTextField extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final String beforeCaret = this.text.substring(0, this.caretPosition);
|
final String beforeCaret = this.text.substring(0, this.caretPosition);
|
||||||
final int baseCaretX = G2DDrawFont.getTextDimensions(beforeCaret, this.getGraphics(), this.fontSize, 1.0).getX();
|
final int caretX = G2DDrawFont.getTextAdvance(beforeCaret, this.getGraphics(), this.fontSize, 1.0);
|
||||||
final int caretX = baseCaretX + this.countTrailingSpaces(beforeCaret) * this.getSpaceWidth();
|
|
||||||
final int visibleWidth = getVisibleWidth();
|
final int visibleWidth = getVisibleWidth();
|
||||||
|
|
||||||
if (caretX < this.scrollOffsetX) {
|
if (caretX < this.scrollOffsetX) {
|
||||||
@@ -424,30 +418,6 @@ public class CustomSingleLineTextField extends Component {
|
|||||||
return (int) (this.getWidth() / this.scaleFactor);
|
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) {
|
private int toLogicalCoord(final int screenCoord) {
|
||||||
return (int) (screenCoord / this.scaleFactor);
|
return (int) (screenCoord / this.scaleFactor);
|
||||||
}
|
}
|
||||||
@@ -602,31 +572,28 @@ public class CustomSingleLineTextField extends Component {
|
|||||||
if (hasSelection && !this.text.isEmpty()) {
|
if (hasSelection && !this.text.isEmpty()) {
|
||||||
final String beforeSel = this.text.substring(0, selStart);
|
final String beforeSel = this.text.substring(0, selStart);
|
||||||
final String selected = this.text.substring(selStart, selEnd);
|
final String selected = this.text.substring(selStart, selEnd);
|
||||||
final String afterSel = this.text.substring(selEnd);
|
|
||||||
|
|
||||||
final int baseBeforeSelWidth = G2DDrawFont.getTextDimensions(beforeSel, g2d, this.fontSize, this.scaleFactor).getX();
|
final int originX = TEXT_PADDING - this.scrollOffsetX;
|
||||||
final int baseSelWidth = G2DDrawFont.getTextDimensions(selected, g2d, this.fontSize, this.scaleFactor).getX();
|
final int beforeSelWidth = G2DDrawFont.getTextAdvance(beforeSel, g2d, this.fontSize, 1.0);
|
||||||
final int spaceWidthScaled = (int) (this.getSpaceWidth() * this.scaleFactor);
|
final int selWidth = G2DDrawFont.getTextAdvance(selected, g2d, this.fontSize, 1.0);
|
||||||
final int beforeSelWidth = baseBeforeSelWidth + this.countTrailingSpaces(beforeSel) * spaceWidthScaled;
|
final int selX = originX + beforeSelWidth;
|
||||||
final int selWidth = baseSelWidth + this.countTrailingSpaces(selected) * spaceWidthScaled;
|
final int selScreenX = (int) (selX * this.scaleFactor);
|
||||||
final int drawOffsetX = TEXT_PADDING - (int) (this.scrollOffsetX * this.scaleFactor);
|
final int selScreenY = (int) (textY * this.scaleFactor);
|
||||||
|
final int selScreenWidth = (int) (selWidth * this.scaleFactor);
|
||||||
|
final int selScreenHeight = (int) (rawFontHeight * this.scaleFactor);
|
||||||
|
|
||||||
if (!beforeSel.isEmpty()) {
|
|
||||||
g2d.setColor(fgColor);
|
g2d.setColor(fgColor);
|
||||||
G2DDrawFont.renderText(g2d, null, this.scaleFactor, (int) (TEXT_PADDING / this.scaleFactor) - this.scrollOffsetX, textY, this.fontSize, beforeSel);
|
G2DDrawFont.renderText(g2d, null, this.scaleFactor, originX, textY, this.fontSize, this.text);
|
||||||
}
|
|
||||||
|
|
||||||
|
if (selScreenWidth > 0) {
|
||||||
g2d.setColor(theme.getSelectionPrimary().toColor());
|
g2d.setColor(theme.getSelectionPrimary().toColor());
|
||||||
g2d.fillRect(drawOffsetX + beforeSelWidth, (int) (textY * this.scaleFactor), selWidth, (int) (rawFontHeight * this.scaleFactor));
|
g2d.fillRect(selScreenX, selScreenY, selScreenWidth, selScreenHeight);
|
||||||
|
|
||||||
|
final var previousClip = g2d.getClip();
|
||||||
|
g2d.clipRect(selScreenX, selScreenY, selScreenWidth, selScreenHeight);
|
||||||
g2d.setColor(theme.getBackgroundNormal().toColor());
|
g2d.setColor(theme.getBackgroundNormal().toColor());
|
||||||
G2DDrawFont.renderText(g2d, null, this.scaleFactor,
|
G2DDrawFont.renderText(g2d, null, this.scaleFactor, originX, textY, this.fontSize, this.text);
|
||||||
(int) ((drawOffsetX + beforeSelWidth) / this.scaleFactor), textY, this.fontSize, selected);
|
g2d.setClip(previousClip);
|
||||||
|
|
||||||
if (!afterSel.isEmpty()) {
|
|
||||||
g2d.setColor(fgColor);
|
|
||||||
G2DDrawFont.renderText(g2d, null, this.scaleFactor,
|
|
||||||
(int) ((drawOffsetX + beforeSelWidth + selWidth) / this.scaleFactor), textY, this.fontSize, afterSel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -645,11 +612,8 @@ public class CustomSingleLineTextField extends Component {
|
|||||||
|
|
||||||
if (this.caretVisible) {
|
if (this.caretVisible) {
|
||||||
final String beforeCaret = this.text.substring(0, this.caretPosition);
|
final String beforeCaret = this.text.substring(0, this.caretPosition);
|
||||||
final var textDim = beforeCaret.isEmpty()
|
final int caretX = (int) ((TEXT_PADDING - this.scrollOffsetX) * this.scaleFactor)
|
||||||
? ImmutableCoordI2.create(0, 0)
|
+ (int) (G2DDrawFont.getTextAdvance(beforeCaret, g2d, this.fontSize, 1.0) * this.scaleFactor);
|
||||||
: G2DDrawFont.getTextDimensions(beforeCaret, g2d, this.fontSize, 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 caretY = (int) (textY * this.scaleFactor);
|
||||||
final int caretHeight = (int) (rawFontHeight * this.scaleFactor);
|
final int caretHeight = (int) (rawFontHeight * this.scaleFactor);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user