From 0caebb08ca5389556f9842f9e7b2a9e4cd35bfd8 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 19:19:06 +0200 Subject: [PATCH] Vastly improved GutterIconRenderer --- .../client/gui/util/GuiScriptTextArea.java | 153 +-------- .../interpreter/hover/GutterIconRenderer.java | 304 ++++++++++++++++++ .../script/interpreter/token/TokenType.java | 21 ++ 3 files changed, 342 insertions(+), 136 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/GutterIconRenderer.java 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 74eb1a5c7..c45c0b817 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -17,6 +17,7 @@ import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; 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.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.hover.GutterIconRenderer; import noppes.npcs.client.gui.util.script.interpreter.hover.HoverState; import noppes.npcs.client.gui.util.script.interpreter.hover.TokenHoverRenderer; import noppes.npcs.client.key.impl.ScriptEditorKeys; @@ -98,15 +99,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { private static int LINE_NUMBER_GUTTER_WIDTH = 25; // ==================== GUTTER ICONS ==================== - /** Icon width for override/implements icons in the gutter (scaled from 32x32 to fit line height) */ - private static final int GUTTER_ICON_SIZE = 10; - /** Extra gutter width to accommodate inheritance icons */ - private static final int ICON_GUTTER_WIDTH = 12; - /** Texture resource for script icons (64x32: first 32x32 = override, second 32x32 = implements) */ - private static final ResourceLocation SCRIPT_ICONS = new ResourceLocation("customnpcs", "textures/gui/script/icons.png"); /** Hover state for gutter icons - tracks which icon the mouse is over */ private MethodInfo hoveredGutterMethod = null; - private int hoveredGutterLine = -1; // ==================== UNDO/REDO ==================== public List undoList = new ArrayList<>(); @@ -483,7 +477,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int maxLineNum = container.linesCount; String maxLineStr = String.valueOf(maxLineNum); int digitWidth = ClientProxy.Font.width(maxLineStr); - LINE_NUMBER_GUTTER_WIDTH = digitWidth + 10 + ICON_GUTTER_WIDTH; // 10px padding + icon space + LINE_NUMBER_GUTTER_WIDTH = digitWidth + 10 + GutterIconRenderer.ICON_GUTTER_WIDTH; // 10px padding + icon space } // Draw outer border around entire area int offset = fullscreen() ? 2 : 1; @@ -651,7 +645,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; String lineNum = String.valueOf(i + 1); int lineNumWidth = ClientProxy.Font.width(lineNum); - int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5 - ICON_GUTTER_WIDTH; // right-align before icon space + int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5 - GutterIconRenderer.ICON_GUTTER_WIDTH; // right-align before icon space int lineNumY = posY + 1; // Highlight current line number int lineNumColor = 0xFF606366; @@ -878,10 +872,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } - // Render GUTTER ICONS for method override/implements - hoveredGutterMethod = null; - hoveredGutterLine = -1; - renderGutterIcons(renderStart, renderEnd, stringYOffset, xMouse, yMouse, fracPixels); + // Render gutter icons for method override/implements + if (container != null && container.getDocument() != null) { + hoveredGutterMethod = GutterIconRenderer.renderIcons(container.lineHeight, + x + LINE_NUMBER_GUTTER_WIDTH - GutterIconRenderer.ICON_GUTTER_WIDTH + 1, y, renderStart, renderEnd, + scroll.getScrolledLine(), stringYOffset, container.getDocument().getAllMethods(), container.lines, + xMouse, yMouse, fracPixels); + } GL11.glPopMatrix(); GL11.glDisable(GL11.GL_SCISSOR_TEST); @@ -905,8 +902,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Draw go to line dialog (overlays everything) goToLineDialog.draw(xMouse, yMouse); KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); - - // Draw hover tooltip (on top of everything) + + // Draw hover tooltips (on top of everything) if (hoverState.isTooltipVisible()) { int xOffset = hasVerticalScrollbar() ? -8 : -2; int viewportWidth = width - LINE_NUMBER_GUTTER_WIDTH; @@ -914,6 +911,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { int viewportHeight = height; TokenHoverRenderer.render(hoverState, viewportX, viewportWidth+xOffset, viewportY, viewportHeight); } + + // Draw gutter icon tooltip + if (hoveredGutterMethod != null) { + GutterIconRenderer.renderTooltip(hoveredGutterMethod, xMouse, yMouse, x, width, y, height); + } } private void scissorViewport() { @@ -927,127 +929,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { GL11.glScissor(scissorX, scissorY, scissorW, scissorH); } - // ==================== GUTTER ICON RENDERING ==================== - - /** - * Render override/implements icons in the gutter for methods with inheritance markers. - * Icons are rendered at the start of lines where method declarations with inheritance exist. - */ - private void renderGutterIcons(int renderStart, int renderEnd, int stringYOffset, int xMouse, int yMouse, float fracPixels) { - if (container == null || container.getDocument() == null) return; - - // Icon position in the gutter (right side of gutter, before line numbers) - int iconX = x + LINE_NUMBER_GUTTER_WIDTH - ICON_GUTTER_WIDTH + 1; - - // Adjust mouse Y for fractional scroll - float adjustedMouseY = yMouse + fracPixels; - - for (int lineIndex = renderStart; lineIndex <= renderEnd; lineIndex++) { - // Get method info for this line if it has an inheritance marker - MethodInfo method = getMethodWithInheritanceMarkerAtLine(lineIndex); - if (method == null) continue; - - int posY = y + (lineIndex - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; - int iconY = posY + (container.lineHeight - GUTTER_ICON_SIZE) / 2 - 1; - - // Determine which icon to draw (override = 0, implements = 1) - boolean isOverride = method.isOverride(); - int iconU = isOverride ? 0 : 32; // U offset in texture (0 for override, 32 for implements) - - // Bind the icons texture - Minecraft.getMinecraft().renderEngine.bindTexture(SCRIPT_ICONS); - - // Draw the icon scaled from 32x32 to GUTTER_ICON_SIZE - GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); - GL11.glPushMatrix(); - float scale = 2; - GL11.glScalef(scale,scale,scale); - GL11.glTranslatef(-4,-3.25F,0); - GuiUtil.drawScaledTexturedRect((int) (iconX/scale), (int) (iconY/scale), iconU, 0, 32, 32, GUTTER_ICON_SIZE, GUTTER_ICON_SIZE, 64, 32); - GL11.glPopMatrix(); - // Check if mouse is hovering over this icon (for tooltip) - int screenPosY = y + (lineIndex - scroll.getScrolledLine()) * container.lineHeight; - if (xMouse >= iconX && xMouse < iconX + GUTTER_ICON_SIZE && - adjustedMouseY >= screenPosY && adjustedMouseY < screenPosY + container.lineHeight) { - hoveredGutterMethod = method; - hoveredGutterLine = lineIndex; - } - } - - // Render gutter icon tooltip if hovering - renderGutterIconTooltip(xMouse, yMouse); - } - - /** - * Get a method with an inheritance marker (override or implements) that starts on the given line. - */ - private MethodInfo getMethodWithInheritanceMarkerAtLine(int lineIndex) { - if (container == null || container.getDocument() == null) return null; - if (lineIndex < 0 || lineIndex >= container.lines.size()) return null; - - LineData lineData = container.lines.get(lineIndex); - int lineStart = lineData.start; - int lineEnd = lineData.end; - - // Check all methods in the document - for (MethodInfo method : container.getDocument().getAllMethods()) { - if (!method.hasInheritanceMarker()) continue; - - // Check if the method's name offset is on this line - int nameOffset = method.getNameOffset(); - if (nameOffset >= lineStart && nameOffset < lineEnd) { - return method; - } - } - - return null; - } - - /** - * Render tooltip for the currently hovered gutter icon. - */ - private void renderGutterIconTooltip(int xMouse, int yMouse) { - if (hoveredGutterMethod == null) return; - - // Build tooltip text - String tooltipText; - if (hoveredGutterMethod.isOverride()) { - String parentName = hoveredGutterMethod.getOverridesFrom() != null - ? hoveredGutterMethod.getOverridesFrom().getSimpleName() - : "parent class"; - tooltipText = "Overrides method in " + parentName; - } else { - String ifaceName = hoveredGutterMethod.getImplementsFrom() != null - ? hoveredGutterMethod.getImplementsFrom().getSimpleName() - : "interface"; - tooltipText = "Implements method from " + ifaceName; - } - - // Calculate tooltip position (to the right of the icon) - int tooltipX = xMouse + 10; - int tooltipY = yMouse - 5; - - // Measure text width - int textWidth = ClientProxy.Font.width(tooltipText); - int padding = 4; - - // Draw tooltip background - drawRect(tooltipX - padding, tooltipY - padding, - tooltipX + textWidth + padding, tooltipY + ClientProxy.Font.height() + padding, - 0xE0000000); - - // Draw tooltip border - drawRect(tooltipX - padding, tooltipY - padding, - tooltipX + textWidth + padding, tooltipY - padding + 1, - 0xFF505050); - drawRect(tooltipX - padding, tooltipY + ClientProxy.Font.height() + padding - 1, - tooltipX + textWidth + padding, tooltipY + ClientProxy.Font.height() + padding, - 0xFF505050); - - // Draw tooltip text - ClientProxy.Font.drawString(tooltipText, tooltipX, tooltipY, 0xFFFFFFFF); - } - // ==================== SELECTION & CURSOR POSITION ==================== // Get cursor position from mouse coordinates diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/GutterIconRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/GutterIconRenderer.java new file mode 100644 index 000000000..3c5087533 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/GutterIconRenderer.java @@ -0,0 +1,304 @@ +package noppes.npcs.client.gui.util.script.interpreter.hover; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.util.GuiUtil; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +/** + * Renders gutter icons for method inheritance (override/implements) in the script editor. + * + * Renders IntelliJ-style tooltips when hovering over gutter icons: + * - Background + border matching TokenHoverRenderer style + * - Colored declaration showing which class/interface the method overrides/implements + * - Uses the same token highlighting system for type names + */ +public class GutterIconRenderer { + + // ==================== CONSTANTS ==================== + + /** Texture resource for script icons (64x32: first 32x32 = override, second 32x32 = implements) */ + private static final ResourceLocation SCRIPT_ICONS = new ResourceLocation("customnpcs", + "textures/gui/script/icons.png"); + + /** Icon size when rendered in the gutter (scaled from 32x32) */ + private static final int GUTTER_ICON_SIZE = 10; + + /** Extra gutter width to accommodate inheritance icons */ + public static final int ICON_GUTTER_WIDTH = 12; + + /** Padding inside the tooltip box */ + private static final int PADDING = 6; + + /** Line spacing between rows */ + private static final int LINE_SPACING = 2; + + /** Vertical offset from mouse */ + private static final int VERTICAL_OFFSET = 10; + + // ==================== COLORS ==================== + + /** Background color (dark gray like IntelliJ) */ + private static final int BG_COLOR = 0xF0313335; + + /** Border color */ + private static final int BORDER_COLOR = 0xFF3C3F41; + + /** Info text color */ + private static final int INFO_COLOR = 0xFFA9B7C6; + + // ==================== RENDERING ==================== + + /** + * Render gutter icons for a range of lines. + * + * @param lineHeight Height of each line + * @param gutterX X position for the icon in the gutter + * @param gutterY Base Y position of the gutter + * @param renderStart First line index to render + * @param renderEnd Last line index to render + * @param scrolledLine Current scroll position + * @param stringYOffset Y offset for text positioning + * @param methods All methods in the document + * @param lines Line data for position calculation + * @param xMouse Mouse X position + * @param yMouse Mouse Y position + * @param fracPixels Fractional scroll offset + * @return The hovered method, or null if no icon is hovered + */ + public static MethodInfo renderIcons( + int lineHeight, + int gutterX, + int gutterY, + int renderStart, + int renderEnd, + int scrolledLine, + int stringYOffset, + List methods, + List lines, + int xMouse, + int yMouse, + float fracPixels) { + + if (methods == null || methods.isEmpty()) + return null; + + MethodInfo hoveredMethod = null; + float adjustedMouseY = yMouse + fracPixels; + + for (int lineIndex = renderStart; lineIndex <= renderEnd; lineIndex++) { + MethodInfo method = getMethodAtLine(lineIndex, methods, lines); + if (method == null || !method.hasInheritanceMarker()) + continue; + + int posY = gutterY + (lineIndex - scrolledLine) * lineHeight + stringYOffset; + int iconY = posY + (lineHeight - GUTTER_ICON_SIZE) / 2 - 1; + + // Draw the icon + renderIcon(gutterX, iconY, method.isOverride()); + + // Check for hover + int iconScaleOffsetX = -4; + int screenPosY = gutterY + (lineIndex - scrolledLine) * lineHeight; + if (xMouse >= gutterX + iconScaleOffsetX && xMouse < gutterX + GUTTER_ICON_SIZE + iconScaleOffsetX && + adjustedMouseY >= screenPosY && adjustedMouseY < screenPosY + lineHeight) { + hoveredMethod = method; + } + } + + return hoveredMethod; + } + + /** + * Render a single gutter icon. + */ + private static void renderIcon(int x, int y, boolean isOverride) { + int iconU = isOverride ? 0 : 32; + + Minecraft.getMinecraft().renderEngine.bindTexture(SCRIPT_ICONS); + GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + + GL11.glPushMatrix(); + float scale = 2.0f, scaleOffsetX = -4, scaleOffsetY = -3.25f; + GL11.glScalef(scale, scale, scale); + GL11.glTranslatef(scaleOffsetX, scaleOffsetY, 0); + GuiUtil.drawScaledTexturedRect( + (int) (x / scale), (int) (y / scale), + iconU, 0, 32, 32, + GUTTER_ICON_SIZE, GUTTER_ICON_SIZE, + 64, 32 + ); + GL11.glPopMatrix(); + } + + /** + * Render the tooltip for a hovered gutter icon. + */ + public static void renderTooltip(MethodInfo method, int mouseX, int mouseY, int viewportX, int viewportWidth, + int viewportY, int viewportHeight) { + if (method == null) + return; + + // Build tooltip content + List segments = buildTooltipContent(method); + if (segments.isEmpty()) + return; + + int lineHeight = ClientProxy.Font.height(); + + // Calculate dimensions + int contentWidth = calculateContentWidth(segments); + int contentHeight = lineHeight; + int boxWidth = contentWidth + PADDING * 2; + int boxHeight = contentHeight + PADDING * 2; + + // Position tooltip + int tooltipX = mouseX + VERTICAL_OFFSET; + int tooltipY = mouseY - 5; + + // Clamp to viewport + int rightBound = viewportX + viewportWidth; + int bottomBound = viewportY + viewportHeight; + + if (tooltipX + boxWidth > rightBound) { + tooltipX = mouseX - boxWidth - 5; + } + if (tooltipX < viewportX) { + tooltipX = viewportX; + } + + if (tooltipY + boxHeight > bottomBound) { + tooltipY = bottomBound - boxHeight; + } + if (tooltipY < viewportY) { + tooltipY = viewportY; + } + + // Render tooltip box + renderTooltipBox(tooltipX, tooltipY, boxWidth, boxHeight, segments); + } + + // ==================== PRIVATE HELPERS ==================== + + /** + * Get the method that starts on the given line. + */ + private static MethodInfo getMethodAtLine(int lineIndex, List methods, List lines) { + if (lineIndex < 0 || lineIndex >= lines.size()) + return null; + + Object lineObj = lines.get(lineIndex); + int lineStart, lineEnd; + + try { + java.lang.reflect.Field startField = lineObj.getClass().getField("start"); + java.lang.reflect.Field endField = lineObj.getClass().getField("end"); + lineStart = startField.getInt(lineObj); + lineEnd = endField.getInt(lineObj); + } catch (Exception e) { + return null; + } + + for (MethodInfo method : methods) { + if (!method.hasInheritanceMarker()) + continue; + int nameOffset = method.getNameOffset(); + if (nameOffset >= lineStart && nameOffset < lineEnd) { + return method; + } + } + + return null; + } + + /** + * Build the tooltip content as colored text segments. + */ + private static List buildTooltipContent(MethodInfo method) { + List segments = new ArrayList<>(); + + if (method.isOverride()) { + TypeInfo overridesFrom = method.getOverridesFrom(); + segments.add(new TextSegment("Overrides method in ", INFO_COLOR)); + + if (overridesFrom != null) { + int color = TokenType.getColor(overridesFrom); + segments.add(new TextSegment(overridesFrom.getSimpleName(), color)); + } else { + segments.add(new TextSegment("parent class", INFO_COLOR)); + } + } else if (method.isImplements()) { + TypeInfo implementsFrom = method.getImplementsFrom(); + segments.add(new TextSegment("Implements method from ", INFO_COLOR)); + + if (implementsFrom != null) { + int color = TokenType.getColor(implementsFrom); + segments.add(new TextSegment(implementsFrom.getSimpleName(), color)); + } else { + segments.add(new TextSegment("interface", INFO_COLOR)); + } + } + + return segments; + } + + /** + * Calculate the width needed for the segments. + */ + private static int calculateContentWidth(List segments) { + int totalWidth = 0; + for (TextSegment segment : segments) { + totalWidth += ClientProxy.Font.width(segment.text); + } + return totalWidth; + } + + /** + * Render the tooltip box with background, border, and content. + */ + private static void renderTooltipBox(int x, int y, int width, int height, List segments) { + GL11.glDisable(GL11.GL_SCISSOR_TEST); + + // Draw background + int paddedHeight = y + height - 4; + Gui.drawRect(x, y, x + width, paddedHeight, BG_COLOR); + + // Draw border + Gui.drawRect(x, y, x + width, y + 1, BORDER_COLOR); // Top + Gui.drawRect(x, paddedHeight - 1, x + width, paddedHeight, BORDER_COLOR); // Bottom + Gui.drawRect(x, y, x + 1, paddedHeight, BORDER_COLOR); // Left + Gui.drawRect(x + width - 1, y, x + width, paddedHeight, BORDER_COLOR); // Right + + // Draw text segments + int currentX = x + PADDING; + int currentY = y + PADDING; + + for (TextSegment segment : segments) { + ClientProxy.Font.drawString(segment.text, currentX, currentY, 0xFF000000 | segment.color); + currentX += ClientProxy.Font.width(segment.text); + } + } + + // ==================== DATA CLASSES ==================== + + /** + * A colored text segment. + */ + private static class TextSegment { + final String text; + final int color; + + TextSegment(String text, int color) { + this.text = text; + this.color = color; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java index 58368a876..b018c942f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java @@ -1,5 +1,7 @@ package noppes.npcs.client.gui.util.script.interpreter.token; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + /** * Defines all token types for syntax highlighting with hex colors and priorities. * Priority determines which token type wins when marks overlap. @@ -57,6 +59,25 @@ public enum TokenType { return priority; } + public static int getColor(TypeInfo typeInfo) { + if (typeInfo == null) + return TokenType.IMPORTED_CLASS.getHexColor(); + + switch (typeInfo.getKind()) { + case INTERFACE: + return TokenType.INTERFACE_DECL.getHexColor(); + case ENUM: + return TokenType.ENUM_DECL.getHexColor(); + case CLASS: + return TokenType.CLASS_DECL.getHexColor(); + default: + break; + } + + // Use the TypeInfo's own token type, which handles ScriptTypeInfo correctly + return typeInfo.getTokenType().getHexColor(); + } + /** * Convert this token type to a Minecraft color code character. * Used for backward compatibility with the existing rendering system.