Restyle buttons, the Shift menu and the title screen

Button is now primary (accent fill) or secondary (a translucent outlined
surface), and IconButton is a square secondary button with a Lucide icon,
replacing the PNG TextureButtons. Neither is scaled with Element.scale any
more, which multiplied click positions instead of dividing them.

The Shift menu is the HUD editor, the mod menu (its button was labelled
"Settings") and cosmetics. The title screen centers the logo over a
compact stack of the game's menus, with the player, a dock of Saturn's
screens and the version along the bottom. The player chip shows the
player's face from their skin and whether they're connected to Saturn.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-27 19:01:42 +02:00
co-authored by claude
parent 05dbec028a
commit 2f4ea77e6f
7 changed files with 323 additions and 73 deletions
@@ -0,0 +1,54 @@
package org.saturnclient.ui.components;
import org.saturnclient.client.ServiceClient;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.config.Theme;
import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.elements.Field;
import org.saturnclient.ui.resources.Fonts;
/**
* The signed-in player: their skin's face, their name, and a dot that's green
* while connected to Saturn.
*/
public class AccountChip extends Element {
public static final int HEIGHT = 40;
private static final int HEAD = 28;
private static final int PADDING = 6;
private static final int DOT = 7;
private static final float NAME_SCALE = 0.7f;
private static final int CONNECTED = 0xFF7FD1A8;
private final String name;
public AccountChip() {
String username = Providers.saturn.getClient().getUsername();
this.name = username != null ? username : "Player";
this.height = HEIGHT;
this.width = PADDING + HEAD + 10 + (int) (Fonts.getWidth(name, Theme.FONT.value) * NAME_SCALE) + 10 + DOT + 12;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1, Theme.getPanelBg(),
Theme.getBorder());
// The face, then the hat layer over it, from the 64x64 skin
IdentifierRef skin = Providers.saturn.getPlayerSkin();
int headY = (height - HEAD) / 2;
renderScope.drawTexture(skin, PADDING, headY, 8, 8, HEAD, HEAD, 8, 8, 64, 64);
renderScope.drawTexture(skin, PADDING, headY, 40, 8, HEAD, HEAD, 8, 8, 64, 64);
int nameX = PADDING + HEAD + 10;
renderScope.drawText(NAME_SCALE, name, nameX, Field.textY(height, NAME_SCALE), Theme.FONT.value,
Theme.FOREGROUND.value);
int dotX = nameX + (int) (Fonts.getWidth(name, Theme.FONT.value) * NAME_SCALE) + 10;
renderScope.drawRoundedRectangle(dotX, (height - DOT) / 2, DOT, DOT, DOT / 2,
ServiceClient.isConnected() ? CONNECTED : Theme.withAlpha(0.3f, Theme.FOREGROUND.value));
}
}
@@ -0,0 +1,85 @@
package org.saturnclient.ui.components;
import java.util.List;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.config.Theme;
import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.elements.Field;
import org.saturnclient.ui.resources.Fonts;
/**
* A glass bar of buttons, each an icon with its label.
*/
public class Dock extends Element {
public record Item(IdentifierRef icon, String label, Runnable onClick) {
}
public static final int HEIGHT = 52;
private static final int PADDING = 6;
private static final int ITEM_HEIGHT = HEIGHT - PADDING * 2;
private static final int ITEM_PADDING = 14;
private static final int ICON = 17;
private static final int ICON_GAP = 8;
private static final int GAP = 4;
private static final float TEXT_SCALE = 0.7f;
private final List<Item> items;
public Dock(List<Item> items) {
this.items = items;
this.height = HEIGHT;
this.width = getItemX(items.size()) - GAP + PADDING;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value + 4, 1, Theme.getPanelBg(),
Theme.withAlpha(0.1f, Theme.FOREGROUND.value));
for (int i = 0; i < items.size(); i++) {
Item item = items.get(i);
int x = getItemX(i);
int itemWidth = getItemWidth(item);
boolean hovered = ctx.isHovering(x, PADDING, itemWidth, ITEM_HEIGHT);
if (hovered) {
renderScope.drawRoundedRectangle(x, PADDING, itemWidth, ITEM_HEIGHT, Theme.WIDGET_RADIUS.value,
Theme.withAlpha(0.08f, Theme.FOREGROUND.value));
}
int color = hovered ? Theme.ACCENT.value : Theme.PRIMARY_FG.value;
renderScope.drawTexture(item.icon(), x + ITEM_PADDING, PADDING + (ITEM_HEIGHT - ICON) / 2, 0, 0, ICON,
ICON, color);
renderScope.drawText(TEXT_SCALE, item.label(), x + ITEM_PADDING + ICON + ICON_GAP,
PADDING + Field.textY(ITEM_HEIGHT, TEXT_SCALE), Theme.FONT.value,
hovered ? Theme.FOREGROUND.value : Theme.PRIMARY_FG.value);
}
}
@Override
public void click(int mouseX, int mouseY) {
for (int i = 0; i < items.size(); i++) {
int x = getItemX(i);
if (mouseX >= x && mouseX <= x + getItemWidth(items.get(i))) {
items.get(i).onClick().run();
return;
}
}
}
private int getItemX(int index) {
int x = PADDING;
for (int i = 0; i < index; i++) {
x += getItemWidth(items.get(i)) + GAP;
}
return x;
}
private static int getItemWidth(Item item) {
return ITEM_PADDING * 2 + ICON + ICON_GAP + (int) (Fonts.getWidth(item.label(), Theme.FONT.value) * TEXT_SCALE);
}
}