Fix UI bugs and bring the remaining screens into the new style #7
@@ -8,6 +8,7 @@ import java.util.Optional;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.emotes.core.Emote;
|
||||
import org.saturnclient.emotes.core.EmoteRegistry;
|
||||
import org.saturnclient.ui.Utils;
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
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() {
|
||||
for (String id : IDS) {
|
||||
String path = "/assets/saturnclient/player_animations/" + id + ".json";
|
||||
|
||||
@@ -47,6 +47,24 @@ public class Utils {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <T> T getOrNull(List<T> list, int index) {
|
||||
return (index >= 0 && index < list.size()) ? list.get(index) : null;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
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;
|
||||
|
||||
public class Textures {
|
||||
@@ -43,6 +47,7 @@ public class Textures {
|
||||
.ofSaturn("textures/gui/components/chevron_right.svg");
|
||||
public static final IdentifierRef ICON_CHEVRON_DOWN = IdentifierRef
|
||||
.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 LOGO = IdentifierRef.ofSaturn("textures/logo/logo.png");
|
||||
@@ -61,6 +66,18 @@ public class Textures {
|
||||
public static final IdentifierRef MOD = IdentifierRef.ofSaturn("textures/gui/components/mod.png");
|
||||
public static final IdentifierRef MOD_BG = IdentifierRef.ofSaturn("textures/gui/components/mod_bg.png");
|
||||
|
||||
/**
|
||||
* Whether a texture file is in the resources, for falling back when an
|
||||
* optional one (like an emote's preview) is missing.
|
||||
*/
|
||||
public static boolean exists(IdentifierRef texture) {
|
||||
try (InputStream in = Providers.saturn.getClient().getResource(texture)) {
|
||||
return in != null;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static IdentifierRef getCloakPreview(String cloak) {
|
||||
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.PanelScreen;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.Utils;
|
||||
import org.saturnclient.ui.anim.SlideFade;
|
||||
import org.saturnclient.ui.components.CosmeticTile;
|
||||
import org.saturnclient.ui.components.Sidebar;
|
||||
@@ -193,18 +194,7 @@ public class CosmeticsMenu extends PanelScreen {
|
||||
|
||||
private static String getName(String id) {
|
||||
String name = NAMES.get(id);
|
||||
if (name != null) {
|
||||
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();
|
||||
return name != null ? name : Utils.nameFromId(id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,21 +1,43 @@
|
||||
package org.saturnclient.ui.screens;
|
||||
|
||||
import org.saturnclient.cosmetics.Emotes;
|
||||
import java.util.List;
|
||||
|
||||
import org.saturnclient.client.ServiceClient;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.config.AnimationConfig;
|
||||
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.Utils;
|
||||
import org.saturnclient.ui.anim.Fade;
|
||||
import org.saturnclient.ui.anim.SlideY;
|
||||
import org.saturnclient.ui.components.EmoteTile;
|
||||
import org.saturnclient.ui.elements.AnimationStagger;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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() {
|
||||
super("Emote Wheel");
|
||||
@@ -23,88 +45,131 @@ public class EmoteWheel extends SaturnScreen {
|
||||
|
||||
@Override
|
||||
public void ui() {
|
||||
int row = 0;
|
||||
int col = 0;
|
||||
|
||||
// =============================
|
||||
// Logo
|
||||
// =============================
|
||||
|
||||
draw(new ImageTexture(Textures.LOGO_TEXT)
|
||||
.dimensions(98, 10)
|
||||
.center(width, height, 0, -105)
|
||||
.animation(new Fade(AnimationConfig.logo.duration.value)));
|
||||
List<String> emotes = Emotes.availableEmotes;
|
||||
int left = (width - GRID) / 2;
|
||||
int top = (height - GRID) / 2 + LOGO / 2;
|
||||
|
||||
draw(new ImageTexture(Config.getLogo())
|
||||
.dimensions(98, 98)
|
||||
.center(width, height, 0, -149)
|
||||
.animation(new SlideY(AnimationConfig.logo, -20)));
|
||||
.dimensions(LOGO, LOGO)
|
||||
.position((width - LOGO) / 2, top - 20 - LOGO)
|
||||
.animation(new SlideY(AnimationConfig.logo, -12)));
|
||||
|
||||
// =============================
|
||||
// Navigation Buttons
|
||||
// =============================
|
||||
|
||||
draw(new TextureButton(Textures.LEFT, () -> {
|
||||
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);
|
||||
|
||||
emoteStagger.draw(
|
||||
new TextureButton(Textures.getEmotePreview(emote), () -> {
|
||||
Providers.saturn.playEmote(ServiceClient.uuid, emote);
|
||||
ServiceClient.emote(emote);
|
||||
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++;
|
||||
}
|
||||
if (emotes.isEmpty()) {
|
||||
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));
|
||||
return;
|
||||
}
|
||||
|
||||
draw(emoteStagger);
|
||||
int pages = (emotes.size() + SLOTS.length - 1) / SLOTS.length;
|
||||
page = Math.min(page, pages - 1);
|
||||
|
||||
AnimationStagger grid = new AnimationStagger(AnimationConfig.emoteWheel);
|
||||
|
||||
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);
|
||||
ServiceClient.emote(emote);
|
||||
provider.close();
|
||||
}
|
||||
|
||||
private void turnPage(int direction) {
|
||||
page += direction;
|
||||
init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldPause() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user