Fix UI bugs and bring the remaining screens into the new style #7

Merged
selimaj-dev merged 5 commits from ui-fixes into master 2026-09-27 17:15:01 +00:00
2 changed files with 141 additions and 57 deletions
Showing only changes of commit 25d288da92 - Show all commits
@@ -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);
}
}
@@ -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<HudMod> 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