From 8a5e1fb0620d0bb4a9230b46407f6aa3c40ae6ba Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 19:07:05 +0200 Subject: [PATCH] Enter/Backspace/Tab/Up/Down now auto scroll to the cursor's position !!! --- .../client/gui/util/GuiScriptTextArea.java | 198 ++++++++++++------ .../gui/util/script/JavaTextContainer.java | 19 +- 2 files changed, 143 insertions(+), 74 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 92a58b042..857df428e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -56,8 +56,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { public List redoList = new ArrayList(); public boolean undoing = false; private long lastClicked = 0L; - // Line number gutter width (left margin for line numbers) - private static int LINE_NUMBER_GUTTER_WIDTH = 25; + // Line number gutter width (left margin for line numbers) - dynamically calculated + private static int LINE_NUMBER_GUTTER_WIDTH = 25; // private static TrueTypeFont font = new TrueTypeFont(new Font("Arial Unicode MS", Font.PLAIN, ConfigClient.FontSize), 1); public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { @@ -104,7 +104,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int scissorH = this.height * scaleFactor; GL11.glScissor(scissorX, scissorY, scissorW, scissorH); - container.visibleLines = height / container.lineHeight; + container.visibleLines = height / container.lineHeight-1; int maxScroll = Math.max(0, this.container.linesCount - container.visibleLines); if (listener instanceof GuiNPCInterface) { @@ -181,6 +181,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { scrollVelocity = 0.0; } } + // Ensure scrollPos stays in valid bounds + if (scrollPos < 0) scrollPos = 0; + if (scrollPos > maxScroll) scrollPos = maxScroll; + // Update integer scrolledLine for compatibility scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); @@ -596,7 +600,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { for (int i = 0; i < list.size(); ++i) { LineData data = (LineData) list.get(i); - if (i >= this.scrolledLine && i < this.scrolledLine + this.container.visibleLines) { + if (i >= this.scrolledLine && i <= this.scrolledLine + this.container.visibleLines) { int yPos = (i - this.scrolledLine) * this.container.lineHeight; if (yMouse >= yPos && yMouse < yPos + this.container.lineHeight) { int lineWidth = 0; @@ -625,55 +629,58 @@ public class GuiScriptTextArea extends GuiNpcTextField { return this.container.text.length(); } - // Ensure the cursor's line is visible by updating the smooth scroll target - // Mimics IntelliJ IDEA behavior: when pressing Enter, maintain the cursor's relative Y position - // in the viewport by scrolling forward to compensate for the newly added line. - private void ensureCursorVisible() { - if (container == null || container.lines == null || container.lines.isEmpty()) return; - - // Find the cursor's current line index - int lineIdx = 0; + // Find which line the cursor is on (0-indexed) + private int getCursorLineIndex() { + if (container == null || container.lines == null || container.lines.isEmpty()) return 0; for (int i = 0; i < container.lines.size(); i++) { LineData ld = container.lines.get(i); - if (cursorPosition >= ld.start && cursorPosition <= ld.end) { - lineIdx = i; - break; + // For last line, use <= since end doesn't include +1 for newline + // For other lines, use < since end includes the newline position + boolean isLastLine = (i == container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (cursorPosition >= ld.start && cursorPosition <= ld.end) + : (cursorPosition >= ld.start && cursorPosition < ld.end); + if (isOnLine) { + return i; } } + return container.lines.size() - 1; + } + + // Check if cursor is on a specific line, handling the last line specially + // (last line doesn't have trailing newline, so use <= for end boundary) + private boolean isCursorOnLine(int lineIndex, LineData line) { + boolean isLastLine = (lineIndex == container.lines.size() - 1); + return isLastLine + ? (cursorPosition >= line.start && cursorPosition <= line.end) + : (cursorPosition >= line.start && cursorPosition < line.end); + } + + // Scroll viewport to keep cursor visible (minimal adjustment, like IntelliJ) + // Only scrolls if cursor is outside the visible area + private void scrollToCursor() { + if (container == null || container.lines == null || container.lines.isEmpty()) return; + int lineIdx = getCursorLineIndex(); int visible = Math.max(1, container.visibleLines); int maxScroll = Math.max(0, container.linesCount - visible); - // Calculate the cursor line's relative position in the current viewport (0 = top, visible-1 = bottom) - double relativePos = lineIdx - scrollPos; + int firstVisible = scrolledLine; + int lastFullyVisible = scrolledLine + visible; - // If cursor is already visible in the viewport - if (relativePos >= 0 && relativePos < visible) { - // When Enter is pressed, a new line is inserted and cursor advances by 1. - // To maintain the same visual Y position, scroll forward by 1 line. - // This keeps the cursor at the same relative position while content scrolls up. - double newScroll = scrollPos + 1.0; - newScroll = Math.max(0, Math.min(newScroll, maxScroll)); - - targetScroll = newScroll; - scrollPos = newScroll; - scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); - scrollVelocity = 0.0; - } else if (relativePos < 0) { - // Cursor moved above the viewport; scroll up to show it near the top - double newScroll = Math.max(0, lineIdx - 2); // keep 2 lines above for context - targetScroll = newScroll; - scrollPos = newScroll; - scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); - scrollVelocity = 0.0; - } else { - // Cursor is below viewport; scroll down to show it - double newScroll = Math.max(0, Math.min(lineIdx - visible + 3, maxScroll)); - targetScroll = newScroll; - scrollPos = newScroll; - scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); - scrollVelocity = 0.0; + if (lineIdx < firstVisible) { + // Cursor is above viewport - scroll up just enough to show it + targetScroll = lineIdx; + } else if (lineIdx > lastFullyVisible) { + // Cursor is below viewport - scroll down just enough to show it + targetScroll = Math.min(lineIdx - visible, maxScroll); } + // If cursor is visible, don't change scroll + } + + // Legacy method name for compatibility + private void ensureCursorVisible() { + scrollToCursor(); } @Override @@ -727,10 +734,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (i == Keyboard.KEY_UP) { setCursor(cursorUp(), GuiScreen.isShiftKeyDown()); + scrollToCursor(); return true; } if (i == Keyboard.KEY_DOWN) { setCursor(cursorDown(), GuiScreen.isShiftKeyDown()); + scrollToCursor(); return true; } if (i == Keyboard.KEY_DELETE) { @@ -787,6 +796,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { String s = getSelectionBeforeText(); setText(s + getSelectionAfterText()); endSelection = cursorPosition = startSelection; + scrollToCursor(); return true; } @@ -797,8 +807,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Find current line for indent-aware backspace LineData curr = null; - for (LineData line : container.lines) { - if (cursorPosition >= line.start && cursorPosition < line.end) { + for (int idx = 0; idx < container.lines.size(); idx++) { + LineData line = container.lines.get(idx); + boolean isLastLine = (idx == container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (cursorPosition >= line.start && cursorPosition <= line.end) + : (cursorPosition >= line.start && cursorPosition < line.end); + if (isOnLine) { curr = line; break; } @@ -830,6 +845,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Cursor goes to end of previous line (at the newline position, which is now end of prev line content) int newCursor = Math.max(0, curr.start - 1); endSelection = cursorPosition = startSelection = newCursor; + scrollToCursor(); return true; } else { // Line has content: merge with previous line @@ -857,6 +873,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(before + spacer + content); int newCursor = before.length() + spacer.length(); endSelection = cursorPosition = startSelection = newCursor; + scrollToCursor(); return true; } } @@ -876,6 +893,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(before + after); startSelection -= 1; endSelection = cursorPosition = startSelection; + scrollToCursor(); return true; } } @@ -886,6 +904,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { startSelection--; setText(s + getSelectionAfterText()); endSelection = cursorPosition = startSelection; + scrollToCursor(); return true; } if (this.isKeyComboCtrlX(i)) { @@ -894,7 +913,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { String s = getSelectionBeforeText(); setText(s + getSelectionAfterText()); endSelection = startSelection = cursorPosition = s.length(); - + scrollToCursor(); } return true; } @@ -906,6 +925,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (this.isKeyComboCtrlV(i)) { addText(NoppesStringUtils.getClipboardContents()); + scrollToCursor(); return true; } if (i == Keyboard.KEY_Z && isCtrlKeyDown()) { @@ -917,6 +937,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(data.text); endSelection = startSelection = cursorPosition = data.cursorPosition; undoing = false; + scrollToCursor(); return true; } if (i == Keyboard.KEY_Y && isCtrlKeyDown()) { @@ -928,6 +949,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(data.text); endSelection = startSelection = cursorPosition = data.cursorPosition; undoing = false; + scrollToCursor(); return true; } if (i == Keyboard.KEY_TAB) { @@ -937,6 +959,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { } else { handleTab(); } + scrollToCursor(); + return true; } if (i == Keyboard.KEY_F && isCtrlKeyDown()) { formatText(); @@ -956,7 +980,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { String leadingSegment = firstNewline == -1 ? after : after.substring(0, firstNewline); if (leadingSegment.trim().length() > 0) { addText("\n" + childIndent); - ensureCursorVisible(); + scrollToCursor(); return true; } @@ -999,17 +1023,17 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (hasMatchingCloseSameIndent) { addText("\n" + childIndent); - ensureCursorVisible(); + scrollToCursor(); } else { String insert = "\n" + childIndent + "\n" + indent + "}"; setText(before + insert + after); int newCursor = before.length() + 1 + childIndent.length(); startSelection = endSelection = cursorPosition = newCursor; - ensureCursorVisible(); + scrollToCursor(); } } else { addText(Character.toString('\n') + getAutoIndentForEnter()); - ensureCursorVisible(); + scrollToCursor(); } } if (ChatAllowedCharacters.isAllowedCharacter(c)) { @@ -1020,31 +1044,37 @@ public class GuiScriptTextArea extends GuiNpcTextField { if ((c == ')' || c == ']' || c == '"' || c == '\'' ) && after.length() > 0 && after.charAt(0) == c) { // move cursor forward over existing closer startSelection = endSelection = cursorPosition = before.length() + 1; + scrollToCursor(); return true; } if (c == '"') { setText(before + "\"\"" + after); startSelection = endSelection = cursorPosition = before.length() + 1; + scrollToCursor(); return true; } if (c == '\'') { setText(before + "''" + after); startSelection = endSelection = cursorPosition = before.length() + 1; + scrollToCursor(); return true; } if (c == '[') { setText(before + "[]" + after); startSelection = endSelection = cursorPosition = before.length() + 1; + scrollToCursor(); return true; } if (c == '(') { setText(before + "()" + after); startSelection = endSelection = cursorPosition = before.length() + 1; + scrollToCursor(); return true; } addText(Character.toString(c)); + scrollToCursor(); } if (i == Keyboard.KEY_SLASH && isCtrlKeyDown()) { @@ -1483,8 +1513,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void handleTab() { LineData currentLine = null; - for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { + for (int i = 0; i < this.container.lines.size(); i++) { + LineData line = this.container.lines.get(i); + boolean isLastLine = (i == this.container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (this.cursorPosition >= line.start && this.cursorPosition <= line.end) + : (this.cursorPosition >= line.start && this.cursorPosition < line.end); + if (isOnLine) { currentLine = line; break; } @@ -1542,8 +1577,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void handleShiftTab() { LineData currentLine = null; - for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { + for (int i = 0; i < this.container.lines.size(); i++) { + LineData line = this.container.lines.get(i); + boolean isLastLine = (i == this.container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (this.cursorPosition >= line.start && this.cursorPosition <= line.end) + : (this.cursorPosition >= line.start && this.cursorPosition < line.end); + if (isOnLine) { currentLine = line; break; } @@ -1626,16 +1666,24 @@ public class GuiScriptTextArea extends GuiNpcTextField { private int cursorUp() { for (int i = 0; i < this.container.lines.size(); ++i) { LineData data = (LineData) this.container.lines.get(i); - if (this.cursorPosition >= data.start && this.cursorPosition < data.end) { + // For last line, use <= since end doesn't include +1 for newline + // For other lines, use < since end includes the newline position + boolean isLastLine = (i == this.container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (this.cursorPosition >= data.start && this.cursorPosition <= data.end) + : (this.cursorPosition >= data.start && this.cursorPosition < data.end); + if (isOnLine) { if (i == 0) { return 0; } - int column = this.cursorPosition - data.start; + int column = Math.min(this.cursorPosition - data.start, data.text.length()); LineData target = this.container.lines.get(i - 1); - int targetPos = this.getSelectionPos(this.x + LINE_NUMBER_GUTTER_WIDTH + 1 + ClientProxy.Font.width(data.text.substring(0, column)), this.y + 1 + (i - 1 - this.scrolledLine) * this.container.lineHeight); + // Calculate target position on the line above, maintaining column if possible + int targetColumn = Math.min(column, target.text.length()); int targetIndent = getLineIndent(target.text); - int minPos = target.start + Math.min(targetIndent, Math.max(0, target.text.length())); + int minPos = target.start + Math.min(targetIndent, target.text.length()); + int targetPos = target.start + targetColumn; return Math.max(minPos, targetPos); } } @@ -1648,12 +1696,25 @@ public class GuiScriptTextArea extends GuiNpcTextField { return cursorPosition; for (int i = 0; i < this.container.lines.size(); ++i) { LineData data = (LineData) this.container.lines.get(i); - if (this.cursorPosition >= data.start && this.cursorPosition < data.end) { - int column = this.cursorPosition - data.start; - LineData target = this.container.lines.get(Math.min(i + 1, this.container.lines.size() - 1)); - int targetPos = this.getSelectionPos(this.x + LINE_NUMBER_GUTTER_WIDTH + 1 + ClientProxy.Font.width(data.text.substring(0, column)), this.y + 1 + (i + 1 - this.scrolledLine) * this.container.lineHeight); + // For last line, use <= since end doesn't include +1 for newline + // For other lines, use < since end includes the newline position + boolean isLastLine = (i == this.container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (this.cursorPosition >= data.start && this.cursorPosition <= data.end) + : (this.cursorPosition >= data.start && this.cursorPosition < data.end); + if (isOnLine) { + // If already on last line, stay at current position + if (i >= this.container.lines.size() - 1) { + return cursorPosition; + } + + int column = Math.min(this.cursorPosition - data.start, data.text.length()); + LineData target = this.container.lines.get(i + 1); + // Calculate target position on the line below, maintaining column if possible + int targetColumn = Math.min(column, target.text.length()); int targetIndent = getLineIndent(target.text); - int minPos = target.start + Math.min(targetIndent, Math.max(0, target.text.length())); + int minPos = target.start + Math.min(targetIndent, target.text.length()); + int targetPos = target.start + targetColumn; return Math.max(minPos, targetPos); } } @@ -1750,14 +1811,17 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.container.formatCodeText(); } - // Ensure scrolledLine stays in bounds and snap smooth scroll to avoid perceived lag + // Ensure scroll state stays in bounds after text change int maxScroll = Math.max(0, this.container.linesCount - this.container.visibleLines); if (this.scrolledLine > maxScroll) { this.scrolledLine = maxScroll; } - // Immediately update fractional scroll targets so new content appears without delay - this.targetScroll = this.scrolledLine; - this.scrollPos = this.scrolledLine; + if (this.targetScroll > maxScroll) { + this.targetScroll = maxScroll; + } + if (this.scrollPos > maxScroll) { + this.scrollPos = maxScroll; + } clampSelectionBounds(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index b0bed6ea9..b225766f9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -144,13 +144,14 @@ public class JavaTextContainer extends TextContainer { } public void init(int width, int height) { - lineHeight = ClientProxy.Font.height(); - if (lineHeight == 0) - lineHeight = 12; + // lineHeight = ClientProxy.Font.height(); + // if (lineHeight == 0) + lineHeight = 13; String[] split = text.split("\\n", -1); // -1 preserves empty lines "" int totalChars = 0; - for (String l : split) { + for (int lineIndex = 0; lineIndex < split.length; lineIndex++) { + String l = split[lineIndex]; StringBuilder line = new StringBuilder(); Matcher m = regexWord.matcher(l); int i = 0; @@ -164,13 +165,17 @@ public class JavaTextContainer extends TextContainer { line.append(word); i = m.start(); } - lines.add(new LineData(line.toString(), totalChars, totalChars + line.length() + 1)); + // Only add +1 for the newline if this is NOT the last line + // The last line has no newline after it + boolean isLastLine = (lineIndex == split.length - 1); + int endOffset = isLastLine ? 0 : 1; + lines.add(new LineData(line.toString(), totalChars, totalChars + line.length() + endOffset)); - totalChars += line.length() + 1; + totalChars += line.length() + endOffset; } linesCount = lines.size(); totalHeight = linesCount * lineHeight; - visibleLines = Math.max(height / lineHeight, 1); + visibleLines = Math.max(height / lineHeight-1, 1); // Invalidate caches that depend on line layout invalidateCaches();