diff --git a/src/main/java/org/saturnclient/common/provider/SaturnProvider.java b/src/main/java/org/saturnclient/common/provider/SaturnProvider.java index 5efbf2e..4d63eec 100644 --- a/src/main/java/org/saturnclient/common/provider/SaturnProvider.java +++ b/src/main/java/org/saturnclient/common/provider/SaturnProvider.java @@ -22,6 +22,12 @@ public interface SaturnProvider { /** The Saturn Client mod version, e.g. {@code 0.1.0-beta3+1.21.11}. */ 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. */ public void showWarning(String title, String message); } diff --git a/src/main/java/org/saturnclient/ui/components/AccountChip.java b/src/main/java/org/saturnclient/ui/components/AccountChip.java new file mode 100644 index 0000000..554f9c6 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/AccountChip.java @@ -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)); + } +} diff --git a/src/main/java/org/saturnclient/ui/components/Dock.java b/src/main/java/org/saturnclient/ui/components/Dock.java new file mode 100644 index 0000000..7f6d881 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/Dock.java @@ -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 items; + + public Dock(List 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); + } +} diff --git a/src/main/java/org/saturnclient/ui/elements/Button.java b/src/main/java/org/saturnclient/ui/elements/Button.java index 3c0d5ed..714514e 100644 --- a/src/main/java/org/saturnclient/ui/elements/Button.java +++ b/src/main/java/org/saturnclient/ui/elements/Button.java @@ -6,24 +6,52 @@ import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.RenderScope; 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 { - private String text; - private Runnable onClick; + public static final int HEIGHT = 40; - 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.primary = primary; 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 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); + boolean hovered = ctx.isHovering(); + drawSurface(renderScope, width, height, primary, hovered); - renderScope.drawText(text, Fonts.centerX(width, text, Theme.FONT.value), Fonts.centerY(height), - Theme.FONT.value, - ctx.isHovering() ? Theme.ACCENT_FG.value : Theme.PRIMARY_FG.value); + int textX = (width - (int) (Fonts.getWidth(text, Theme.FONT.value) * TEXT_SCALE)) / 2; + renderScope.drawText(TEXT_SCALE, text, textX, Field.textY(height, TEXT_SCALE), Theme.FONT.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 diff --git a/src/main/java/org/saturnclient/ui/elements/IconButton.java b/src/main/java/org/saturnclient/ui/elements/IconButton.java new file mode 100644 index 0000000..764d297 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/elements/IconButton.java @@ -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(); + } +} diff --git a/src/main/java/org/saturnclient/ui/screens/ShiftMenu.java b/src/main/java/org/saturnclient/ui/screens/ShiftMenu.java index 2915e68..6b1770b 100644 --- a/src/main/java/org/saturnclient/ui/screens/ShiftMenu.java +++ b/src/main/java/org/saturnclient/ui/screens/ShiftMenu.java @@ -8,18 +8,24 @@ import org.saturnclient.ui.anim.Fade; import org.saturnclient.ui.anim.SlideY; import org.saturnclient.ui.elements.AnimationStagger; import org.saturnclient.ui.elements.Button; +import org.saturnclient.ui.elements.IconButton; import org.saturnclient.ui.elements.ImageTexture; -import org.saturnclient.ui.elements.TextureButton; 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 { + private static final int MODS_WIDTH = 120; + private static final int GAP = 6; + public ShiftMenu() { super("Shift Menu"); } @Override public void ui() { - // Logo animations (unchanged) draw(new ImageTexture(Textures.LOGO_TEXT) .dimensions(98, 10) .center(width, height, 0, -36) @@ -30,32 +36,24 @@ public class ShiftMenu extends SaturnScreen { .center(width, height, 0, -80) .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); + int x = 0; - stagger.draw(new TextureButton(Textures.HUD_ICON, () -> { - Providers.saturn.getClient().setScreen(new HudEditor()); - }).dimensions(btnHeight, btnHeight).position(0, 0) - .animation(new Fade(AnimationConfig.shiftMenu))); + stagger.draw(new IconButton(Textures.ICON_HUD_EDITOR, + () -> Providers.saturn.getClient().setScreen(new HudEditor())) + .position(x, 0).animation(new Fade(AnimationConfig.shiftMenu))); + x += Button.HEIGHT + GAP; - stagger.draw(new Button("Settings", () -> { - Providers.saturn.getClient().setScreen(new ModMenu()); - }).dimensions(btnWidth, btnHeight).position(btnHeight + spacing, 0) - .animation(new Fade(AnimationConfig.shiftMenu))); + stagger.draw(new Button("Mods", true, () -> Providers.saturn.getClient().setScreen(new ModMenu())) + .dimensions(MODS_WIDTH, Button.HEIGHT) + .position(x, 0).animation(new Fade(AnimationConfig.shiftMenu))); + x += MODS_WIDTH + GAP; - stagger.draw(new TextureButton(Textures.COSMETICS, () -> { - Providers.saturn.getClient().setScreen(new CosmeticsMenu()); - }).dimensions(btnHeight, btnHeight).position(btnWidth + btnHeight + (spacing * 2), 0) - .animation(new Fade(AnimationConfig.shiftMenu))); + stagger.draw(new IconButton(Textures.ICON_COSMETICS, + () -> Providers.saturn.getClient().setScreen(new CosmeticsMenu())) + .position(x, 0).animation(new Fade(AnimationConfig.shiftMenu))); + x += Button.HEIGHT; - // Draw the stagger at the absolute center of the screen - draw(stagger.dimensions(btnWidth + (btnHeight * 2) + (spacing * 2), btnHeight).center(width, height, 0, - 0)); + draw(stagger.position((width - x) / 2, (height - Button.HEIGHT) / 2)); } } diff --git a/src/main/java/org/saturnclient/ui/screens/TitleMenu.java b/src/main/java/org/saturnclient/ui/screens/TitleMenu.java index d134334..781a007 100644 --- a/src/main/java/org/saturnclient/ui/screens/TitleMenu.java +++ b/src/main/java/org/saturnclient/ui/screens/TitleMenu.java @@ -1,21 +1,40 @@ package org.saturnclient.ui.screens; +import java.util.List; + import org.saturnclient.common.provider.Providers; import org.saturnclient.common.ref.game.MinecraftClientRef.MinecraftScreen; import org.saturnclient.config.AnimationConfig; import org.saturnclient.config.Config; +import org.saturnclient.config.Theme; +import org.saturnclient.ui.Element; import org.saturnclient.ui.SaturnScreen; import org.saturnclient.ui.anim.Fade; import org.saturnclient.ui.anim.SlideFade; 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.Button; 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; +/** + * 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 { + 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() { super("Title Menu"); } @@ -24,53 +43,73 @@ public class TitleMenu extends SaturnScreen { public void ui() { blurBackground = false; - draw(new ImageTexture(Textures.LOGO_TEXT).dimensions(98, 10).center(width, height, 0, -40) - .animation(new Fade(AnimationConfig.logo.duration.value))); - draw(new ImageTexture(Config.getLogo()).dimensions(98, 98).center(width, height, 0, -84) + // Logo and menus, centered together a little above the middle + int menusHeight = BUTTON_HEIGHT * 3 + GAP * 2; + 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))); + 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", () -> { - Providers.saturn.getClient().setScreen(MinecraftScreen.SelectWorld); - }).scale(0.69f).dimensions(420, 52).position(0, 0) - .animation(new SlideFade(AnimationConfig.mainMenu, -10))); + addMenuButton(menus, new Button("Singleplayer", true, + () -> Providers.saturn.getClient().setScreen(MinecraftScreen.SelectWorld)) + .dimensions(COLUMN_WIDTH, BUTTON_HEIGHT).position(0, 0)); + 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", () -> { - Providers.saturn.getClient().setScreen(MinecraftScreen.Multiplayer); - }).scale(0.69f).dimensions(420, 52).position(0, 40) - .animation(new SlideFade(AnimationConfig.mainMenu, -10))); + draw(menus.position((width - COLUMN_WIDTH) / 2, top + LOGO + 16 + WORDMARK_HEIGHT + 36)); - mainButtonStagger.draw(new Button("OPTIONS", () -> { - Providers.saturn.getClient().setScreen(MinecraftScreen.Options); - }).scale(0.69f).dimensions(206, 52).position(0, 80) - .animation(new SlideFade(AnimationConfig.mainMenu, -10))); + // Along the bottom: the player, Saturn's screens, the version + draw(new AccountChip() + .position(EDGE, height - EDGE - AccountChip.HEIGHT) + .animation(new Fade(AnimationConfig.mainMenu))); - mainButtonStagger.draw(new Button("QUIT", () -> { - Providers.saturn.getClient().scheduleStop(); - }).scale(0.69f).dimensions(206, 52).position(146, 80) - .animation(new SlideFade(AnimationConfig.mainMenu, -10))); + Dock dock = new Dock(List.of( + new Dock.Item(Textures.ICON_MODS, "Mods", + () -> Providers.saturn.getClient().setScreen(new ModMenu())), + 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); - - 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)); + private static void addMenuButton(AnimationStagger stagger, Element button) { + stagger.draw(button.animation(new SlideFade(AnimationConfig.mainMenu, -10))); } }