Rework the HUD editor

HUDs are drawn as they are in game, background included, through the
new HudModPreview.draw, and the world behind the editor isn't blurred.
Each has an outline that turns the accent color with the mod's name while
hovered or dragged, and a visible resize handle that's easier to grab.
Scale stays within its range and HUDs stay on screen, and a hint says how
to use the editor.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-27 19:09:06 +02:00
co-authored by claude
parent f0971ff128
commit 25d288da92
2 changed files with 141 additions and 57 deletions
@@ -15,13 +15,21 @@ public class HudModPreview extends Element {
@Override @Override
public void render(RenderScope scope, ElementContext ctx) { 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) { if (layout.renderBackground) {
scope.drawRoundedRectangle(0, 0, layout.width, layout.height, scope.drawRoundedRectangle(0, 0, layout.width, layout.height,
layout.radius.value, layout.bgColor.value); layout.radius.value, layout.bgColor.value);
} }
feature.renderDummy(scope); mod.renderDummy(scope);
} }
} }
@@ -1,59 +1,71 @@
package org.saturnclient.ui.screens; 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.ConfigManager;
import org.saturnclient.config.Theme;
import org.saturnclient.mod.HudMod; import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod; import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModLayout; import org.saturnclient.mod.ModLayout;
import org.saturnclient.mod.ModManager; import org.saturnclient.mod.ModManager;
import org.saturnclient.ui.RenderScope; import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.SaturnScreen; 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 { 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<HudMod> hudMods = new ArrayList<>(); private final List<HudMod> hudMods = new ArrayList<>();
private HudMod draggingMod = null; private HudMod draggingMod = null;
private boolean resizing = false; private boolean resizing = false;
private int offsetX, offsetY; private int offsetX, offsetY;
private static final int RESIZE_MARGIN = 3;
public HudEditor() { public HudEditor() {
super("HUD Editor"); super("HUD Editor");
for (Mod m : ModManager.ENABLED_MODS) { for (Mod m : ModManager.ENABLED_MODS) {
if (m.isEnabled() && m instanceof HudMod) { if (m.isEnabled() && m instanceof HudMod hudMod) {
hudMods.add((HudMod) m); hudMods.add(hudMod);
} }
} }
} }
@Override
public void ui() {
// Placing HUDs needs a clear view of the game.
blurBackground = false;
}
@Override @Override
public boolean mouseClicked(double mouseX, double mouseY, int button) { public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (button == 0) { if (button == 0) {
for (int m = hudMods.size() - 1; m >= 0; m--) { HudMod mod = getModAt(mouseX, mouseY);
HudMod mod = hudMods.get(m);
if (mod != null) {
ModLayout dim = mod.getDimensions(); ModLayout dim = mod.getDimensions();
draggingMod = mod;
// Check if clicking the corner for resizing resizing = isOnHandle(mouseX, mouseY, dim);
if (isCorner(mouseX, mouseY, dim)) { offsetX = (int) (mouseX - (resizing ? right(dim) : dim.x.value));
draggingMod = mod; offsetY = (int) (mouseY - (resizing ? bottom(dim) : dim.y.value));
resizing = true; return 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;
}
} }
} }
return super.mouseClicked(mouseX, mouseY, button); return super.mouseClicked(mouseX, mouseY, button);
} }
@@ -72,57 +84,121 @@ public class HudEditor extends SaturnScreen {
ModLayout dim = draggingMod.getDimensions(); ModLayout dim = draggingMod.getDimensions();
if (resizing) { if (resizing) {
// Scale based on cursor movement // The handle follows the cursor: the scale is the new width over the unscaled width.
float newScale = Math.max(0.5f, ((float) (mouseX - dim.x.value) / dim.width)); float scale = (float) (mouseX - offsetX - dim.x.value) / dim.width;
dim.scale.value = newScale; 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 { } else {
// Move the mod
dim.x.value = (int) mouseX - offsetX; dim.x.value = (int) mouseX - offsetX;
dim.y.value = (int) mouseY - offsetY; dim.y.value = (int) mouseY - offsetY;
} }
keepOnScreen(dim);
} }
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
} }
@Override
public void ui() {
}
@Override @Override
public void render(RenderScope renderScope, int mouseX, int mouseY, float delta, long elapsed) { 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) { for (HudMod mod : hudMods) {
ModLayout dim = mod.getDimensions(); ModLayout dim = mod.getDimensions();
MatrixStackRef matrices = renderScope.getMatrixStack(); boolean active = mod == hovered;
matrices.push();
matrices.translate(dim.x.value, dim.y.value); renderScope.getMatrixStack().push();
matrices.scale(dim.scale.value, dim.scale.value); 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 drawFrame(renderScope, mod, dim, active);
mod.renderDummy(renderScope); }
matrices.pop(); drawHint(renderScope);
}
// Draw border around mod /**
int borderX = dim.x.value; * The outline around a HUD, its resize handle, and its name while it's
int borderY = dim.y.value; * hovered or dragged.
int borderW = (int) (dim.width * dim.scale.value); */
int borderH = (int) (dim.height * dim.scale.value); 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) { private void drawHint(RenderScope renderScope) {
return mouseX >= dim.x.value && mouseX <= dim.x.value + (dim.width * dim.scale.value) && int guiWidth = width / 2;
mouseY >= dim.y.value && mouseY <= dim.y.value + (dim.height * dim.scale.value); 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; * The HUD under the mouse, preferring the one drawn on top, or null.
float bottomY = dim.y.value + dim.height * dim.scale.value; */
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 @Override