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
40 changed files with 686 additions and 475 deletions
@@ -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);
} }
@@ -8,6 +8,7 @@ import java.util.Optional;
import org.saturnclient.common.provider.Providers; import org.saturnclient.common.provider.Providers;
import org.saturnclient.emotes.core.Emote; import org.saturnclient.emotes.core.Emote;
import org.saturnclient.emotes.core.EmoteRegistry; import org.saturnclient.emotes.core.EmoteRegistry;
import org.saturnclient.ui.Utils;
/** /**
* Saturn's emotes, loaded from {@code assets/saturnclient/player_animations/<id>.json} (Emotecraft * Saturn's emotes, loaded from {@code assets/saturnclient/player_animations/<id>.json} (Emotecraft
@@ -23,6 +24,13 @@ public class Emotes {
/** The emotes that loaded, in wheel order. */ /** The emotes that loaded, in wheel order. */
public static List<String> availableEmotes = new ArrayList<>(); public static List<String> availableEmotes = new ArrayList<>();
/**
* The emote's name as shown to players.
*/
public static String getName(String id) {
return id.equals("tpose") ? "T-Pose" : Utils.nameFromId(id);
}
public static void initialize() { public static void initialize() {
for (String id : IDS) { for (String id : IDS) {
String path = "/assets/saturnclient/player_animations/" + id + ".json"; String path = "/assets/saturnclient/player_animations/" + id + ".json";
@@ -11,7 +11,6 @@ public class Element {
public boolean focused; public boolean focused;
public Animation animation; public Animation animation;
public float scale = 1.0f; public float scale = 1.0f;
public Integer duration = null; // Element duration, will fade out if not null
public void render(RenderScope renderScope, ElementContext ctx) { public void render(RenderScope renderScope, ElementContext ctx) {
} }
+16 -14
View File
@@ -1,7 +1,5 @@
package org.saturnclient.ui; package org.saturnclient.ui;
import java.util.List;
import org.saturnclient.common.provider.GLFWProvider; import org.saturnclient.common.provider.GLFWProvider;
public class Utils { public class Utils {
@@ -47,17 +45,21 @@ public class Utils {
}; };
} }
public static <T> T getOrNull(List<T> list, int index) { /**
return (index >= 0 && index < list.size()) ? list.get(index) : null; * A display name from an id: each underscore-separated word capitalized, so
* "black_hole_white" becomes "Black Hole White".
*/
public static String nameFromId(String id) {
StringBuilder builder = new StringBuilder();
for (String word : id.split("_")) {
if (word.isEmpty()) {
continue;
}
if (!builder.isEmpty()) {
builder.append(' ');
}
builder.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1));
}
return builder.toString();
} }
// public static void notify(NotificationKind kind, String title, String toast)
// {
// if (SaturnClient.client.currentScreen instanceof SaturnScreen) {
// ((SaturnScreen) SaturnClient.client.currentScreen)
// .draw(new Notification(SaturnClient.client.currentScreen.width,
// SaturnClient.client.currentScreen.height, kind,
// title, toast));
// }
// }
} }
@@ -1,27 +0,0 @@
package org.saturnclient.ui.anim;
import org.saturnclient.config.AnimationConfig;
import org.saturnclient.ui.Element;
public class FadeOut extends Animation {
float targetOpacity;
public FadeOut(int duration) {
super(duration);
}
public FadeOut(AnimationConfig config) {
super(config);
}
@Override
public void tick(double progress, Element element) {
element.opacity = targetOpacity - (float) progress;
}
@Override
public void init(Element element) {
targetOpacity = element.opacity;
// element.opacity = 1.0f;
}
}
@@ -1,39 +0,0 @@
package org.saturnclient.ui.anim;
import org.saturnclient.config.AnimationConfig;
import org.saturnclient.ui.Element;
public class SlideX extends Animation {
int offset = 20;
double startX;
public SlideX(AnimationConfig config) {
super(config);
}
public SlideX(AnimationConfig config, int offset) {
super(config);
this.offset = offset;
}
public SlideX(int duration) {
super(duration);
}
public SlideX(int duration, int offset) {
super(duration);
this.offset = offset;
}
@Override
public void init(Element element) {
startX = element.x - offset;
element.x = (int) startX;
}
@Override
public void tick(double progress, Element element) {
double currentX = startX + offset * progress;
element.x = (int) currentX;
}
}
@@ -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,67 @@
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;
/**
* A glass bar of square icon buttons.
*/
public class Dock extends Element {
public record Item(IdentifierRef icon, Runnable onClick) {
}
public static final int HEIGHT = 52;
private static final int PADDING = 6;
private static final int ITEM = HEIGHT - PADDING * 2;
private static final int ICON = 18;
private static final int GAP = 4;
private final List<Item> items;
public Dock(List<Item> items) {
this.items = items;
this.height = HEIGHT;
this.width = PADDING * 2 + items.size() * ITEM + (items.size() - 1) * GAP;
}
@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++) {
int x = getItemX(i);
boolean hovered = ctx.isHovering(x, PADDING, ITEM, ITEM);
if (hovered) {
renderScope.drawRoundedRectangle(x, PADDING, ITEM, ITEM, Theme.WIDGET_RADIUS.value,
Theme.withAlpha(0.08f, Theme.FOREGROUND.value));
}
int offset = (ITEM - ICON) / 2;
renderScope.drawTexture(items.get(i).icon(), x + offset, PADDING + offset, 0, 0, ICON, ICON,
hovered ? Theme.ACCENT.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 + ITEM) {
items.get(i).onClick().run();
return;
}
}
}
private static int getItemX(int index) {
return PADDING + index * (ITEM + GAP);
}
}
@@ -0,0 +1,68 @@
package org.saturnclient.ui.components;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.config.Theme;
import org.saturnclient.cosmetics.Emotes;
import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.ui.resources.Textures;
/**
* One emote in the emote wheel: its preview in its own colors and its name.
* An emote without a preview shows an icon instead.
*/
public class EmoteTile extends Element {
public static final int SIZE = 86;
private static final int INSET = 6;
private static final int NAME_AREA = 20;
private static final int PLACEHOLDER = 26;
private static final float NAME_SCALE = 0.56f;
private final String name;
/** The preview, or null when the emote has none */
private final IdentifierRef preview;
private final Runnable onPlay;
public EmoteTile(String emote, Runnable onPlay) {
IdentifierRef texture = Textures.getEmotePreview(emote);
this.name = Emotes.getName(emote);
this.preview = Textures.exists(texture) ? texture : null;
this.onPlay = onPlay;
this.width = SIZE;
this.height = SIZE;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
boolean hovered = ctx.isHovering();
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, hovered ? 2 : 1,
Theme.withAlpha(0.72f, Theme.PRIMARY.value),
hovered ? Theme.ACCENT.value : Theme.withAlpha(0.06f, Theme.FOREGROUND.value));
int imageSize = height - INSET * 2 - NAME_AREA;
if (preview != null) {
renderScope.drawTexture(preview, (width - imageSize) / 2, INSET, 0, 0, imageSize, imageSize);
} else {
int iconY = INSET + (imageSize - PLACEHOLDER) / 2;
renderScope.drawTexture(Textures.ICON_EMOTE, (width - PLACEHOLDER) / 2, iconY, 0, 0, PLACEHOLDER,
PLACEHOLDER, Theme.getMutedFg());
}
String text = Fonts.ellipsize(name, Theme.FONT.value, NAME_SCALE, width - INSET * 2);
int textX = (width - (int) (Fonts.getWidth(text, Theme.FONT.value) * NAME_SCALE)) / 2;
int textY = height - INSET - NAME_AREA + (NAME_AREA - (int) (Fonts.getHeight() * NAME_SCALE)) / 2;
renderScope.drawText(NAME_SCALE, text, textX, textY, Theme.FONT.value,
hovered ? Theme.FOREGROUND.value : Theme.PRIMARY_FG.value);
}
@Override
public void click(int mouseX, int mouseY) {
onPlay.run();
}
}
@@ -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);
} }
} }
@@ -6,24 +6,41 @@ 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: a translucent, outlined surface that brightens on hover.
*/
public class Button extends Element { public class Button extends Element {
private String text; public static final int HEIGHT = 40;
private Runnable onClick;
private static final float TEXT_SCALE = 0.72f;
private final String text;
private final Runnable onClick;
public Button(String text, Runnable onClick) { public Button(String text, Runnable onClick) {
this.text = text; this.text = text;
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, 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); 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 hovered) {
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
@@ -1,30 +0,0 @@
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;
public class ClickableTexture extends Element {
IdentifierRef sprite;
private Runnable onClick;
public ClickableTexture(IdentifierRef sprite, Runnable onClick) {
this.sprite = sprite;
this.onClick = onClick;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawTexture(sprite, 0, 0, 0, 0, width, height,
ctx.isHovering() ? Theme.ACCENT.value : Theme.PRIMARY.value);
}
@Override
public void click(int mouseX, int mouseY) {
if (onClick != null) {
onClick.run();
}
}
}
@@ -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 {@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, 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();
}
}
@@ -1,59 +0,0 @@
package org.saturnclient.ui.elements;
import org.saturnclient.config.Theme;
import org.saturnclient.config.property.ColorProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.anim.SlideX;
import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.ui.resources.Textures;
public class Notification extends Element {
private static ColorProperty error = Property.color(0xFFbf212f);
private static ColorProperty info = Property.color(0xFF264b96);
private static ColorProperty success = Property.color(0xFF27b376);
public static enum NotificationKind {
Error,
Info,
Success,
}
NotificationKind kind;
String text;
String title;
public Notification(int width, int height, NotificationKind kind, String title, String text) {
this.kind = kind;
this.text = text;
this.title = title;
this.width = (int) Math.max(Fonts.getWidth(text, 1) * 0.6f, Fonts.getWidth(title, 1)) + 20;
this.height = 50;
this.x = width - this.width - 15;
this.y = height - this.height - 15;
this.animation = new SlideX(700, -width);
this.duration = 3000;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, 10, Theme.PRIMARY.value);
switch (kind) {
case Error:
renderScope.drawTexture(Textures.CIRCLE_X, 9, 9, 0, 0, 15, 15, error.value);
break;
case Info:
renderScope.drawTexture(Textures.INFO, 9, 9, 0, 0, 15, 15, info.value);
break;
case Success:
renderScope.drawTexture(Textures.CHECK, 9, 9, 0, 0, 15, 15, success.value);
break;
}
renderScope.drawText(0.7f, title, 32, 10, 1, -1);
renderScope.drawText(0.5f, text, 32, 28, 1, -1);
}
}
@@ -1,22 +0,0 @@
package org.saturnclient.ui.elements;
import org.saturnclient.config.Theme;
import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
public class Text extends Element {
private String text;
public Text(String text) {
this.text = text;
width = Fonts.getWidth(text, Theme.FONT.value);
height = Fonts.getHeight();
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawText(this.text, 0, 0, Theme.FONT.value, Theme.FOREGROUND.value);
}
}
@@ -1,37 +0,0 @@
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;
public class TextureButton extends Element {
IdentifierRef sprite;
private Runnable onClick;
static int padding = 14;
public TextureButton(IdentifierRef sprite, Runnable onClick) {
this.sprite = sprite;
this.onClick = onClick;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value,
ctx.isHovering() ? Theme.ACCENT.value : Theme.PRIMARY.value);
int texWidth = width - padding;
int texHeight = height - padding;
renderScope.drawTexture(sprite, padding / 2, padding / 2, 0, 0, texWidth, texHeight,
ctx.isHovering() ? Theme.ACCENT_FG.value : Theme.PRIMARY_FG.value);
}
@Override
public void click(int mouseX, int mouseY) {
if (onClick != null) {
onClick.run();
}
}
}
@@ -1,29 +1,14 @@
package org.saturnclient.ui.resources; package org.saturnclient.ui.resources;
import java.io.IOException;
import java.io.InputStream;
import org.saturnclient.common.provider.Providers;
import org.saturnclient.common.ref.asset.IdentifierRef; import org.saturnclient.common.ref.asset.IdentifierRef;
public class Textures { public class Textures {
public static final IdentifierRef CROSSHAIR_RANGE = IdentifierRef public static final IdentifierRef CROSSHAIR_RANGE = IdentifierRef
.ofSaturn("textures/gui/hud/crosshair_range.png"); .ofSaturn("textures/gui/hud/crosshair_range.png");
public static final IdentifierRef COSMETICS = IdentifierRef.ofSaturn("textures/gui/components/cosmetics.png");
public static final IdentifierRef HUD_ICON = IdentifierRef.ofSaturn("textures/gui/components/hud.png");
public static final IdentifierRef SEARCH = IdentifierRef.ofSaturn("textures/gui/components/search.png");
public static final IdentifierRef CLOSE = IdentifierRef.ofSaturn("textures/gui/components/close.png");
public static final IdentifierRef MODS_TAB = IdentifierRef.ofSaturn("textures/gui/components/mods_tab.png");
public static final IdentifierRef SETTINGS = IdentifierRef.ofSaturn("textures/gui/components/settings.png");
public static final IdentifierRef CLOAK = IdentifierRef.ofSaturn("textures/gui/components/cloak.png");
public static final IdentifierRef HAT = IdentifierRef.ofSaturn("textures/gui/components/hat.png");
public static final IdentifierRef RESET = IdentifierRef.ofSaturn("textures/gui/components/reset.png");
public static final IdentifierRef TOGGLE_INDICATOR = IdentifierRef
.ofSaturn("textures/gui/components/toggle_indicator.png");
public static final IdentifierRef LEFT = IdentifierRef.ofSaturn("textures/gui/components/left.png");
public static final IdentifierRef RIGHT = IdentifierRef.ofSaturn("textures/gui/components/right.png");
public static final IdentifierRef STORE = IdentifierRef.ofSaturn("textures/gui/components/store.png");
public static final IdentifierRef COINS = IdentifierRef.ofSaturn("textures/gui/components/coins.png");
public static final IdentifierRef SHIRT = IdentifierRef.ofSaturn("textures/gui/components/shirt.png");
public static final IdentifierRef CIRCLE_X = IdentifierRef.ofSaturn("textures/gui/components/circle_x.png");
public static final IdentifierRef INFO = IdentifierRef.ofSaturn("textures/gui/components/info.png");
public static final IdentifierRef CHECK = IdentifierRef.ofSaturn("textures/gui/components/check.png");
// Lucide icons (SVG, drawn in white so they can be tinted) // Lucide icons (SVG, drawn in white so they can be tinted)
public static final IdentifierRef ICON_MODS = IdentifierRef.ofSaturn("textures/gui/components/mods.svg"); public static final IdentifierRef ICON_MODS = IdentifierRef.ofSaturn("textures/gui/components/mods.svg");
@@ -43,6 +28,7 @@ public class Textures {
.ofSaturn("textures/gui/components/chevron_right.svg"); .ofSaturn("textures/gui/components/chevron_right.svg");
public static final IdentifierRef ICON_CHEVRON_DOWN = IdentifierRef public static final IdentifierRef ICON_CHEVRON_DOWN = IdentifierRef
.ofSaturn("textures/gui/components/chevron_down.svg"); .ofSaturn("textures/gui/components/chevron_down.svg");
public static final IdentifierRef ICON_EMOTE = IdentifierRef.ofSaturn("textures/gui/components/emote.svg");
public static final IdentifierRef ICON_CHECK = IdentifierRef.ofSaturn("textures/gui/components/check.svg"); public static final IdentifierRef ICON_CHECK = IdentifierRef.ofSaturn("textures/gui/components/check.svg");
public static final IdentifierRef LOGO = IdentifierRef.ofSaturn("textures/logo/logo.png"); public static final IdentifierRef LOGO = IdentifierRef.ofSaturn("textures/logo/logo.png");
@@ -51,15 +37,17 @@ public class Textures {
public static final IdentifierRef LOGO_TEXT_BIG = IdentifierRef.ofSaturn("textures/logo/text_big.png"); public static final IdentifierRef LOGO_TEXT_BIG = IdentifierRef.ofSaturn("textures/logo/text_big.png");
public static final IdentifierRef SPLASH = IdentifierRef.ofSaturn("textures/gui/splash.png"); public static final IdentifierRef SPLASH = IdentifierRef.ofSaturn("textures/gui/splash.png");
// Widgets /**
public static final IdentifierRef BUTTON_BORDER = IdentifierRef.ofSaturn("button_border"); * Whether a texture file is in the resources, for falling back when an
public static final IdentifierRef BUTTON = IdentifierRef.ofSaturn("button"); * optional one (like an emote's preview) is missing.
public static final IdentifierRef BUTTON_OP = IdentifierRef.ofSaturn("button_op"); */
public static final IdentifierRef SETTINGS_BG = IdentifierRef.ofSaturn("settings_bg"); public static boolean exists(IdentifierRef texture) {
public static final IdentifierRef RECT = IdentifierRef.ofSaturn("rect"); try (InputStream in = Providers.saturn.getClient().getResource(texture)) {
public static final IdentifierRef RECT_BORDER = IdentifierRef.ofSaturn("rect_border"); return in != null;
public static final IdentifierRef MOD = IdentifierRef.ofSaturn("textures/gui/components/mod.png"); } catch (IOException e) {
public static final IdentifierRef MOD_BG = IdentifierRef.ofSaturn("textures/gui/components/mod_bg.png"); return false;
}
}
public static IdentifierRef getCloakPreview(String cloak) { public static IdentifierRef getCloakPreview(String cloak) {
return IdentifierRef.ofSaturn("textures/gui/cloak/" + (cloak.isEmpty() ? "none" : cloak) + ".png"); return IdentifierRef.ofSaturn("textures/gui/cloak/" + (cloak.isEmpty() ? "none" : cloak) + ".png");
@@ -19,6 +19,7 @@ import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.PanelScreen; import org.saturnclient.ui.PanelScreen;
import org.saturnclient.ui.RenderScope; import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.Utils;
import org.saturnclient.ui.anim.SlideFade; import org.saturnclient.ui.anim.SlideFade;
import org.saturnclient.ui.components.CosmeticTile; import org.saturnclient.ui.components.CosmeticTile;
import org.saturnclient.ui.components.Sidebar; import org.saturnclient.ui.components.Sidebar;
@@ -193,18 +194,7 @@ public class CosmeticsMenu extends PanelScreen {
private static String getName(String id) { private static String getName(String id) {
String name = NAMES.get(id); String name = NAMES.get(id);
if (name != null) { return name != null ? name : Utils.nameFromId(id);
return name;
}
StringBuilder builder = new StringBuilder();
for (String word : id.split("_")) {
if (!builder.isEmpty()) {
builder.append(' ');
}
builder.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1));
}
return builder.toString();
} }
/** /**
@@ -1,21 +1,43 @@
package org.saturnclient.ui.screens; package org.saturnclient.ui.screens;
import org.saturnclient.cosmetics.Emotes; import java.util.List;
import org.saturnclient.client.ServiceClient; import org.saturnclient.client.ServiceClient;
import org.saturnclient.common.provider.Providers; import org.saturnclient.common.provider.Providers;
import org.saturnclient.common.ref.asset.IdentifierRef;
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.cosmetics.Emotes;
import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.SaturnScreen; import org.saturnclient.ui.SaturnScreen;
import org.saturnclient.ui.Utils;
import org.saturnclient.ui.anim.Fade; import org.saturnclient.ui.anim.Fade;
import org.saturnclient.ui.anim.SlideY; import org.saturnclient.ui.anim.SlideY;
import org.saturnclient.ui.components.EmoteTile;
import org.saturnclient.ui.elements.AnimationStagger; import org.saturnclient.ui.elements.AnimationStagger;
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.Textures; import org.saturnclient.ui.resources.Textures;
/**
* A 3x3 wheel of emotes to play, with the page controls in the middle. The
* emote wheel key opens and closes it (in the version's key handler).
*/
public class EmoteWheel extends SaturnScreen { public class EmoteWheel extends SaturnScreen {
int page = 1; private static final int GAP = 10;
private static final int GRID = EmoteTile.SIZE * 3 + GAP * 2;
private static final int LOGO = 48;
/** The grid's cells in the order emotes fill them, around the middle one */
private static final int[][] SLOTS = {
{ 0, 0 }, { 1, 0 }, { 2, 0 },
{ 0, 1 }, { 2, 1 },
{ 0, 2 }, { 1, 2 }, { 2, 2 } };
private int page = 0;
public EmoteWheel() { public EmoteWheel() {
super("Emote Wheel"); super("Emote Wheel");
@@ -23,88 +45,131 @@ public class EmoteWheel extends SaturnScreen {
@Override @Override
public void ui() { public void ui() {
int row = 0; List<String> emotes = Emotes.availableEmotes;
int col = 0; int left = (width - GRID) / 2;
int top = (height - GRID) / 2 + LOGO / 2;
// =============================
// Logo
// =============================
draw(new ImageTexture(Textures.LOGO_TEXT)
.dimensions(98, 10)
.center(width, height, 0, -105)
.animation(new Fade(AnimationConfig.logo.duration.value)));
draw(new ImageTexture(Config.getLogo()) draw(new ImageTexture(Config.getLogo())
.dimensions(98, 98) .dimensions(LOGO, LOGO)
.center(width, height, 0, -149) .position((width - LOGO) / 2, top - 20 - LOGO)
.animation(new SlideY(AnimationConfig.logo, -20))); .animation(new SlideY(AnimationConfig.logo, -12)));
// ============================= if (emotes.isEmpty()) {
// Navigation Buttons String message = "No emotes available";
// ============================= int x = (width - (int) (Fonts.getWidth(message, Theme.getBodyFont()) * 0.7f)) / 2;
draw(new Label(() -> message, 0.7f, Theme::getBodyFont, Theme::getMutedFg).position(x, top + GRID / 2));
draw(new TextureButton(Textures.LEFT, () -> { return;
if (page > 1) {
page--;
elements.clear();
ui();
}
}).dimensions(30, 30)
.center(width, height, -50, 105)
.animation(new Fade(500)));
draw(new TextureButton(Textures.RIGHT, () -> {
if ((page * 8) < Emotes.availableEmotes.size()) {
page++;
elements.clear();
ui();
}
}).dimensions(30, 30)
.center(width, height, 50, 105)
.animation(new Fade(500)));
// =============================
// Emote Grid (Staggered)
// =============================
AnimationStagger emoteStagger = new AnimationStagger(AnimationConfig.emoteWheel);
while (row < 3) {
int idx = (row * 3 + col) + ((page - 1) * 8);
if (row == 1 && col == 1) {
col++;
} }
String emote = Utils.getOrNull(Emotes.availableEmotes, idx); int pages = (emotes.size() + SLOTS.length - 1) / SLOTS.length;
page = Math.min(page, pages - 1);
emoteStagger.draw( AnimationStagger grid = new AnimationStagger(AnimationConfig.emoteWheel);
new TextureButton(Textures.getEmotePreview(emote), () -> {
for (int slot = 0; slot < SLOTS.length; slot++) {
int index = page * SLOTS.length + slot;
if (index >= emotes.size()) {
break;
}
String emote = emotes.get(index);
grid.draw(new EmoteTile(emote, () -> play(emote))
.position(SLOTS[slot][0] * (EmoteTile.SIZE + GAP), SLOTS[slot][1] * (EmoteTile.SIZE + GAP))
.animation(new SlideY(AnimationConfig.emoteWheel, 12)));
}
draw(grid.position(left, top));
draw(new PageControl(page, pages, () -> turnPage(-1), () -> turnPage(1))
.position(left + EmoteTile.SIZE + GAP, top + EmoteTile.SIZE + GAP)
.animation(new Fade(AnimationConfig.emoteWheel)));
}
private void play(String emote) {
Providers.saturn.playEmote(ServiceClient.uuid, emote); Providers.saturn.playEmote(ServiceClient.uuid, emote);
ServiceClient.emote(emote); ServiceClient.emote(emote);
provider.close(); provider.close();
})
.dimensions(70, 70)
.center(width, height,
-80 + (col * 80),
-35 + (row * 80))
.animation(new SlideY(AnimationConfig.emoteWheel, 12)));
if (col < 2) {
col++;
} else {
col = 0;
row++;
}
} }
draw(emoteStagger); private void turnPage(int direction) {
page += direction;
init();
} }
@Override @Override
public boolean shouldPause() { public boolean shouldPause() {
return false; return false;
} }
/**
* The middle of the wheel: "Emotes", and the page with arrows to turn it
* when there's more than one.
*/
private static class PageControl extends Element {
private static final int ARROW = 16;
private static final int ARROW_AREA = 26;
private final int page;
private final int pages;
private final Runnable onPrevious;
private final Runnable onNext;
PageControl(int page, int pages, Runnable onPrevious, Runnable onNext) {
this.page = page;
this.pages = pages;
this.onPrevious = onPrevious;
this.onNext = onNext;
this.width = EmoteTile.SIZE;
this.height = EmoteTile.SIZE;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
Theme.getPanelBg(), Theme.getBorder());
float titleScale = 0.54f;
drawCentered(renderScope, "EMOTES", titleScale, pages > 1 ? 24 : (height - 10) / 2, Theme.getMutedFg());
if (pages <= 1) {
return;
}
drawCentered(renderScope, (page + 1) + " / " + pages, 0.66f, 48, Theme.FOREGROUND.value);
int arrowY = 48 + ((int) (Fonts.getHeight() * 0.66f) - ARROW) / 2;
drawArrow(renderScope, ctx, Textures.ICON_CHEVRON_LEFT, 4, arrowY, canGoBack());
drawArrow(renderScope, ctx, Textures.ICON_CHEVRON_RIGHT, width - 4 - ARROW, arrowY, canGoForward());
}
private void drawArrow(RenderScope renderScope, ElementContext ctx, IdentifierRef icon, int x, int y,
boolean enabled) {
boolean hovered = enabled && ctx.isHovering(x - 5, 0, ARROW_AREA, height);
int color = !enabled ? Theme.withAlpha(0.2f, Theme.FOREGROUND.value)
: hovered ? Theme.ACCENT.value : Theme.getMutedFg();
renderScope.drawTexture(icon, x, y, 0, 0, ARROW, ARROW, color);
}
private void drawCentered(RenderScope renderScope, String text, float scale, int y, int color) {
int x = (width - (int) (Fonts.getWidth(text, Theme.FONT.value) * scale)) / 2;
renderScope.drawText(scale, text, x, y, Theme.FONT.value, color);
}
@Override
public void click(int mouseX, int mouseY) {
if (mouseX < ARROW_AREA && canGoBack()) {
onPrevious.run();
} else if (mouseX > width - ARROW_AREA && canGoForward()) {
onNext.run();
}
}
private boolean canGoBack() {
return page > 0;
}
private boolean canGoForward() {
return page < pages - 1;
}
}
} }
@@ -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();
// Check if clicking the corner for resizing
if (isCorner(mouseX, mouseY, dim)) {
draggingMod = mod; draggingMod = mod;
resizing = true; resizing = isOnHandle(mouseX, mouseY, dim);
offsetX = (int) (mouseX - (dim.x.value + dim.width * dim.scale.value)); offsetX = (int) (mouseX - (resizing ? right(dim) : dim.x.value));
offsetY = (int) (mouseY - (dim.y.value + dim.height * dim.scale.value)); offsetY = (int) (mouseY - (resizing ? bottom(dim) : dim.y.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 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
@@ -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", () -> 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,72 @@ 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",
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",
() -> Providers.saturn.getClient().setScreen(MinecraftScreen.Multiplayer))
.dimensions(COLUMN_WIDTH, BUTTON_HEIGHT).position(0, rowHeight));
addMenuButton(menus, new Button("Options",
() -> Providers.saturn.getClient().setScreen(MinecraftScreen.Options))
.dimensions(halfWidth, BUTTON_HEIGHT).position(0, rowHeight * 2));
addMenuButton(menus, new Button("Quit",
() -> 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, () -> Providers.saturn.getClient().setScreen(new ModMenu())),
}).scale(0.69f).dimensions(206, 52).position(146, 80) new Dock.Item(Textures.ICON_COSMETICS,
.animation(new SlideFade(AnimationConfig.mainMenu, -10))); () -> Providers.saturn.getClient().setScreen(new CosmeticsMenu())),
new Dock.Item(Textures.ICON_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));
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" x2="9.01" y1="9" y2="9"/><line x1="15" x2="15.01" y1="9" y2="9"/></svg>

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 958 B