diff --git a/src/main/java/org/saturnclient/ui/components/HudModPreview.java b/src/main/java/org/saturnclient/ui/components/HudModPreview.java index a4e803a..ba1754d 100644 --- a/src/main/java/org/saturnclient/ui/components/HudModPreview.java +++ b/src/main/java/org/saturnclient/ui/components/HudModPreview.java @@ -15,13 +15,21 @@ public class HudModPreview extends Element { @Override public void render(RenderScope scope, ElementContext ctx) { - ModLayout layout = feature.getDimensions(); + draw(scope, feature); + } + + /** + * Draws a HUD mod with sample content and the background it has in game, + * at the origin and its layout's unscaled size. + */ + public static void draw(RenderScope scope, HudMod mod) { + ModLayout layout = mod.getDimensions(); if (layout.renderBackground) { scope.drawRoundedRectangle(0, 0, layout.width, layout.height, layout.radius.value, layout.bgColor.value); } - feature.renderDummy(scope); + mod.renderDummy(scope); } } diff --git a/src/main/java/org/saturnclient/ui/screens/HudEditor.java b/src/main/java/org/saturnclient/ui/screens/HudEditor.java index 7047b26..8f10f80 100644 --- a/src/main/java/org/saturnclient/ui/screens/HudEditor.java +++ b/src/main/java/org/saturnclient/ui/screens/HudEditor.java @@ -1,59 +1,71 @@ package org.saturnclient.ui.screens; -import org.saturnclient.common.ref.render.MatrixStackRef; +import java.util.ArrayList; +import java.util.List; + import org.saturnclient.config.ConfigManager; +import org.saturnclient.config.Theme; import org.saturnclient.mod.HudMod; import org.saturnclient.mod.Mod; import org.saturnclient.mod.ModLayout; import org.saturnclient.mod.ModManager; import org.saturnclient.ui.RenderScope; import org.saturnclient.ui.SaturnScreen; +import org.saturnclient.ui.components.HudModPreview; +import org.saturnclient.ui.resources.Fonts; -import java.util.ArrayList; -import java.util.List; - +/** + * Moves and resizes the enabled HUD mods, drawn as they are in game. Drag one + * to move it, or drag its corner handle to resize it. + * + * Unlike other Saturn screens this works in GUI pixels, like the HUD itself: + * it draws without the half scale and takes the mouse unscaled. + */ public class HudEditor extends SaturnScreen { + private static final String HINT = "Drag to move · Drag the corner to resize · Esc when done"; + + /** Size of the resize handle, and how far around it still grabs it */ + private static final int HANDLE = 4; + private static final int HANDLE_REACH = 4; + private static final float LABEL_SCALE = 0.4f; + private static final float HINT_SCALE = 0.45f; + private final List hudMods = new ArrayList<>(); + private HudMod draggingMod = null; private boolean resizing = false; private int offsetX, offsetY; - private static final int RESIZE_MARGIN = 3; public HudEditor() { super("HUD Editor"); for (Mod m : ModManager.ENABLED_MODS) { - if (m.isEnabled() && m instanceof HudMod) { - hudMods.add((HudMod) m); + if (m.isEnabled() && m instanceof HudMod hudMod) { + hudMods.add(hudMod); } } } + @Override + public void ui() { + // Placing HUDs needs a clear view of the game. + blurBackground = false; + } + @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { if (button == 0) { - for (int m = hudMods.size() - 1; m >= 0; m--) { - HudMod mod = hudMods.get(m); + HudMod mod = getModAt(mouseX, mouseY); + + if (mod != null) { ModLayout dim = mod.getDimensions(); - - // Check if clicking the corner for resizing - if (isCorner(mouseX, mouseY, dim)) { - draggingMod = mod; - resizing = true; - offsetX = (int) (mouseX - (dim.x.value + dim.width * dim.scale.value)); - offsetY = (int) (mouseY - (dim.y.value + dim.height * dim.scale.value)); - return true; - } - - // Check if clicking inside the mod - if (isInside(mouseX, mouseY, dim)) { - draggingMod = mod; - resizing = false; - offsetX = (int) (mouseX - dim.x.value); - offsetY = (int) (mouseY - dim.y.value); - return true; - } + draggingMod = mod; + resizing = isOnHandle(mouseX, mouseY, dim); + offsetX = (int) (mouseX - (resizing ? right(dim) : dim.x.value)); + offsetY = (int) (mouseY - (resizing ? bottom(dim) : dim.y.value)); + return true; } } + return super.mouseClicked(mouseX, mouseY, button); } @@ -72,57 +84,121 @@ public class HudEditor extends SaturnScreen { ModLayout dim = draggingMod.getDimensions(); if (resizing) { - // Scale based on cursor movement - float newScale = Math.max(0.5f, ((float) (mouseX - dim.x.value) / dim.width)); - dim.scale.value = newScale; + // The handle follows the cursor: the scale is the new width over the unscaled width. + float scale = (float) (mouseX - offsetX - dim.x.value) / dim.width; + float min = dim.scale.min != null ? dim.scale.min : 0.5f; + float max = dim.scale.max != null ? dim.scale.max : 4f; + dim.scale.value = Math.max(min, Math.min(max, scale)); } else { - // Move the mod dim.x.value = (int) mouseX - offsetX; dim.y.value = (int) mouseY - offsetY; } + + keepOnScreen(dim); } + return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); } - @Override - public void ui() { - } - @Override public void render(RenderScope renderScope, int mouseX, int mouseY, float delta, long elapsed) { + // The screen hands over the mouse in Saturn units, twice GUI pixels. + int guiMouseX = mouseX / 2; + int guiMouseY = mouseY / 2; + HudMod hovered = draggingMod != null ? draggingMod : getModAt(guiMouseX, guiMouseY); + for (HudMod mod : hudMods) { ModLayout dim = mod.getDimensions(); - MatrixStackRef matrices = renderScope.getMatrixStack(); - matrices.push(); + boolean active = mod == hovered; - matrices.translate(dim.x.value, dim.y.value); - matrices.scale(dim.scale.value, dim.scale.value); + renderScope.getMatrixStack().push(); + renderScope.getMatrixStack().translate(dim.x.value, dim.y.value); + renderScope.getMatrixStack().scale(dim.scale.value, dim.scale.value); + HudModPreview.draw(renderScope, mod); + renderScope.getMatrixStack().pop(); - // Render the mod - mod.renderDummy(renderScope); + drawFrame(renderScope, mod, dim, active); + } - matrices.pop(); + drawHint(renderScope); + } - // Draw border around mod - int borderX = dim.x.value; - int borderY = dim.y.value; - int borderW = (int) (dim.width * dim.scale.value); - int borderH = (int) (dim.height * dim.scale.value); + /** + * The outline around a HUD, its resize handle, and its name while it's + * hovered or dragged. + */ + private void drawFrame(RenderScope renderScope, HudMod mod, ModLayout dim, boolean active) { + int x = dim.x.value; + int y = dim.y.value; + int w = right(dim) - x; + int h = bottom(dim) - y; + int color = active ? Theme.ACCENT.value : Theme.withAlpha(0.45f, Theme.FOREGROUND.value); - renderScope.drawBorder(borderX, borderY, borderW, borderH, 0xFFFFFFFF); // White border + if (active) { + renderScope.drawRectangle(x, y, w, h, Theme.withAlpha(0.08f, Theme.ACCENT.value)); + } + + renderScope.drawBorder(x - 1, y - 1, w + 2, h + 2, color); + renderScope.drawRectangle(x + w - HANDLE / 2, y + h - HANDLE / 2, HANDLE, HANDLE, color); + + if (active && mod instanceof Mod named) { + int labelHeight = (int) (Fonts.getHeight() * LABEL_SCALE); + int labelY = y - labelHeight - 4 < 0 ? y + h + 4 : y - labelHeight - 4; + renderScope.drawText(LABEL_SCALE, named.getName(), x, labelY, Theme.FONT.value, Theme.FOREGROUND.value); } } - private boolean isInside(double mouseX, double mouseY, ModLayout dim) { - return mouseX >= dim.x.value && mouseX <= dim.x.value + (dim.width * dim.scale.value) && - mouseY >= dim.y.value && mouseY <= dim.y.value + (dim.height * dim.scale.value); + private void drawHint(RenderScope renderScope) { + int guiWidth = width / 2; + int textWidth = (int) (Fonts.getWidth(HINT, Theme.getBodyFont()) * HINT_SCALE); + int textHeight = (int) (Fonts.getHeight() * HINT_SCALE); + int pillWidth = textWidth + 20; + int pillHeight = textHeight + 10; + int x = (guiWidth - pillWidth) / 2; + int y = 8; + + renderScope.drawRoundedRectangle(x, y, pillWidth, pillHeight, pillHeight / 2, 1, Theme.getPanelBg(), + Theme.getBorder()); + renderScope.drawText(HINT_SCALE, HINT, x + 10, y + 5, Theme.getBodyFont(), Theme.PRIMARY_FG.value); } - private boolean isCorner(double mouseX, double mouseY, ModLayout dim) { - float rightX = dim.x.value + dim.width * dim.scale.value; - float bottomY = dim.y.value + dim.height * dim.scale.value; + /** + * The HUD under the mouse, preferring the one drawn on top, or null. + */ + private HudMod getModAt(double mouseX, double mouseY) { + for (int i = hudMods.size() - 1; i >= 0; i--) { + ModLayout dim = hudMods.get(i).getDimensions(); + if (isOnHandle(mouseX, mouseY, dim) || isInside(mouseX, mouseY, dim)) { + return hudMods.get(i); + } + } + return null; + } - return Math.abs(mouseX - rightX) < RESIZE_MARGIN && Math.abs(mouseY - bottomY) < RESIZE_MARGIN; + private void keepOnScreen(ModLayout dim) { + int guiWidth = width / 2; + int guiHeight = height / 2; + int w = right(dim) - dim.x.value; + int h = bottom(dim) - dim.y.value; + + dim.x.value = Math.max(0, Math.min(dim.x.value, guiWidth - w)); + dim.y.value = Math.max(0, Math.min(dim.y.value, guiHeight - h)); + } + + private static int right(ModLayout dim) { + return dim.x.value + (int) (dim.width * dim.scale.value); + } + + private static int bottom(ModLayout dim) { + return dim.y.value + (int) (dim.height * dim.scale.value); + } + + private static boolean isInside(double mouseX, double mouseY, ModLayout dim) { + return mouseX >= dim.x.value && mouseX <= right(dim) && mouseY >= dim.y.value && mouseY <= bottom(dim); + } + + private static boolean isOnHandle(double mouseX, double mouseY, ModLayout dim) { + return Math.abs(mouseX - right(dim)) <= HANDLE_REACH && Math.abs(mouseY - bottom(dim)) <= HANDLE_REACH; } @Override