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 acf13e760..647621117 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -629,9 +629,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { ((GuiNPCInterface) listener).mouseScroll = wheelDelta; // Intercept wheel scroll for tooltip (highest priority) - if (wheelDelta != 0 && hoverState.isTooltipVisible() && hoverState.isMouseOverTooltip(xMouse, yMouse)) { - hoverState.scrollTooltip(wheelDelta); - wheelDelta = 0; + // Only consume the wheel event when the tooltip actually has content to scroll; + // otherwise let it fall through to the editor so non-scrollable tooltips don't eat input. + if (wheelDelta != 0 && hoverState.isTooltipVisible()) { + if (hoverState.isMouseOverTooltip(xMouse, yMouse)) { + hoverState.scrollTooltip(wheelDelta); + wheelDelta = 0; + } else { + hoverState.clearHover(); + } } // Let autocomplete menu consume scroll first if visible diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java index 2aaf8eed4..af018e5bb 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java @@ -19,7 +19,7 @@ import noppes.npcs.client.gui.util.script.interpreter.token.Token; public class HoverState { /** Minimum hover time (ms) before showing tooltip */ - private static final long HOVER_DELAY_MS = 100; + private static final long HOVER_DELAY_MS = 250; // ==================== STATE ==================== @@ -89,6 +89,11 @@ public class HoverState { /** Whether click-to-pin behaviour is enabled for this hover state. */ private boolean clickToPinEnabled = true; + /** Locked mouse position for tooltip placement (captured once when tooltip becomes visible) */ + private int lockedTooltipMouseX; + private int lockedTooltipMouseY; + private boolean isTooltipPositionLocked; + // ==================== UPDATE ==================== /** @@ -172,6 +177,9 @@ public class HoverState { if (elapsed >= HOVER_DELAY_MS && !tooltipVisible) { tooltipVisible = true; hoverInfo = TokenHoverInfo.fromToken(token); + lockedTooltipMouseX = lastMouseX; + lockedTooltipMouseY = lastMouseY; + isTooltipPositionLocked = true; } } } @@ -223,6 +231,9 @@ public class HoverState { if (elapsed >= HOVER_DELAY_MS && !tooltipVisible) { tooltipVisible = true; hoverInfo = TokenHoverInfo.fromToken(token); + lockedTooltipMouseX = lastMouseX; + lockedTooltipMouseY = lastMouseY; + isTooltipPositionLocked = true; } } } @@ -249,6 +260,7 @@ public class HoverState { targetScrollOffset = 0; tooltipMaxScroll = 0; isDraggingScrollbar = false; + isTooltipPositionLocked = false; } } } @@ -284,6 +296,11 @@ public class HoverState { this.tokenWidth = tokenW; // ensure hoveredToken reflects pinned token this.hoveredToken = token; + if (this.tooltipVisible && !this.isTooltipPositionLocked) { + this.lockedTooltipMouseX = lastMouseX; + this.lockedTooltipMouseY = lastMouseY; + this.isTooltipPositionLocked = true; + } } /** @@ -294,10 +311,15 @@ public class HoverState { this.pinnedHoverInfo = null; this.tooltipVisible = false; this.hoverInfo = null; + this.isTooltipPositionLocked = false; } public boolean isPinned() { return pinnedToken != null; } + public int getLockedMouseX() { return lockedTooltipMouseX; } + public int getLockedMouseY() { return lockedTooltipMouseY; } + public boolean isPositionLocked() { return isTooltipPositionLocked; } + // ==================== POSITION CALCULATION ==================== /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index fb3567223..567f0a8f7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -17,7 +17,6 @@ import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.token.Token; import noppes.npcs.client.gui.util.script.interpreter.token.TokenErrorMessage; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; -import noppes.npcs.client.gui.util.script.interpreter.type.IntersectionTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; @@ -186,7 +185,6 @@ public class TokenHoverInfo { break; case LITERAL: case KEYWORD: - case MODIFIER: case STRING: case COMMENT: if (info.hasErrors()) @@ -443,7 +441,7 @@ public class TokenHoverInfo { if (token.getMethodInfo() != null) { MethodInfo constructor = token.getMethodInfo(); declaration.add(new TextSegment("\n", TokenType.DEFAULT.getHexColor())); - additionalInfo.add("Constructor"); + // additionalInfo.add("Constructor"); buildConstructorDeclaration(constructor, typeInfo); } } @@ -458,7 +456,7 @@ public class TokenHoverInfo { TypeInfo bound = typeInfo.getBoundType(); if (bound != null && bound.getJavaClass() != null && bound.getJavaClass() != Object.class) { - addSegment(" extends ", TokenType.MODIFIER.getHexColor()); + addSegment(" extends ", TokenType.KEYWORD.getHexColor()); addSegment(bound.getSimpleName(), getColorForTypeInfo(bound)); } addSegment(">", TokenType.DEFAULT.getHexColor()); @@ -674,7 +672,7 @@ public class TokenHoverInfo { addInitializationTokens(token, fieldInfo); // Show it's a local variable - additionalInfo.add("Local variable"); + // additionalInfo.add("Local variable"); } public String getPackageName(TypeInfo type) { @@ -723,7 +721,7 @@ public class TokenHoverInfo { addSegment(fieldInfo.getName(), TokenType.PARAMETER.getHexColor()); - additionalInfo.add("Parameter"); + // additionalInfo.add("Parameter"); } private void extractUndefinedInfo(Token token) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java index aed5d4391..ec9fae79a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java @@ -39,7 +39,7 @@ public class TokenHoverRenderer { private static final int SEPARATOR_SPACING = 5; /** Vertical offset from token */ - private static final int VERTICAL_OFFSET = 4; + private static final int VERTICAL_OFFSET = 1; /** Maximum tooltip width as percentage of viewport */ private static final float MAX_WIDTH_RATIO = 0.9f; @@ -137,9 +137,20 @@ public class TokenHoverRenderer { } } - // Position the tooltip — use dragged position if user has panned the panel - int tooltipX = positionOverridden ? hoverState.getOverriddenTooltipX() : tokenX; - int tooltipY = positionOverridden ? hoverState.getOverriddenTooltipY() : tokenY + lineHeight + VERTICAL_OFFSET; + // Position the tooltip — use dragged position if user has panned the panel, + // otherwise use locked mouse position (captured when tooltip first became visible) + int tooltipX; + int tooltipY; + if (positionOverridden) { + tooltipX = hoverState.getOverriddenTooltipX(); + tooltipY = hoverState.getOverriddenTooltipY(); + } else if (hoverState.isPositionLocked()) { + tooltipX = hoverState.getLockedMouseX() + 2; + tooltipY = tokenY + lineHeight + VERTICAL_OFFSET; + } else { + tooltipX = tokenX; + tooltipY = tokenY + lineHeight + VERTICAL_OFFSET; + } // Clamp position — use screen bounds when user has dragged the panel outside the editor viewport int leftBound = useScreenBounds ? 0 : viewportX; @@ -164,7 +175,7 @@ public class TokenHoverRenderer { if (useScreenBounds) { tooltipY = bottomBound - boxHeight; } else { - tooltipY = tokenY - boxHeight - VERTICAL_OFFSET; // try above token + tooltipY = tokenY - boxHeight; } } if (tooltipY < topBound) { @@ -176,13 +187,20 @@ public class TokenHoverRenderer { // Mouse-over detection bounds: // When dragged, use panel rect only — no gap extension (token is unrelated to panel position) - // When default, union the token bottom and the tooltip rect so both directions are covered + // When default and tooltip is BELOW the token, union the token bottom and the tooltip rect + // so the small gap between token and tooltip doesn't break hover detection. + // When tooltip is ABOVE the token, use only the tooltip rect — bridging would cover + // intervening tokens and prevent hover-switching between them. if (hoverState.hasOverriddenPosition()) { hoverState.setTooltipBounds(tooltipX, tooltipY, boxWidth, boxHeight); - } else { - int boundsTopY = Math.min(tokenY + lineHeight, tooltipY); - int boundsBottomY = Math.max(tooltipY + boxHeight, tokenY + lineHeight); + } else if (tooltipY >= tokenY + lineHeight) { + // Tooltip is below the token — bridge the gap + int boundsTopY = tokenY + lineHeight; + int boundsBottomY = tooltipY + boxHeight; hoverState.setTooltipBounds(tooltipX, boundsTopY, boxWidth, boundsBottomY - boundsTopY); + } else { + // Tooltip is above the token — use tooltip rect only + hoverState.setTooltipBounds(tooltipX, tooltipY, boxWidth, boxHeight); } // Render the tooltip @@ -218,7 +236,7 @@ public class TokenHoverRenderer { int boxHeight = visibleContentHeight + PADDING * 2; // Store scroll metadata into HoverState - hoverState.setTooltipMaxScroll(Math.max(0, totalContentHeight - visibleContentHeight + BOTTOM_GAP)); + hoverState.setTooltipMaxScroll(Math.max(0, totalContentHeight - visibleContentHeight + (hasScrollbar? BOTTOM_GAP : 0))); // Draw background Gui.drawRect(x, y, x + boxWidth, y + boxHeight, BG_COLOR);