fix(CustomMultiLineTextArea): adjust content margins and improve text rendering logic

This commit is contained in:
extio-de
2026-05-25 11:28:10 +02:00
parent a06846c34b
commit 9db3e61692

View File

@@ -38,7 +38,7 @@ public class CustomMultiLineTextArea extends Component {
private static final int CARET_BLINK_INTERVAL = 500; private static final int CARET_BLINK_INTERVAL = 500;
private static final int TEXT_PADDING = 2; private static final int CONTENT_MARGIN = 5;
private static final int TEXT_AREA_PADDING = 4; private static final int TEXT_AREA_PADDING = 4;
@@ -522,7 +522,7 @@ public class CustomMultiLineTextArea extends Component {
} }
private int getTextAreaWidth() { private int getTextAreaWidth() {
return (int) ((this.getWidth() / this.scaleFactor) - SCROLLBAR_WIDTH - TEXT_AREA_PADDING); return Math.max(0, (int) ((this.getWidth() / this.scaleFactor) - SCROLLBAR_WIDTH - TEXT_AREA_PADDING - (CONTENT_MARGIN * 2)));
} }
private List<String> wrapLine(final String line, final int maxWidth) { private List<String> wrapLine(final String line, final int maxWidth) {
@@ -678,7 +678,7 @@ public class CustomMultiLineTextArea extends Component {
final int logicalMouseX = toLogicalCoord(mouseX); final int logicalMouseX = toLogicalCoord(mouseX);
final int logicalMouseY = toLogicalCoord(mouseY); final int logicalMouseY = toLogicalCoord(mouseY);
final int clickedWrappedLine = Math.max(0, Math.min(wrappedLines.size() - 1, (logicalMouseY + this.scrollOffsetY) / lineHeight)); final int clickedWrappedLine = Math.max(0, Math.min(wrappedLines.size() - 1, (Math.max(0, logicalMouseY - CONTENT_MARGIN) + this.scrollOffsetY) / lineHeight));
int wrappedLineCount = 0; int wrappedLineCount = 0;
int charOffset = 0; int charOffset = 0;
@@ -697,7 +697,7 @@ public class CustomMultiLineTextArea extends Component {
for (int j = 0; j <= clickedLine.length(); j++) { for (int j = 0; j <= clickedLine.length(); j++) {
final String substr = clickedLine.substring(0, j); final String substr = clickedLine.substring(0, j);
final int textWidth = G2DDrawFont.getTextDimensions(substr, this.getGraphics(), this.fontSize, 1.0).getX(); final int textWidth = G2DDrawFont.getTextDimensions(substr, this.getGraphics(), this.fontSize, 1.0).getX();
final int dist = Math.abs(textWidth - (logicalMouseX - TEXT_PADDING)); final int dist = Math.abs(textWidth - (logicalMouseX - CONTENT_MARGIN));
if (dist < bestDist) { if (dist < bestDist) {
bestDist = dist; bestDist = dist;
bestPos = j; bestPos = j;
@@ -775,7 +775,7 @@ public class CustomMultiLineTextArea extends Component {
} }
private int getVisibleHeight() { private int getVisibleHeight() {
return (int) (this.getHeight() / this.scaleFactor); return Math.max(0, (int) (this.getHeight() / this.scaleFactor) - (CONTENT_MARGIN * 2));
} }
private int toLogicalCoord(final int screenCoord) { private int toLogicalCoord(final int screenCoord) {
@@ -786,13 +786,51 @@ public class CustomMultiLineTextArea extends Component {
return (int) (logicalCoord * this.scaleFactor); return (int) (logicalCoord * this.scaleFactor);
} }
private int getLogicalTextWidth(final Graphics2D g2d, final String text) {
if (text == null || text.isEmpty()) {
return 0;
}
return G2DDrawFont.getTextDimensions(text, g2d, this.fontSize, 1.0).getX();
}
private void renderTextSegment(final Graphics2D g2d, final Color color, final int x, final int y, final String text) {
if (text == null || text.isEmpty()) {
return;
}
g2d.setColor(color);
G2DDrawFont.renderText(g2d, null, this.scaleFactor, x, y, this.fontSize, text);
}
private void renderSelectedLine(final Graphics2D g2d, final Theme theme, final Color fgColor, final int x, final int y, final int lineHeight,
final String line, final int lineSelStart, final int lineSelEnd) {
final var beforeSelection = line.substring(0, lineSelStart);
final var throughSelection = line.substring(0, lineSelEnd);
final var selectionStartX = x + this.getLogicalTextWidth(g2d, beforeSelection);
final var selectionEndX = x + this.getLogicalTextWidth(g2d, throughSelection);
final var selectionWidth = Math.max(0, selectionEndX - selectionStartX);
this.renderTextSegment(g2d, fgColor, x, y, line);
if (selectionWidth == 0) {
return;
}
g2d.setColor(theme.getSelectionPrimary().toColor());
g2d.fillRect(this.toScreenCoord(selectionStartX), this.toScreenCoord(y), this.toScreenCoord(selectionWidth), this.toScreenCoord(lineHeight));
final var previousClip = g2d.getClip();
g2d.clipRect(this.toScreenCoord(selectionStartX), this.toScreenCoord(y), this.toScreenCoord(selectionWidth), this.toScreenCoord(lineHeight));
this.renderTextSegment(g2d, theme.getBackgroundNormal().toColor(), x, y, line);
g2d.setClip(previousClip);
}
private boolean isInScrollbarArea(final int mouseX, final int mouseY) { private boolean isInScrollbarArea(final int mouseX, final int mouseY) {
final int scrollbarX = toScreenCoord(getTextAreaWidth() + TEXT_AREA_PADDING); final int scrollbarX = toScreenCoord(CONTENT_MARGIN + getTextAreaWidth() + TEXT_AREA_PADDING);
return mouseX >= scrollbarX; return mouseX >= scrollbarX;
} }
private void handleScrollbarClick(final int mouseY) { private void handleScrollbarClick(final int mouseY) {
final int scaledY = toLogicalCoord(mouseY); final int scrollbarHeight = getVisibleHeight();
final int scaledY = Math.max(0, Math.min(scrollbarHeight, toLogicalCoord(mouseY) - CONTENT_MARGIN));
if (scaledY >= this.scrollbarThumbY && scaledY < this.scrollbarThumbY + this.scrollbarThumbHeight) { if (scaledY >= this.scrollbarThumbY && scaledY < this.scrollbarThumbY + this.scrollbarThumbHeight) {
this.scrollbarDragging = true; this.scrollbarDragging = true;
this.scrollbarDragStartY = scaledY; this.scrollbarDragStartY = scaledY;
@@ -804,7 +842,6 @@ public class CustomMultiLineTextArea extends Component {
final int clickedThumbY = scaledY - this.scrollbarThumbHeight / 2; final int clickedThumbY = scaledY - this.scrollbarThumbHeight / 2;
final int totalContentHeight = getWrappedLines().size() * lineHeight; final int totalContentHeight = getWrappedLines().size() * lineHeight;
final int maxScroll = Math.max(0, totalContentHeight - visibleHeight); final int maxScroll = Math.max(0, totalContentHeight - visibleHeight);
final int scrollbarHeight = getVisibleHeight();
final int maxThumbY = scrollbarHeight - this.scrollbarThumbHeight; final int maxThumbY = scrollbarHeight - this.scrollbarThumbHeight;
if (maxThumbY > 0) { if (maxThumbY > 0) {
@@ -816,13 +853,13 @@ public class CustomMultiLineTextArea extends Component {
} }
private void handleScrollbarDrag(final int mouseY) { private void handleScrollbarDrag(final int mouseY) {
final int scaledY = toLogicalCoord(mouseY); final int scrollbarHeight = getVisibleHeight();
final int scaledY = Math.max(0, Math.min(scrollbarHeight, toLogicalCoord(mouseY) - CONTENT_MARGIN));
final int deltaY = scaledY - this.scrollbarDragStartY; final int deltaY = scaledY - this.scrollbarDragStartY;
final int lineHeight = getLineHeight(); final int lineHeight = getLineHeight();
final int visibleHeight = getVisibleHeight(); final int visibleHeight = getVisibleHeight();
final int totalContentHeight = getWrappedLines().size() * lineHeight; final int totalContentHeight = getWrappedLines().size() * lineHeight;
final int maxScroll = Math.max(0, totalContentHeight - visibleHeight); final int maxScroll = Math.max(0, totalContentHeight - visibleHeight);
final int scrollbarHeight = getVisibleHeight();
final int maxThumbY = scrollbarHeight - this.scrollbarThumbHeight; final int maxThumbY = scrollbarHeight - this.scrollbarThumbHeight;
if (maxThumbY > 0) { if (maxThumbY > 0) {
@@ -979,7 +1016,8 @@ public class CustomMultiLineTextArea extends Component {
g2d.setColor(theme.getBorderInner().toColor()); g2d.setColor(theme.getBorderInner().toColor());
g2d.drawRect(1, 1, this.getWidth() - 2, this.getHeight() - 2); g2d.drawRect(1, 1, this.getWidth() - 2, this.getHeight() - 2);
final int textAreaWidth = toScreenCoord(getTextAreaWidth() + TEXT_AREA_PADDING); final int textAreaX = toScreenCoord(CONTENT_MARGIN);
final int textAreaWidth = toScreenCoord(getTextAreaWidth());
final int visibleHeight = getVisibleHeight(); final int visibleHeight = getVisibleHeight();
final List<String> wrappedLines = getWrappedLines(); final List<String> wrappedLines = getWrappedLines();
@@ -992,7 +1030,7 @@ public class CustomMultiLineTextArea extends Component {
final Color fgColor = this.foregroundColor != null ? this.foregroundColor : theme.getTextNormal().toColor(); final Color fgColor = this.foregroundColor != null ? this.foregroundColor : theme.getTextNormal().toColor();
final var oldClip = g2d.getClip(); final var oldClip = g2d.getClip();
g2d.setClip(0, 0, textAreaWidth, this.getHeight()); g2d.setClip(textAreaX, 0, textAreaWidth, this.getHeight());
final int startLine = Math.max(0, this.scrollOffsetY / lineHeight); final int startLine = Math.max(0, this.scrollOffsetY / lineHeight);
final int endLine = Math.min(wrappedLines.size(), (this.scrollOffsetY + visibleHeight) / lineHeight + 2); final int endLine = Math.min(wrappedLines.size(), (this.scrollOffsetY + visibleHeight) / lineHeight + 2);
@@ -1003,45 +1041,18 @@ public class CustomMultiLineTextArea extends Component {
for (int i = startLine; i < endLine; i++) { for (int i = startLine; i < endLine; i++) {
final String line = wrappedLines.get(i); final String line = wrappedLines.get(i);
final int yPos = i * lineHeight - this.scrollOffsetY; final int yPos = CONTENT_MARGIN + i * lineHeight - this.scrollOffsetY;
if (yPos + lineHeight >= 0 && yPos < visibleHeight) { if (yPos + lineHeight >= CONTENT_MARGIN && yPos < CONTENT_MARGIN + visibleHeight) {
final int charOffset = getCharOffsetForWrappedLine(i, wrappedLines); final int charOffset = getCharOffsetForWrappedLine(i, wrappedLines);
final int lineX = CONTENT_MARGIN;
if (hasSelection && charOffset < selEnd && charOffset + line.length() > selStart) { if (hasSelection && charOffset < selEnd && charOffset + line.length() > selStart) {
final int lineSelStart = Math.max(0, selStart - charOffset); final int lineSelStart = Math.max(0, selStart - charOffset);
final int lineSelEnd = Math.min(line.length(), selEnd - charOffset); final int lineSelEnd = Math.min(line.length(), selEnd - charOffset);
this.renderSelectedLine(g2d, theme, fgColor, lineX, yPos, lineHeight, line, lineSelStart, lineSelEnd);
if (lineSelStart > 0) {
final String beforeSel = line.substring(0, lineSelStart);
final var textDim = G2DDrawFont.getTextDimensions(beforeSel, g2d, this.fontSize, 1.0);
final var textDimScaled = ImmutableCoordI2.create((int) (textDim.getX() * this.scaleFactor), (int) (textDim.getY() * this.scaleFactor));
g2d.setColor(fgColor);
G2DDrawFont.renderText(g2d, textDimScaled, this.scaleFactor, TEXT_PADDING, yPos, this.fontSize, beforeSel);
}
final String selected = line.substring(lineSelStart, lineSelEnd);
final String beforeSelText = lineSelStart > 0 ? line.substring(0, lineSelStart) : "";
final var beforeSelTextDim = G2DDrawFont.getTextDimensions(beforeSelText, g2d, this.fontSize, this.scaleFactor);
final int selX = beforeSelTextDim.getX() + TEXT_PADDING;
final var selDim = G2DDrawFont.getTextDimensions(selected, g2d, this.fontSize, this.scaleFactor);
g2d.setColor(theme.getSelectionPrimary().toColor());
g2d.fillRect(selX, (int) (yPos * this.scaleFactor), selDim.getX(), (int) (lineHeight * this.scaleFactor));
g2d.setColor(theme.getBackgroundNormal().toColor());
G2DDrawFont.renderText(g2d, null, this.scaleFactor, (int) (selX / this.scaleFactor), yPos, this.fontSize, selected);
if (lineSelEnd < line.length()) {
final String afterSel = line.substring(lineSelEnd);
g2d.setColor(fgColor);
G2DDrawFont.renderText(g2d, null, this.scaleFactor, (int) ((selX + selDim.getX()) / this.scaleFactor), yPos, this.fontSize, afterSel);
}
} }
else { else {
final var textDim = G2DDrawFont.getTextDimensions(line, g2d, this.fontSize, 1.0); renderTextSegment(g2d, fgColor, lineX, yPos, line);
final var textDimScaled = ImmutableCoordI2.create((int) (textDim.getX() * this.scaleFactor), (int) (textDim.getY() * this.scaleFactor));
g2d.setColor(fgColor);
G2DDrawFont.renderText(g2d, textDimScaled, this.scaleFactor, TEXT_PADDING, yPos, this.fontSize, line);
} }
} }
} }
@@ -1064,11 +1075,11 @@ public class CustomMultiLineTextArea extends Component {
: ""; : "";
final var textDim = beforeCaret.isEmpty() ? ImmutableCoordI2.create(0, 0) : G2DDrawFont.getTextDimensions(beforeCaret, g2d, this.fontSize, this.scaleFactor); 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 spaceWidth = beforeCaret.endsWith(" ") ? getSpaceWidth() : 0;
final int caretX = textDim.getX() + TEXT_PADDING + (int) (spaceWidth * this.scaleFactor); final int caretX = textDim.getX() + toScreenCoord(CONTENT_MARGIN) + (int) (spaceWidth * this.scaleFactor);
final int caretY = (int) ((caretWrappedLineIndex * lineHeight - this.scrollOffsetY) * this.scaleFactor); final int caretY = (int) ((CONTENT_MARGIN + caretWrappedLineIndex * lineHeight - this.scrollOffsetY) * this.scaleFactor);
final int caretHeight = (int) (rawFontHeight * this.scaleFactor); final int caretHeight = (int) (rawFontHeight * this.scaleFactor);
if (caretY >= 0 && caretY < this.getHeight()) { if (caretY >= toScreenCoord(CONTENT_MARGIN) && caretY < toScreenCoord(CONTENT_MARGIN + visibleHeight)) {
g2d.setColor((currentTime % 1000 < 500) ? theme.getSelectionPrimary().toColor() : theme.getSelectionSecondary().toColor()); g2d.setColor((currentTime % 1000 < 500) ? theme.getSelectionPrimary().toColor() : theme.getSelectionSecondary().toColor());
g2d.fillRect(caretX, caretY, 2, caretHeight); g2d.fillRect(caretX, caretY, 2, caretHeight);
} }
@@ -1084,11 +1095,12 @@ public class CustomMultiLineTextArea extends Component {
} }
private void drawScrollbar(final Graphics2D g2d, final Theme theme, final int visibleHeight, final int totalContentHeight) { private void drawScrollbar(final Graphics2D g2d, final Theme theme, final int visibleHeight, final int totalContentHeight) {
final int scrollbarX = toLogicalCoord(this.getWidth()) - SCROLLBAR_WIDTH; final int scrollbarX = CONTENT_MARGIN + getTextAreaWidth() + TEXT_AREA_PADDING;
final int scrollbarY = CONTENT_MARGIN;
final int scrollbarHeight = getVisibleHeight(); final int scrollbarHeight = getVisibleHeight();
g2d.setColor(theme.getBackgroundNormal().toColor().darker()); g2d.setColor(theme.getBackgroundNormal().toColor().darker());
g2d.fillRect(toScreenCoord(scrollbarX), 0, toScreenCoord(SCROLLBAR_WIDTH), this.getHeight()); g2d.fillRect(toScreenCoord(scrollbarX), toScreenCoord(scrollbarY), toScreenCoord(SCROLLBAR_WIDTH), toScreenCoord(scrollbarHeight));
final double visibleRatio = Math.min(1.0, (double) visibleHeight / totalContentHeight); final double visibleRatio = Math.min(1.0, (double) visibleHeight / totalContentHeight);
this.scrollbarThumbHeight = Math.max(SCROLLBAR_MIN_THUMB_HEIGHT, (int) (scrollbarHeight * visibleRatio)); this.scrollbarThumbHeight = Math.max(SCROLLBAR_MIN_THUMB_HEIGHT, (int) (scrollbarHeight * visibleRatio));
@@ -1103,7 +1115,7 @@ public class CustomMultiLineTextArea extends Component {
} }
g2d.setColor(theme.getSelectionPrimary().toColor()); g2d.setColor(theme.getSelectionPrimary().toColor());
g2d.fillRect(toScreenCoord(scrollbarX + 2), toScreenCoord(this.scrollbarThumbY), g2d.fillRect(toScreenCoord(scrollbarX + 2), toScreenCoord(scrollbarY + this.scrollbarThumbY),
toScreenCoord(SCROLLBAR_WIDTH - 4), toScreenCoord(this.scrollbarThumbHeight)); toScreenCoord(SCROLLBAR_WIDTH - 4), toScreenCoord(this.scrollbarThumbHeight));
} }