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
7 changed files with 323 additions and 73 deletions
Showing only changes of commit 2f4ea77e6f - Show all commits
@@ -22,6 +22,12 @@ public interface SaturnProvider {
/** The Saturn Client mod version, e.g. {@code 0.1.0-beta3+1.21.11}. */ /** The Saturn Client mod version, e.g. {@code 0.1.0-beta3+1.21.11}. */
public String getModVersion(); public String getModVersion();
/**
* The signed-in player's 64x64 skin texture, or the default skin until
* theirs has loaded.
*/
public IdentifierRef getPlayerSkin();
/** Shows a warning toast. Safe to call from any thread. */ /** Shows a warning toast. Safe to call from any thread. */
public void showWarning(String title, String message); public void showWarning(String title, String message);
} }
@@ -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);
}
}
@@ -6,24 +6,52 @@ import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope; import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts; import org.saturnclient.ui.resources.Fonts;
/**
* A text button: primary (filled with the accent color) for the main action,
* or secondary (a translucent, outlined surface) for the rest.
*/
public class Button extends Element { public class Button extends Element {
private String text; public static final int HEIGHT = 40;
private Runnable onClick;
public Button(String text, Runnable onClick) { private static final float TEXT_SCALE = 0.72f;
private final String text;
private final boolean primary;
private final Runnable onClick;
public Button(String text, boolean primary, Runnable onClick) {
this.text = text; this.text = text;
this.primary = primary;
this.onClick = onClick; this.onClick = onClick;
this.dimensions(Fonts.getWidth(text, Theme.FONT.value) + 50, 38); this.width = (int) (Fonts.getWidth(text, Theme.FONT.value) * TEXT_SCALE) + 48;
this.height = HEIGHT;
} }
@Override @Override
public void render(RenderScope renderScope, ElementContext ctx) { public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, boolean hovered = ctx.isHovering();
ctx.isHovering() ? Theme.ACCENT.value : Theme.PRIMARY.value); drawSurface(renderScope, width, height, primary, hovered);
renderScope.drawText(text, Fonts.centerX(width, text, Theme.FONT.value), Fonts.centerY(height), int textX = (width - (int) (Fonts.getWidth(text, Theme.FONT.value) * TEXT_SCALE)) / 2;
Theme.FONT.value, renderScope.drawText(TEXT_SCALE, text, textX, Field.textY(height, TEXT_SCALE), Theme.FONT.value,
ctx.isHovering() ? Theme.ACCENT_FG.value : Theme.PRIMARY_FG.value); primary ? Theme.ACCENT_FG.value : hovered ? Theme.FOREGROUND.value : Theme.PRIMARY_FG.value);
}
/**
* The button's surface, shared with {@link IconButton}.
*/
static void drawSurface(RenderScope renderScope, int width, int height, boolean primary, boolean hovered) {
if (primary) {
// Lighten the accent on hover by laying white over it
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, Theme.ACCENT.value);
if (hovered) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 0x26FFFFFF);
}
} else {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
hovered ? Theme.withAlpha(0.9f, Theme.PRIMARY.value) : Theme.getPanelBg(),
Theme.withAlpha(hovered ? 0.25f : 0.1f, Theme.FOREGROUND.value));
}
} }
@Override @Override
@@ -0,0 +1,40 @@
package org.saturnclient.ui.elements;
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;
/**
* A square secondary {@link Button} showing an icon, which turns the accent
* color on hover.
*/
public class IconButton extends Element {
private static final float ICON_RATIO = 0.48f;
private final IdentifierRef icon;
private final Runnable onClick;
public IconButton(IdentifierRef icon, Runnable onClick) {
this.icon = icon;
this.onClick = onClick;
this.width = Button.HEIGHT;
this.height = Button.HEIGHT;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
boolean hovered = ctx.isHovering();
Button.drawSurface(renderScope, width, height, false, hovered);
int size = (int) (Math.min(width, height) * ICON_RATIO);
renderScope.drawTexture(icon, (width - size) / 2, (height - size) / 2, 0, 0, size, size,
hovered ? Theme.ACCENT.value : Theme.PRIMARY_FG.value);
}
@Override
public void click(int mouseX, int mouseY) {
onClick.run();
}
}
@@ -8,18 +8,24 @@ import org.saturnclient.ui.anim.Fade;
import org.saturnclient.ui.anim.SlideY; import org.saturnclient.ui.anim.SlideY;
import org.saturnclient.ui.elements.AnimationStagger; import org.saturnclient.ui.elements.AnimationStagger;
import org.saturnclient.ui.elements.Button; import org.saturnclient.ui.elements.Button;
import org.saturnclient.ui.elements.IconButton;
import org.saturnclient.ui.elements.ImageTexture; import org.saturnclient.ui.elements.ImageTexture;
import org.saturnclient.ui.elements.TextureButton;
import org.saturnclient.ui.resources.Textures; import org.saturnclient.ui.resources.Textures;
/**
* The in-game quick menu: the logo over the HUD editor, mod menu and
* cosmetics.
*/
public class ShiftMenu extends SaturnScreen { public class ShiftMenu extends SaturnScreen {
private static final int MODS_WIDTH = 120;
private static final int GAP = 6;
public ShiftMenu() { public ShiftMenu() {
super("Shift Menu"); super("Shift Menu");
} }
@Override @Override
public void ui() { public void ui() {
// Logo animations (unchanged)
draw(new ImageTexture(Textures.LOGO_TEXT) draw(new ImageTexture(Textures.LOGO_TEXT)
.dimensions(98, 10) .dimensions(98, 10)
.center(width, height, 0, -36) .center(width, height, 0, -36)
@@ -30,32 +36,24 @@ public class ShiftMenu extends SaturnScreen {
.center(width, height, 0, -80) .center(width, height, 0, -80)
.animation(new SlideY(AnimationConfig.logo, -20))); .animation(new SlideY(AnimationConfig.logo, -20)));
// Spacing and sizes
int btnWidth = 120;
int btnHeight = 40;
int spacing = 4;
// Create stagger
AnimationStagger stagger = new AnimationStagger(AnimationConfig.shiftMenu.stagger.value); AnimationStagger stagger = new AnimationStagger(AnimationConfig.shiftMenu.stagger.value);
int x = 0;
stagger.draw(new TextureButton(Textures.HUD_ICON, () -> { stagger.draw(new IconButton(Textures.ICON_HUD_EDITOR,
Providers.saturn.getClient().setScreen(new HudEditor()); () -> Providers.saturn.getClient().setScreen(new HudEditor()))
}).dimensions(btnHeight, btnHeight).position(0, 0) .position(x, 0).animation(new Fade(AnimationConfig.shiftMenu)));
.animation(new Fade(AnimationConfig.shiftMenu))); x += Button.HEIGHT + GAP;
stagger.draw(new Button("Settings", () -> { stagger.draw(new Button("Mods", true, () -> Providers.saturn.getClient().setScreen(new ModMenu()))
Providers.saturn.getClient().setScreen(new ModMenu()); .dimensions(MODS_WIDTH, Button.HEIGHT)
}).dimensions(btnWidth, btnHeight).position(btnHeight + spacing, 0) .position(x, 0).animation(new Fade(AnimationConfig.shiftMenu)));
.animation(new Fade(AnimationConfig.shiftMenu))); x += MODS_WIDTH + GAP;
stagger.draw(new TextureButton(Textures.COSMETICS, () -> { stagger.draw(new IconButton(Textures.ICON_COSMETICS,
Providers.saturn.getClient().setScreen(new CosmeticsMenu()); () -> Providers.saturn.getClient().setScreen(new CosmeticsMenu()))
}).dimensions(btnHeight, btnHeight).position(btnWidth + btnHeight + (spacing * 2), 0) .position(x, 0).animation(new Fade(AnimationConfig.shiftMenu)));
.animation(new Fade(AnimationConfig.shiftMenu))); x += Button.HEIGHT;
// Draw the stagger at the absolute center of the screen draw(stagger.position((width - x) / 2, (height - Button.HEIGHT) / 2));
draw(stagger.dimensions(btnWidth + (btnHeight * 2) + (spacing * 2), btnHeight).center(width, height, 0,
0));
} }
} }
@@ -1,21 +1,40 @@
package org.saturnclient.ui.screens; package org.saturnclient.ui.screens;
import java.util.List;
import org.saturnclient.common.provider.Providers; import org.saturnclient.common.provider.Providers;
import org.saturnclient.common.ref.game.MinecraftClientRef.MinecraftScreen; import org.saturnclient.common.ref.game.MinecraftClientRef.MinecraftScreen;
import org.saturnclient.config.AnimationConfig; import org.saturnclient.config.AnimationConfig;
import org.saturnclient.config.Config; import org.saturnclient.config.Config;
import org.saturnclient.config.Theme;
import org.saturnclient.ui.Element;
import org.saturnclient.ui.SaturnScreen; import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.anim.Fade; import org.saturnclient.ui.anim.Fade;
import org.saturnclient.ui.anim.SlideFade; import org.saturnclient.ui.anim.SlideFade;
import org.saturnclient.ui.anim.SlideY; import org.saturnclient.ui.anim.SlideY;
import org.saturnclient.ui.components.AccountChip;
import org.saturnclient.ui.components.Dock;
import org.saturnclient.ui.elements.AnimationStagger; import org.saturnclient.ui.elements.AnimationStagger;
import org.saturnclient.ui.elements.Button; import org.saturnclient.ui.elements.Button;
import org.saturnclient.ui.elements.ImageTexture; import org.saturnclient.ui.elements.ImageTexture;
import org.saturnclient.ui.elements.TextureButton; import org.saturnclient.ui.elements.Label;
import org.saturnclient.ui.resources.Fonts; import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.ui.resources.Textures; import org.saturnclient.ui.resources.Textures;
/**
* Saturn's title screen: the logo over the game's menus in the middle, and
* along the bottom the player, a dock of Saturn's screens and the version.
*/
public class TitleMenu extends SaturnScreen { public class TitleMenu extends SaturnScreen {
private static final int LOGO = 88;
private static final int WORDMARK_WIDTH = 200;
private static final int WORDMARK_HEIGHT = 20;
private static final int COLUMN_WIDTH = 280;
private static final int BUTTON_HEIGHT = 36;
private static final int GAP = 8;
private static final int EDGE = 28;
private static final float VERSION_SCALE = 0.62f;
public TitleMenu() { public TitleMenu() {
super("Title Menu"); super("Title Menu");
} }
@@ -24,53 +43,73 @@ public class TitleMenu extends SaturnScreen {
public void ui() { public void ui() {
blurBackground = false; blurBackground = false;
draw(new ImageTexture(Textures.LOGO_TEXT).dimensions(98, 10).center(width, height, 0, -40) // Logo and menus, centered together a little above the middle
.animation(new Fade(AnimationConfig.logo.duration.value))); int menusHeight = BUTTON_HEIGHT * 3 + GAP * 2;
draw(new ImageTexture(Config.getLogo()).dimensions(98, 98).center(width, height, 0, -84) int blockHeight = LOGO + 16 + WORDMARK_HEIGHT + 36 + menusHeight;
int top = (height - blockHeight) / 2 - 20;
draw(new ImageTexture(Config.getLogo())
.dimensions(LOGO, LOGO)
.position((width - LOGO) / 2, top)
.animation(new SlideY(AnimationConfig.logo, -20))); .animation(new SlideY(AnimationConfig.logo, -20)));
draw(new ImageTexture(Textures.LOGO_TEXT_BIG)
.dimensions(WORDMARK_WIDTH, WORDMARK_HEIGHT)
.position((width - WORDMARK_WIDTH) / 2, top + LOGO + 16)
.animation(new Fade(AnimationConfig.logo.duration.value)));
AnimationStagger mainButtonStagger = new AnimationStagger(AnimationConfig.mainMenu); int rowHeight = BUTTON_HEIGHT + GAP;
int halfWidth = (COLUMN_WIDTH - GAP) / 2;
AnimationStagger menus = new AnimationStagger(AnimationConfig.mainMenu);
mainButtonStagger.draw(new Button("SINGLEPLAYER", () -> { addMenuButton(menus, new Button("Singleplayer", true,
Providers.saturn.getClient().setScreen(MinecraftScreen.SelectWorld); () -> Providers.saturn.getClient().setScreen(MinecraftScreen.SelectWorld))
}).scale(0.69f).dimensions(420, 52).position(0, 0) .dimensions(COLUMN_WIDTH, BUTTON_HEIGHT).position(0, 0));
.animation(new SlideFade(AnimationConfig.mainMenu, -10))); addMenuButton(menus, new Button("Multiplayer", false,
() -> Providers.saturn.getClient().setScreen(MinecraftScreen.Multiplayer))
.dimensions(COLUMN_WIDTH, BUTTON_HEIGHT).position(0, rowHeight));
addMenuButton(menus, new Button("Options", false,
() -> Providers.saturn.getClient().setScreen(MinecraftScreen.Options))
.dimensions(halfWidth, BUTTON_HEIGHT).position(0, rowHeight * 2));
addMenuButton(menus, new Button("Quit", false,
() -> Providers.saturn.getClient().scheduleStop())
.dimensions(halfWidth, BUTTON_HEIGHT).position(COLUMN_WIDTH - halfWidth, rowHeight * 2));
mainButtonStagger.draw(new Button("MULTIPLAYER", () -> { draw(menus.position((width - COLUMN_WIDTH) / 2, top + LOGO + 16 + WORDMARK_HEIGHT + 36));
Providers.saturn.getClient().setScreen(MinecraftScreen.Multiplayer);
}).scale(0.69f).dimensions(420, 52).position(0, 40)
.animation(new SlideFade(AnimationConfig.mainMenu, -10)));
mainButtonStagger.draw(new Button("OPTIONS", () -> { // Along the bottom: the player, Saturn's screens, the version
Providers.saturn.getClient().setScreen(MinecraftScreen.Options); draw(new AccountChip()
}).scale(0.69f).dimensions(206, 52).position(0, 80) .position(EDGE, height - EDGE - AccountChip.HEIGHT)
.animation(new SlideFade(AnimationConfig.mainMenu, -10))); .animation(new Fade(AnimationConfig.mainMenu)));
mainButtonStagger.draw(new Button("QUIT", () -> { Dock dock = new Dock(List.of(
Providers.saturn.getClient().scheduleStop(); new Dock.Item(Textures.ICON_MODS, "Mods",
}).scale(0.69f).dimensions(206, 52).position(146, 80) () -> Providers.saturn.getClient().setScreen(new ModMenu())),
.animation(new SlideFade(AnimationConfig.mainMenu, -10))); new Dock.Item(Textures.ICON_COSMETICS, "Cosmetics",
() -> Providers.saturn.getClient().setScreen(new CosmeticsMenu())),
new Dock.Item(Textures.ICON_HUD_EDITOR, "HUD editor",
() -> Providers.saturn.getClient().setScreen(new HudEditor()))));
draw(dock.position((width - dock.width) / 2, height - EDGE - Dock.HEIGHT)
.animation(new SlideFade(AnimationConfig.mainMenu, 12)));
draw(mainButtonStagger.dimensions(289, 143).center(width, height, 0, 50)); String version = getVersionText();
int versionWidth = (int) (Fonts.getWidth(version, Theme.getBodyFont()) * VERSION_SCALE);
int versionHeight = (int) (Fonts.getHeight() * VERSION_SCALE);
draw(new Label(() -> version, VERSION_SCALE, Theme::getBodyFont, Theme::getMutedFg)
.position(width - EDGE - versionWidth, height - EDGE - (AccountChip.HEIGHT + versionHeight) / 2)
.animation(new Fade(AnimationConfig.mainMenu)));
}
int s = Fonts.getHeight() + 20; /**
* "Saturn Client 0.1.0 · Minecraft 1.21.11", from the mod version
* "0.1.0+1.21.11".
*/
private static String getVersionText() {
String[] version = Providers.saturn.getModVersion().split("\\+", 2);
String text = "Saturn Client " + version[0];
return version.length > 1 ? text + " · Minecraft " + version[1] : text;
}
AnimationStagger saturnStagger = new AnimationStagger(AnimationConfig.mainMenu.stagger.value); private static void addMenuButton(AnimationStagger stagger, Element button) {
stagger.draw(button.animation(new SlideFade(AnimationConfig.mainMenu, -10)));
saturnStagger.draw(new TextureButton(Textures.HUD_ICON, () -> {
Providers.saturn.getClient().setScreen(new HudEditor());
}).dimensions(s, s).position(0, 0).animation(new SlideFade(AnimationConfig.mainMenu, s + 8)));
saturnStagger.draw(new TextureButton(Textures.SETTINGS, () -> {
Providers.saturn.getClient().setScreen(new ModMenu());
}).dimensions(s, s).position(s + 2, 0)
.animation(new SlideFade(AnimationConfig.mainMenu, s + 8)));
saturnStagger.draw(new TextureButton(Textures.CLOAK, () -> {
Providers.saturn.getClient().setScreen(new CosmeticsMenu());
}).dimensions(s, s).position((s + 2) * 2, 0)
.animation(new SlideFade(AnimationConfig.mainMenu, s + 8)));
draw(saturnStagger.dimensions(((s + 2) * 2) + s, s).centerHorizontal(width, height, 0, 8));
} }
} }