Compare commits
47
Commits
5df42e2b7c
...
e8f686b2d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8f686b2d3 | ||
|
|
df5afe99e3 | ||
|
|
653738568a | ||
|
|
a6a324a61a | ||
|
|
b0d75af8c4 | ||
|
|
c973e053aa | ||
|
|
bbf6b88ed3 | ||
|
|
ba71a5b838 | ||
|
|
6d5df3057e | ||
|
|
2fceb58024 | ||
|
|
7e98bbcd6c | ||
|
|
9ede612089 | ||
|
|
ed2914aaf7 | ||
|
|
69c830d466 | ||
|
|
7d0bfd26c9 | ||
|
|
7cc37b7c74 | ||
|
|
524fc36bf3 | ||
|
|
7bc308155e | ||
|
|
4d9c629eaf | ||
|
|
5c0912f0cf | ||
|
|
3436054112 | ||
|
|
adedd688a7 | ||
|
|
a7ad458627 | ||
|
|
320b2fd9a5 | ||
|
|
a7e3e537b1 | ||
|
|
ef0ead6bf8 | ||
|
|
136621cc8d | ||
|
|
25d288da92 | ||
|
|
f0971ff128 | ||
|
|
2f4ea77e6f | ||
|
|
05dbec028a | ||
|
|
caf472da96 | ||
|
|
c3667f8f66 | ||
|
|
85c7057ba1 | ||
|
|
55b78989ce | ||
|
|
8b58fe919d | ||
|
|
6bfdfc40c9 | ||
|
|
351507d60d | ||
|
|
33c09b6922 | ||
|
|
06a90d8e51 | ||
|
|
7264c7b38b | ||
|
|
a413648be9 | ||
|
|
df09271e81 | ||
|
|
dfebd2367d | ||
|
|
4ac7c60d07 | ||
|
|
15fa973553 | ||
|
|
b2cf0cbc38 |
@@ -8,4 +8,5 @@ java {
|
||||
|
||||
dependencies {
|
||||
implementation "dev.selimaj:session-java:0.2.0"
|
||||
implementation "org.saturnclient:emotes-core:${emotes_version}"
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package org.saturnclient.common.ref.game;
|
||||
|
||||
/**
|
||||
* An item stack. Its methods are implemented in ItemStackMixin, so their names must not match any of
|
||||
* ItemStack's own methods.
|
||||
*/
|
||||
public interface ItemStackRef {
|
||||
public boolean isEmpty();
|
||||
public int getMaxDamage();
|
||||
public int getDamage();
|
||||
public boolean isEmptyStack();
|
||||
public int getMaxItemDamage();
|
||||
public int getItemDamage();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package org.saturnclient.common.ref.render;
|
||||
|
||||
/**
|
||||
* The game window. Its methods are implemented in WindowMixin, so their names must not match any of
|
||||
* Window's own methods.
|
||||
*/
|
||||
public interface WindowRef {
|
||||
public int getWidth();
|
||||
/** The window's width in screen coordinates */
|
||||
public int getWindowWidth();
|
||||
|
||||
public int getHeight();
|
||||
/** The window's height in screen coordinates */
|
||||
public int getWindowHeight();
|
||||
|
||||
public int getFramebufferWidth();
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package org.saturnclient.config;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.saturnclient.config.property.CurveProperty;
|
||||
import org.saturnclient.config.property.IntProperty;
|
||||
import org.saturnclient.config.property.Property;
|
||||
import org.saturnclient.config.property.SelectProperty;
|
||||
|
||||
public class AnimationConfig {
|
||||
public static ConfigManager config;
|
||||
@@ -25,6 +29,10 @@ public class AnimationConfig {
|
||||
// Emote wheel
|
||||
public static final AnimationConfig emoteWheel = new AnimationConfig(true, 700, 40);
|
||||
|
||||
/** Every menu's animation, in the order they're shown in the settings */
|
||||
public static final List<AnimationConfig> ALL = List.of(logo, mainMenu, modMenu, shiftMenu, cosmeticsMenu,
|
||||
emoteWheel);
|
||||
|
||||
public static void init(ConfigManager parent) {
|
||||
config = new ConfigManager(parent, "Animations");
|
||||
|
||||
@@ -39,18 +47,36 @@ public class AnimationConfig {
|
||||
// Animation properties
|
||||
public final IntProperty duration;
|
||||
public final IntProperty stagger;
|
||||
public final SelectProperty curve;
|
||||
public final CurveProperty curve;
|
||||
|
||||
public AnimationConfig(boolean enabled, int delay, int stagger) {
|
||||
this.duration = Property.integer(delay);
|
||||
this.stagger = Property.integer(stagger);
|
||||
this.curve = Property.select(1, "Ease In Out Cubic", "Ease Out Cubic", "Ease In Out Back");
|
||||
this.duration = Property.integer(delay).range(0, 2000, 50).unit("ms");
|
||||
this.stagger = Property.integer(stagger).range(0, 300, 10).unit("ms");
|
||||
this.curve = Property.curve(1);
|
||||
}
|
||||
|
||||
/** The menu this animates, like "Mod Menu" */
|
||||
public String name;
|
||||
|
||||
/**
|
||||
* This animation's settings under short names ("Duration"), for showing
|
||||
* them grouped under {@link #name}. They're saved under longer names
|
||||
* ("Mod Menu Duration"), which stay as they are so saved settings keep
|
||||
* loading.
|
||||
*/
|
||||
public Map<String, Property> getProperties() {
|
||||
Map<String, Property> properties = new LinkedHashMap<>();
|
||||
properties.put("Duration", duration);
|
||||
properties.put("Stagger", stagger);
|
||||
properties.put("Curve", curve);
|
||||
return properties;
|
||||
}
|
||||
|
||||
public void init(String name) {
|
||||
this.name = name;
|
||||
// config.property("Enable " + name + " Animations", this.enabled);
|
||||
config.property(name + " Duration", this.duration);
|
||||
config.property(name + " Stagger", this.stagger);
|
||||
config.property(name + " Curve", this.curve);
|
||||
config.property(name + " Duration", this.duration, "How long the animation takes");
|
||||
config.property(name + " Stagger", this.stagger, "Delay between items appearing one after another");
|
||||
config.property(name + " Curve", this.curve, "Easing of the animation");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import org.saturnclient.common.provider.GLFWProvider;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.config.property.BoolProperty;
|
||||
import org.saturnclient.config.property.FloatProperty;
|
||||
import org.saturnclient.config.property.IntProperty;
|
||||
import org.saturnclient.config.property.KeybindingProperty;
|
||||
import org.saturnclient.config.property.Property;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
@@ -17,8 +19,33 @@ public class Config {
|
||||
public static BoolProperty saturnTitleScreen = Property.bool(true);
|
||||
public static BoolProperty cloakPhysics = Property.bool(true);
|
||||
public static BoolProperty stagger = Property.bool(true);
|
||||
/** The world or panorama behind Saturn's screens, faded in as they open */
|
||||
public static IntProperty backgroundBlur = Property.integer(5).range(0, 10);
|
||||
/** Glass behind translucent menus (panels), and widgets (buttons, the dock) */
|
||||
public static IntProperty menuBlur = Property.integer(8).range(0, 20);
|
||||
public static IntProperty widgetBlur = Property.integer(6).range(0, 20);
|
||||
// How big Saturn's screens are, relative to Minecraft's GUI scale
|
||||
public static FloatProperty titleScale = guiScale();
|
||||
public static FloatProperty menuScale = guiScale();
|
||||
public static FloatProperty hudMenuScale = guiScale();
|
||||
|
||||
/** Added in 0.1.3, so a config without it was saved by an older version */
|
||||
static final String MENU_BLUR = "Menu blur";
|
||||
public static KeybindingProperty openEmoteWheel = Property.keybinding(GLFWProvider.GLFW_KEY_B);
|
||||
|
||||
private static FloatProperty guiScale() {
|
||||
return Property.floatProp(1f).range(0.5f, 2f, 0.05f).unit("×");
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any glass can be drawn: a glass blur is on, and a background it
|
||||
* goes behind isn't solid.
|
||||
*/
|
||||
public static boolean usesGlass() {
|
||||
boolean translucent = Theme.getPanelBg() >>> 24 < 0xFF || Theme.PRIMARY.value >>> 24 < 0xFF;
|
||||
return translucent && (menuBlur.value > 0 || widgetBlur.value > 0);
|
||||
}
|
||||
|
||||
public static IdentifierRef getLogo() {
|
||||
return realisticLogo.value ? Textures.REALISTIC_LOGO : Textures.LOGO;
|
||||
}
|
||||
@@ -27,11 +54,17 @@ public class Config {
|
||||
File configFile = new File(Providers.saturn.getClient().getRunDirectory(), "saturn.json");
|
||||
config = new ConfigManager(configFile, "Saturn Client");
|
||||
|
||||
config.property("Realistic logo", realisticLogo);
|
||||
config.property("Saturn client title screen", saturnTitleScreen);
|
||||
config.property("Open Emote Wheel", openEmoteWheel);
|
||||
config.property("Cloak Physics", cloakPhysics);
|
||||
config.property("Stagger Animations", stagger);
|
||||
config.property("Realistic logo", realisticLogo, "Use the realistic Saturn logo");
|
||||
config.property("Saturn client title screen", saturnTitleScreen, "Replace the title screen with Saturn's");
|
||||
config.property("Open Emote Wheel", openEmoteWheel, "The key that opens the emote wheel");
|
||||
config.property("Cloak Physics", cloakPhysics, "Let cloaks swing as you move");
|
||||
config.property("Stagger Animations", stagger, "Animate list items one after another");
|
||||
config.property("World blur", backgroundBlur, "How much the world or panorama behind Saturn's screens is blurred");
|
||||
config.property(MENU_BLUR, menuBlur, "How much the game is blurred through translucent menus");
|
||||
config.property("Widget blur", widgetBlur, "How much the game is blurred through translucent buttons and the dock");
|
||||
config.property("Title screen scale", titleScale, "How big the title screen is, relative to Minecraft's GUI scale");
|
||||
config.property("Menu scale", menuScale, "How big the mods, settings and cosmetics menus are, relative to Minecraft's GUI scale");
|
||||
config.property("HUD menu scale", hudMenuScale, "How big the emote wheel and shift menu are, relative to Minecraft's GUI scale");
|
||||
|
||||
// Initialize a sub namespace for theme
|
||||
Theme.init(config);
|
||||
|
||||
@@ -70,6 +70,14 @@ public class ConfigManager {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a property with the line shown under its name
|
||||
*/
|
||||
public Property property(String name, Property value, String description) {
|
||||
value.description = description;
|
||||
return property(name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add empty namespace
|
||||
*/
|
||||
@@ -148,10 +156,17 @@ public class ConfigManager {
|
||||
* Load only a single property immediately after creation
|
||||
*/
|
||||
private void loadSingleProperty(String name, Property prop) {
|
||||
JsonNode element = getSaved(name);
|
||||
|
||||
if (element != null) {
|
||||
prop.loadFromJson(element);
|
||||
}
|
||||
}
|
||||
|
||||
private JsonNode getSaved(String name) {
|
||||
JsonNode source = cachedJson != null ? cachedJson : loadAndCache();
|
||||
if (source == null || !source.isObject())
|
||||
return;
|
||||
return null;
|
||||
|
||||
JsonNode currentScope = source;
|
||||
|
||||
@@ -162,16 +177,12 @@ public class ConfigManager {
|
||||
if (next != null && next.isObject()) {
|
||||
currentScope = next;
|
||||
} else {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
JsonNode element = currentScope.get(name);
|
||||
|
||||
if (element != null) {
|
||||
prop.loadFromJson(element);
|
||||
}
|
||||
return currentScope.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
package org.saturnclient.config;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.saturnclient.config.property.ColorProperty;
|
||||
import org.saturnclient.config.property.IntProperty;
|
||||
import org.saturnclient.config.property.Property;
|
||||
import org.saturnclient.config.property.SelectProperty;
|
||||
|
||||
public class Theme {
|
||||
// Main UI background (slightly warm dark to match Saturn tone)
|
||||
public static final ColorProperty BACKGROUND = Property.color(0xFF12100D);
|
||||
public static ConfigManager config;
|
||||
|
||||
// Main UI background (slightly warm dark to match Saturn tone), translucent over the world
|
||||
public static final ColorProperty BACKGROUND = Property.color(0xD112100D);
|
||||
|
||||
// Default text color
|
||||
public static final ColorProperty FOREGROUND = Property.color(0xFFF2E6D2);
|
||||
|
||||
// Widget default background (normal state)
|
||||
public static final ColorProperty PRIMARY = Property.color(0xFF1C1915);
|
||||
// Widget default background (normal state), translucent over the world
|
||||
public static final ColorProperty PRIMARY = Property.color(0xB81C1915);
|
||||
|
||||
// Widget text color
|
||||
public static final ColorProperty PRIMARY_FG = Property.color(0xFFEAD9B8);
|
||||
@@ -28,26 +32,55 @@ public class Theme {
|
||||
public static final ColorProperty SCROLL = Property.color(0xFFE2BF7D);
|
||||
|
||||
// UI metrics
|
||||
public static final IntProperty BG_RADIUS = Property.integer(12);
|
||||
public static final IntProperty WIDGET_RADIUS = Property.integer(10);
|
||||
public static final IntProperty BG_RADIUS = Property.integer(12).range(0, 24);
|
||||
public static final IntProperty WIDGET_RADIUS = Property.integer(10).range(0, 20);
|
||||
|
||||
// Font
|
||||
public static final SelectProperty FONT = Property.font(2);
|
||||
|
||||
/**
|
||||
* A full set of theme colors that can be applied in one go.
|
||||
*/
|
||||
public record Preset(String name, int background, int foreground, int primary, int primaryFg, int accent,
|
||||
int accentFg, int scroll) {
|
||||
|
||||
public void apply() {
|
||||
BACKGROUND.value = background;
|
||||
FOREGROUND.value = foreground;
|
||||
PRIMARY.value = primary;
|
||||
PRIMARY_FG.value = primaryFg;
|
||||
ACCENT.value = accent;
|
||||
ACCENT_FG.value = accentFg;
|
||||
SCROLL.value = scroll;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return BACKGROUND.value == background && FOREGROUND.value == foreground && PRIMARY.value == primary
|
||||
&& PRIMARY_FG.value == primaryFg && ACCENT.value == accent && ACCENT_FG.value == accentFg
|
||||
&& SCROLL.value == scroll;
|
||||
}
|
||||
}
|
||||
|
||||
public static final List<Preset> PRESETS = List.of(
|
||||
new Preset("Saturn", 0xD112100D, 0xFFF2E6D2, 0xB81C1915, 0xFFEAD9B8, 0xFFE2BF7D, 0xFF1A140A, 0xFFE2BF7D),
|
||||
new Preset("Midnight", 0xD10E1117, 0xFFE4EAF5, 0xB8171C26, 0xFFC9D4E8, 0xFF8FB3FF, 0xFF0A1224, 0xFF8FB3FF),
|
||||
new Preset("Amethyst", 0xD1120F17, 0xFFEDE4F7, 0xB81C1724, 0xFFD8C9EB, 0xFFC79BFF, 0xFF1A0F2A, 0xFFC79BFF),
|
||||
new Preset("Emerald", 0xD10D1210, 0xFFE2F2EA, 0xB816201B, 0xFFC4E0D2, 0xFF7FD1A8, 0xFF0A1F15, 0xFF7FD1A8));
|
||||
|
||||
public static void init(ConfigManager parent) {
|
||||
ConfigManager config = new ConfigManager(parent, "UI Style");
|
||||
config.property("Background", BACKGROUND);
|
||||
config.property("Foreground", FOREGROUND);
|
||||
config.property("Primary", PRIMARY);
|
||||
config.property("Primary Foreground", PRIMARY_FG);
|
||||
config.property("Accent", ACCENT);
|
||||
config.property("Accent Foreground", ACCENT_FG);
|
||||
config.property("Scrollbar Color", SCROLL);
|
||||
config = new ConfigManager(parent, "UI Style");
|
||||
config.property("Background", BACKGROUND, "Panels and screens");
|
||||
config.property("Foreground", FOREGROUND, "Text");
|
||||
config.property("Primary", PRIMARY, "Buttons and cards");
|
||||
config.property("Primary Foreground", PRIMARY_FG, "Text on buttons and cards");
|
||||
config.property("Accent", ACCENT, "Selected and enabled items");
|
||||
config.property("Accent Foreground", ACCENT_FG, "Text on the accent color");
|
||||
config.property("Scrollbar Color", SCROLL, "The scrollbar's handle");
|
||||
|
||||
config.property("Background Radius", BG_RADIUS);
|
||||
config.property("Widget Radius", WIDGET_RADIUS);
|
||||
config.property("Background Radius", BG_RADIUS, "Corner rounding of panels");
|
||||
config.property("Widget Radius", WIDGET_RADIUS, "Corner rounding of buttons and cards");
|
||||
|
||||
config.property("Font", FONT);
|
||||
config.property("Font", FONT, "Font of every Saturn screen");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,6 +97,44 @@ public class Theme {
|
||||
return hover ? ACCENT_FG.value : PRIMARY_FG.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Background for panels drawn over the world, as translucent as the
|
||||
* background color is
|
||||
*/
|
||||
public static int getPanelBg() {
|
||||
return BACKGROUND.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Background for popups, which sit over other panels: the background color,
|
||||
* kept nearly solid so what's under it doesn't show through
|
||||
*/
|
||||
public static int getPopupBg() {
|
||||
return withAlpha(0.97f, BACKGROUND.value | 0xFF000000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Faint outline for panels, cards and inputs
|
||||
*/
|
||||
public static int getBorder() {
|
||||
return withAlpha(0.08f, FOREGROUND.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Secondary text: descriptions, counts and placeholders
|
||||
*/
|
||||
public static int getMutedFg() {
|
||||
return withAlpha(0.65f, FOREGROUND.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Font for secondary text. Inter bold is too heavy there, so it becomes
|
||||
* regular Inter; other fonts are kept.
|
||||
*/
|
||||
public static int getBodyFont() {
|
||||
return FONT.value == 2 ? 1 : FONT.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies existing alpha with 0–255 alpha.
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,12 @@ public class BoolProperty extends Property {
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.value = defaultValue;
|
||||
this.isReset = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode toJson() {
|
||||
return JsonNodeFactory.instance.booleanNode(this.value);
|
||||
|
||||
@@ -12,6 +12,12 @@ public class ColorProperty extends Property {
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.value = defaultValue;
|
||||
this.isReset = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode toJson() {
|
||||
return JsonNodeFactory.instance.textNode(
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.saturnclient.config.property;
|
||||
|
||||
/**
|
||||
* An animation's easing curve. The options are in the order
|
||||
* {@code Animation.getCurve} takes them, and it's saved like any select.
|
||||
*/
|
||||
public class CurveProperty extends SelectProperty {
|
||||
|
||||
public CurveProperty(int defaultValue) {
|
||||
super(defaultValue, "Ease In Out Cubic", "Ease Out Cubic", "Ease In Out Back");
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,42 @@ public class FloatProperty extends Property {
|
||||
public float value;
|
||||
public final float defaultValue;
|
||||
|
||||
/** Bounds for a slider, when the setting has a sensible range */
|
||||
public Float min, max;
|
||||
public float step = 0.1f;
|
||||
/** Shown after the value, like "×" */
|
||||
public String unit = "";
|
||||
|
||||
public FloatProperty(float defaultValue) {
|
||||
this.value = defaultValue;
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives the setting a range, so it's edited with a slider.
|
||||
*/
|
||||
public FloatProperty range(float min, float max, float step) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.step = step;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FloatProperty unit(String unit) {
|
||||
this.unit = unit;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasRange() {
|
||||
return min != null && max != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.value = defaultValue;
|
||||
this.isReset = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode toJson() {
|
||||
return JsonNodeFactory.instance.numberNode(this.value);
|
||||
|
||||
@@ -7,11 +7,46 @@ public class IntProperty extends Property {
|
||||
public int value;
|
||||
public final int defaultValue;
|
||||
|
||||
/** Bounds for a slider, when the setting has a sensible range */
|
||||
public Integer min, max;
|
||||
public int step = 1;
|
||||
/** Shown after the value, like "ms" */
|
||||
public String unit = "";
|
||||
|
||||
public IntProperty(int defaultValue) {
|
||||
this.value = defaultValue;
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives the setting a range, so it's edited with a slider.
|
||||
*/
|
||||
public IntProperty range(int min, int max) {
|
||||
return range(min, max, 1);
|
||||
}
|
||||
|
||||
public IntProperty range(int min, int max, int step) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.step = step;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IntProperty unit(String unit) {
|
||||
this.unit = unit;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasRange() {
|
||||
return min != null && max != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.value = defaultValue;
|
||||
this.isReset = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode toJson() {
|
||||
return JsonNodeFactory.instance.numberNode(this.value);
|
||||
|
||||
@@ -15,6 +15,12 @@ public class KeybindingProperty extends Property {
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.value = defaultValue;
|
||||
this.isReset = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode toJson() {
|
||||
return JsonNodeFactory.instance.numberNode(this.value);
|
||||
|
||||
@@ -8,4 +8,12 @@ public class NamedProperty {
|
||||
this.name = name;
|
||||
this.prop = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the line shown under the setting's name.
|
||||
*/
|
||||
public NamedProperty description(String description) {
|
||||
this.prop.description = description;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,11 @@ public class NamespaceProperty extends Property {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.value.values().forEach(Property::reset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode toJson() {
|
||||
ObjectNode nested = JsonNodeFactory.instance.objectNode();
|
||||
|
||||
@@ -7,6 +7,9 @@ import java.util.Map;
|
||||
public abstract class Property {
|
||||
public boolean isReset = false;
|
||||
|
||||
/** One line under the setting's name saying what it does, or "" */
|
||||
public String description = "";
|
||||
|
||||
// ---------- Factory Methods ----------
|
||||
|
||||
public static NamespaceProperty namespace(Map<String, Property> value) {
|
||||
@@ -41,6 +44,10 @@ public abstract class Property {
|
||||
return select(value, "Default", "Inter", "Inter bold");
|
||||
}
|
||||
|
||||
public static CurveProperty curve(int value) {
|
||||
return new CurveProperty(value);
|
||||
}
|
||||
|
||||
public static SelectProperty select(Integer value, String... availableValues) {
|
||||
return new SelectProperty(value, availableValues);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@ public class SelectProperty extends Property {
|
||||
this.availableValues = availableValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.value = defaultValue;
|
||||
this.isReset = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode toJson() {
|
||||
return JsonNodeFactory.instance.numberNode(this.value);
|
||||
|
||||
@@ -12,6 +12,12 @@ public class StringProperty extends Property {
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.value = defaultValue;
|
||||
this.isReset = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode toJson() {
|
||||
return JsonNodeFactory.instance.textNode(this.value);
|
||||
|
||||
@@ -38,7 +38,14 @@ public class Cloaks {
|
||||
availableCloaks.add(0, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* The cloak's texture, or null for no cloak ("", the "None" choice) and for a cloak this version
|
||||
* has no texture for, which would otherwise draw as the missing texture.
|
||||
*/
|
||||
public static IdentifierRef getCurrentCloakTexture(String cloakName) {
|
||||
if (cloakName == null || !Arrays.asList(ALL_CLOAKS).contains(cloakName)) {
|
||||
return null;
|
||||
}
|
||||
if (Arrays.asList(ANIMATED_CLOAKS).contains(cloakName)) {
|
||||
return IdentifierRef.ofSaturn("textures/cloaks/" + cloakName + ".gif");
|
||||
} else {
|
||||
|
||||
@@ -1,17 +1,53 @@
|
||||
package org.saturnclient.cosmetics;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
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
|
||||
* format). They live in Saturn's own registry rather than the emote library's shared one, so another
|
||||
* mod using the library can't replace them.
|
||||
*/
|
||||
public class Emotes {
|
||||
private static final List<String> IDS = List.of("tpose", "back_flip", "facepalm", "cry", "giga_chad",
|
||||
"twerk", "get_the_griddy", "take_the_l", "dab", "wave", "salute", "shrug", "67");
|
||||
|
||||
private static final EmoteRegistry REGISTRY = new EmoteRegistry();
|
||||
|
||||
/** 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() {
|
||||
availableEmotes.add("tpose");
|
||||
availableEmotes.add("back_flip");
|
||||
availableEmotes.add("facepalm");
|
||||
availableEmotes.add("cry");
|
||||
availableEmotes.add("giga_chad");
|
||||
availableEmotes.add("front_flip");
|
||||
for (String id : IDS) {
|
||||
String path = "/assets/saturnclient/player_animations/" + id + ".json";
|
||||
try (InputStream in = Emotes.class.getResourceAsStream(path)) {
|
||||
if (in == null) {
|
||||
Providers.saturn.logError("Missing emote " + path);
|
||||
continue;
|
||||
}
|
||||
REGISTRY.loadEmotecraft(id, in);
|
||||
availableEmotes.add(id);
|
||||
} catch (Exception e) {
|
||||
Providers.saturn.logError("Failed to load emote " + id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Optional<Emote> get(String id) {
|
||||
return REGISTRY.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.saturnclient.mod;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.saturnclient.config.property.Property;
|
||||
@@ -12,10 +13,10 @@ import org.saturnclient.config.property.NamedProperty;
|
||||
public class ModLayout {
|
||||
public IntProperty x = Property.integer(0);
|
||||
public IntProperty y = Property.integer(0);
|
||||
public FloatProperty scale = Property.floatProp(1.0f);
|
||||
public FloatProperty scale = Property.floatProp(1.0f).range(0.5f, 4f, 0.1f).unit("×");
|
||||
public ColorProperty bgColor = Property.color(0x00000000);
|
||||
public ColorProperty fgColor = Property.color(0xFFffffff);
|
||||
public IntProperty radius = Property.integer(0);
|
||||
public IntProperty radius = Property.integer(0).range(0, 20);
|
||||
public SelectProperty font = Property.font(0);
|
||||
public boolean renderBackground = true;
|
||||
|
||||
@@ -23,14 +24,22 @@ public class ModLayout {
|
||||
public int height = 0;
|
||||
|
||||
public NamedProperty prop() {
|
||||
return Property.namespace(Map.of(
|
||||
"X", x,
|
||||
"Y", y,
|
||||
"Scale", scale,
|
||||
"Background Color", bgColor,
|
||||
"Foreground Color", fgColor,
|
||||
"Corner Radius", radius,
|
||||
"Font", font)).named("In-Game Display");
|
||||
// A LinkedHashMap, unlike Map.of, keeps the order the settings are shown in.
|
||||
Map<String, Property> props = new LinkedHashMap<>();
|
||||
put(props, "X", x, "Distance from the left of the screen");
|
||||
put(props, "Y", y, "Distance from the top of the screen");
|
||||
put(props, "Scale", scale, "Size of the display");
|
||||
put(props, "Background Color", bgColor, "Fill behind the display");
|
||||
put(props, "Foreground Color", fgColor, "Color of the text");
|
||||
put(props, "Corner Radius", radius, "Rounding of the background's corners");
|
||||
put(props, "Font", font, "Font of the text");
|
||||
|
||||
return Property.namespace(props).named("In-Game Display");
|
||||
}
|
||||
|
||||
private static void put(Map<String, Property> props, String name, Property prop, String description) {
|
||||
prop.description = description;
|
||||
props.put(name, prop);
|
||||
}
|
||||
|
||||
public ModLayout() {
|
||||
|
||||
@@ -32,7 +32,7 @@ public class ArmorDisplayMod extends Mod implements HudMod {
|
||||
.tags("Utility")
|
||||
.requires(Providers.feature::player),
|
||||
enabled.named("Enabled"),
|
||||
useMainHand.named("Use Main Hand"),
|
||||
useMainHand.named("Use Main Hand").description("Also show the item in your main hand"),
|
||||
layout.prop());
|
||||
}
|
||||
|
||||
@@ -75,16 +75,16 @@ public class ArmorDisplayMod extends Mod implements HudMod {
|
||||
|
||||
int row = 0;
|
||||
|
||||
if (useMainHand.value && !mainHand.isEmpty()) {
|
||||
if (useMainHand.value && !mainHand.isEmptyStack()) {
|
||||
row = renderSlot(scope, mainHand, row);
|
||||
}
|
||||
if (!helmet.isEmpty())
|
||||
if (!helmet.isEmptyStack())
|
||||
row = renderSlot(scope, helmet, row);
|
||||
if (!chestplate.isEmpty())
|
||||
if (!chestplate.isEmptyStack())
|
||||
row = renderSlot(scope, chestplate, row);
|
||||
if (!leggings.isEmpty())
|
||||
if (!leggings.isEmptyStack())
|
||||
row = renderSlot(scope, leggings, row);
|
||||
if (!boots.isEmpty())
|
||||
if (!boots.isEmptyStack())
|
||||
row = renderSlot(scope, boots, row);
|
||||
}
|
||||
|
||||
@@ -94,8 +94,8 @@ public class ArmorDisplayMod extends Mod implements HudMod {
|
||||
private int renderSlot(RenderScope scope, ItemStackRef item, int row) {
|
||||
scope.drawItem(item, 0, 15 * row);
|
||||
|
||||
if (item.getMaxDamage() > 0) {
|
||||
int remaining = item.getMaxDamage() - item.getDamage();
|
||||
if (item.getMaxItemDamage() > 0) {
|
||||
int remaining = item.getMaxItemDamage() - item.getItemDamage();
|
||||
scope.drawText(0.5f, String.valueOf(remaining),
|
||||
17, (15 * row) + 3,
|
||||
layout.font.value, layout.fgColor.value);
|
||||
|
||||
@@ -31,8 +31,8 @@ public class ClockMod extends Mod implements HudMod {
|
||||
.version("v0.2.0")
|
||||
.tags("Utility"),
|
||||
enabled.named("Enabled"),
|
||||
format.named("Format"),
|
||||
showSeconds.named("Show seconds"),
|
||||
format.named("Format").description("24-hour or 12-hour time"),
|
||||
showSeconds.named("Show seconds").description("Include seconds in the time"),
|
||||
layout.prop());
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ public class CoordinatesMod extends Mod implements HudMod {
|
||||
.tags("Utility")
|
||||
.requires(Providers.feature::player),
|
||||
enabled.named("Enabled"),
|
||||
displayMethod.named("Display Method"),
|
||||
displayMethod.named("Display Method").description("One line or one per axis, with or without labels"),
|
||||
layout.prop());
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ import java.util.function.Function;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.common.provider.GLFWProvider;
|
||||
import org.saturnclient.config.property.BoolProperty;
|
||||
import org.saturnclient.config.property.CurveProperty;
|
||||
import org.saturnclient.config.property.FloatProperty;
|
||||
import org.saturnclient.config.property.IntProperty;
|
||||
import org.saturnclient.config.property.KeybindingProperty;
|
||||
import org.saturnclient.config.property.Property;
|
||||
import org.saturnclient.config.property.SelectProperty;
|
||||
import org.saturnclient.mod.Mod;
|
||||
import org.saturnclient.mod.ModSpec;
|
||||
import org.saturnclient.common.feature.RenderFeature;
|
||||
@@ -22,10 +22,10 @@ public class FreelookMod extends Mod {
|
||||
private static final BoolProperty toggle = Property.bool(true);
|
||||
private static final KeybindingProperty freelookKey = Property.keybinding(GLFWProvider.GLFW_KEY_H);
|
||||
|
||||
public static final FloatProperty zoomLevel = Property.floatProp(2.0f);
|
||||
public static final IntProperty zoomInDuration = Property.integer(500);
|
||||
public static final IntProperty zoomOutDuration = Property.integer(300);
|
||||
public static final SelectProperty curve = Property.select(1, "Ease In Out Cubic", "Ease Out Cubic", "Ease In Out Back");
|
||||
public static final FloatProperty zoomLevel = Property.floatProp(2.0f).range(1f, 5f, 0.1f).unit("×");
|
||||
public static final IntProperty zoomInDuration = Property.integer(500).range(0, 2000, 50).unit("ms");
|
||||
public static final IntProperty zoomOutDuration = Property.integer(300).range(0, 2000, 50).unit("ms");
|
||||
public static final CurveProperty curve = Property.curve(1);
|
||||
|
||||
// --- State Variables ---
|
||||
private static boolean isFreeLooking = false;
|
||||
@@ -43,12 +43,12 @@ public class FreelookMod extends Mod {
|
||||
.version("v0.3.1")
|
||||
.requires(Providers.feature::render),
|
||||
enabled.named("Enabled"),
|
||||
toggle.named("Toggle freelook"),
|
||||
freelookKey.named("Freelook Keybinding"),
|
||||
zoomLevel.named("Zoom Level"),
|
||||
zoomInDuration.named("Zoom In Duration"),
|
||||
zoomOutDuration.named("Zoom Out Duration"),
|
||||
curve.named("Curve"));
|
||||
toggle.named("Toggle freelook").description("Press the key once instead of holding it"),
|
||||
freelookKey.named("Freelook Keybinding").description("The key that starts freelook"),
|
||||
zoomLevel.named("Zoom Level").description("Zoom when freelook starts in first person"),
|
||||
zoomInDuration.named("Zoom In Duration").description("How long that zoom takes to ease in"),
|
||||
zoomOutDuration.named("Zoom Out Duration").description("How long that zoom takes to ease out"),
|
||||
curve.named("Curve").description("Easing of the zoom"));
|
||||
}
|
||||
|
||||
// --- Lifecycle Methods ---
|
||||
|
||||
@@ -19,7 +19,7 @@ import org.saturnclient.mod.ModSpec;
|
||||
public class FullbrightMod extends Mod {
|
||||
|
||||
private static final BoolProperty enabled = Property.bool(false);
|
||||
private static final IntProperty brightness = Property.integer(100);
|
||||
private static final IntProperty brightness = Property.integer(100).range(0, 1000, 10);
|
||||
|
||||
public FullbrightMod() {
|
||||
super(
|
||||
@@ -28,7 +28,7 @@ public class FullbrightMod extends Mod {
|
||||
.tags("Camera")
|
||||
.version("v0.2.0"),
|
||||
enabled.named("Enabled"),
|
||||
brightness.named("Brightness %"));
|
||||
brightness.named("Brightness %").description("How bright the world gets"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@@ -31,8 +31,8 @@ public class HealthDisplayMod extends Mod implements HudMod {
|
||||
.tags("Utility")
|
||||
.requires(Providers.feature::player),
|
||||
enabled.named("Enabled"),
|
||||
displayMode.named("Display mode"),
|
||||
decimals.named("Decimals"),
|
||||
displayMode.named("Display mode").description("Show health points or hearts"),
|
||||
decimals.named("Decimals").description("Decimal places shown"),
|
||||
layout.prop());
|
||||
}
|
||||
|
||||
|
||||
@@ -54,11 +54,11 @@ public class KeystrokesMod extends Mod implements HudMod {
|
||||
.tags("Utility")
|
||||
.requires(Providers.feature::player),
|
||||
enabled.named("Enabled"),
|
||||
showMouse.named("Show mouse clicks"),
|
||||
showSpace.named("Show space"),
|
||||
showMouse.named("Show mouse clicks").description("Show the left and right mouse buttons"),
|
||||
showSpace.named("Show space").description("Show the space bar"),
|
||||
layout.prop(),
|
||||
clickFg.named("Clicked fg"),
|
||||
clickBg.named("Clicked bg"));
|
||||
clickFg.named("Clicked fg").description("Text color of a pressed key"),
|
||||
clickBg.named("Clicked bg").description("Background of a pressed key"));
|
||||
layout.renderBackground = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,16 +58,16 @@ public class NametagsMod extends Mod {
|
||||
.tags("Camera", "Utility")
|
||||
.version("v0.2.0"),
|
||||
enabled.named("Enabled"),
|
||||
healthDisplay.named("Display health"),
|
||||
players.named("Players"),
|
||||
hostile.named("Hostile"),
|
||||
passive.named("Passive"),
|
||||
heartEmoji.named("Heart emoji"),
|
||||
obfuscate.named("Obfuscate enemy names"),
|
||||
unit.named("Unit"),
|
||||
format.named("Format"),
|
||||
nameColor.named("Name color"),
|
||||
healthColor.named("Health color"));
|
||||
healthDisplay.named("Display health").description("Show health after the name"),
|
||||
players.named("Players").description("Show health on players"),
|
||||
hostile.named("Hostile").description("Show health on hostile mobs"),
|
||||
passive.named("Passive").description("Show health on passive mobs"),
|
||||
heartEmoji.named("Heart emoji").description("Add a heart after the health"),
|
||||
obfuscate.named("Obfuscate enemy names").description("Scramble names into unreadable text"),
|
||||
unit.named("Unit").description("Show health points or hearts"),
|
||||
format.named("Format").description("Value, value out of total, or percent"),
|
||||
nameColor.named("Name color").description("Color of the name"),
|
||||
healthColor.named("Health color").description("Color of the health"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@@ -34,9 +34,9 @@ public class SpeedometerMod extends Mod implements HudMod {
|
||||
.tags("Utility")
|
||||
.requires(Providers.feature::player),
|
||||
enabled.named("Enabled"),
|
||||
axis.named("Speed type"),
|
||||
unitText.named("Unit text"),
|
||||
showLabel.named("Show speed label"),
|
||||
axis.named("Speed type").description("All movement, only horizontal or only vertical"),
|
||||
unitText.named("Unit text").description("Unit shown after the speed"),
|
||||
showLabel.named("Show speed label").description("Show “Speed:” before the value"),
|
||||
layout.prop());
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import java.util.function.Function;
|
||||
|
||||
import org.saturnclient.common.provider.GLFWProvider;
|
||||
import org.saturnclient.config.property.BoolProperty;
|
||||
import org.saturnclient.config.property.CurveProperty;
|
||||
import org.saturnclient.config.property.FloatProperty;
|
||||
import org.saturnclient.config.property.IntProperty;
|
||||
import org.saturnclient.config.property.KeybindingProperty;
|
||||
import org.saturnclient.config.property.Property;
|
||||
import org.saturnclient.config.property.SelectProperty;
|
||||
import org.saturnclient.mod.Mod;
|
||||
import org.saturnclient.mod.ModSpec;
|
||||
import org.saturnclient.ui.anim.Animation;
|
||||
@@ -18,11 +18,10 @@ public class ZoomMod extends Mod {
|
||||
private static final BoolProperty enabled = Property.bool(false);
|
||||
private static final BoolProperty toggle = Property.bool(false);
|
||||
public static final KeybindingProperty zoomKey = Property.keybinding(GLFWProvider.GLFW_KEY_C);
|
||||
public static final FloatProperty zoomLevel = Property.floatProp(3.0f);
|
||||
public static final IntProperty zoomInDuration = Property.integer(700);
|
||||
public static final IntProperty zoomOutDuration = Property.integer(300);
|
||||
public static final SelectProperty curve = Property.select(1, "Ease In Out Cubic", "Ease Out Cubic",
|
||||
"Ease In Out Back");
|
||||
public static final FloatProperty zoomLevel = Property.floatProp(3.0f).range(1f, 10f, 0.5f).unit("×");
|
||||
public static final IntProperty zoomInDuration = Property.integer(700).range(0, 2000, 50).unit("ms");
|
||||
public static final IntProperty zoomOutDuration = Property.integer(300).range(0, 2000, 50).unit("ms");
|
||||
public static final CurveProperty curve = Property.curve(1);
|
||||
|
||||
public ZoomMod() {
|
||||
super(
|
||||
@@ -31,12 +30,12 @@ public class ZoomMod extends Mod {
|
||||
.tags("Camera", "Utility")
|
||||
.version("v0.2.0"),
|
||||
enabled.named("Enabled"),
|
||||
toggle.named("Toggle zoom"),
|
||||
zoomKey.named("Zoom Keybinding"),
|
||||
zoomLevel.named("Zoom Level"),
|
||||
zoomInDuration.named("Zoom In Duration"),
|
||||
zoomOutDuration.named("Zoom Out Duration"),
|
||||
curve.named("Curve"));
|
||||
toggle.named("Toggle zoom").description("Press the key once instead of holding it"),
|
||||
zoomKey.named("Zoom Keybinding").description("The key that zooms in"),
|
||||
zoomLevel.named("Zoom Level").description("How far the camera zooms in"),
|
||||
zoomInDuration.named("Zoom In Duration").description("How long zooming in takes"),
|
||||
zoomOutDuration.named("Zoom Out Duration").description("How long zooming back out takes"),
|
||||
curve.named("Curve").description("Easing of the zoom"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@@ -11,7 +11,6 @@ public class Element {
|
||||
public boolean focused;
|
||||
public Animation animation;
|
||||
public float scale = 1.0f;
|
||||
public Integer duration = null; // Element duration, will fade out if not null
|
||||
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
}
|
||||
|
||||
@@ -1,20 +1,54 @@
|
||||
package org.saturnclient.ui;
|
||||
|
||||
public class ElementContext {
|
||||
/**
|
||||
* Where a container's children are drawn: the screen position of their
|
||||
* (0, 0), and the part of the screen, top to bottom, where they can be seen.
|
||||
*/
|
||||
public record Frame(int x, int y, int visibleTop, int visibleBottom) {
|
||||
/** The whole screen, for top-level elements */
|
||||
public static final Frame SCREEN = new Frame(0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
public int mouseX, mouseY;
|
||||
/** The element's top-left corner on the screen, for placing popups next to it */
|
||||
public int originX, originY;
|
||||
/** The part of the screen, top to bottom, where the element can be seen */
|
||||
public int visibleTop, visibleBottom;
|
||||
public int elementWidth, elementHeight;
|
||||
public float elementScale;
|
||||
public long elapsed;
|
||||
|
||||
public ElementContext(long elapsed, int mouseX, int mouseY, Element e) {
|
||||
public ElementContext(long elapsed, int mouseX, int mouseY, Frame frame, Element e) {
|
||||
this.mouseX = mouseX - e.x;
|
||||
this.mouseY = mouseY - e.y;
|
||||
this.originX = frame.x() + e.x;
|
||||
this.originY = frame.y() + e.y;
|
||||
this.visibleTop = frame.visibleTop();
|
||||
this.visibleBottom = frame.visibleBottom();
|
||||
this.elementWidth = e.width;
|
||||
this.elementHeight = e.height;
|
||||
this.elementScale = e.scale;
|
||||
this.elapsed = elapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* The frame for this element's children, drawn {@code dx}, {@code dy} from
|
||||
* its corner and seen only between {@code clipTop} and {@code clipBottom}
|
||||
* (relative to its corner).
|
||||
*/
|
||||
public Frame childFrame(int dx, int dy, int clipTop, int clipBottom) {
|
||||
return new Frame(originX + dx, originY + dy,
|
||||
Math.max(visibleTop, originY + clipTop), Math.min(visibleBottom, originY + clipBottom));
|
||||
}
|
||||
|
||||
/**
|
||||
* The frame for children drawn at this element's corner and not clipped.
|
||||
*/
|
||||
public Frame childFrame() {
|
||||
return new Frame(originX, originY, visibleTop, visibleBottom);
|
||||
}
|
||||
|
||||
public boolean isHovering() {
|
||||
return Utils.isHovering(mouseX, mouseY, elementWidth, elementHeight, elementScale);
|
||||
}
|
||||
|
||||
@@ -15,14 +15,27 @@ public class ElementRenderer {
|
||||
}
|
||||
|
||||
public static void render(List<Element> elements, long elapsed, RenderScope renderScope, int mouseX, int mouseY) {
|
||||
render(elements, elapsed, renderScope, mouseX, mouseY, ElementContext.Frame.SCREEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a container's children within its {@code frame}.
|
||||
*/
|
||||
public static void render(List<Element> elements, long elapsed, RenderScope renderScope, int mouseX, int mouseY,
|
||||
ElementContext.Frame frame) {
|
||||
for (Element element : elements) {
|
||||
element.playAnimationFrame(elapsed);
|
||||
|
||||
// Not faded in yet
|
||||
if (element.opacity <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
renderScope.getMatrixStack().push();
|
||||
renderScope.setOpacity(element.opacity);
|
||||
renderScope.getMatrixStack().translate(element.x, element.y);
|
||||
renderScope.getMatrixStack().scale(element.scale, element.scale);
|
||||
element.render(renderScope, new ElementContext(elapsed, mouseX, mouseY, element));
|
||||
element.render(renderScope, new ElementContext(elapsed, mouseX, mouseY, frame, element));
|
||||
renderScope.getMatrixStack().pop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.saturnclient.ui;
|
||||
|
||||
import org.saturnclient.ui.components.Sidebar;
|
||||
import org.saturnclient.ui.elements.ImageTexture;
|
||||
import org.saturnclient.ui.elements.Panel;
|
||||
import org.saturnclient.ui.elements.Scroll;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
|
||||
/**
|
||||
* A screen laid out as the sidebar beside one panel, centered and shrunk to
|
||||
* fit smaller windows. Subclasses fill the panel in {@link #panel()}, using the
|
||||
* content bounds this class computes.
|
||||
*/
|
||||
public abstract class PanelScreen extends SaturnScreen {
|
||||
private static final int MAX_WIDTH = 900;
|
||||
private static final int MAX_HEIGHT = 580;
|
||||
private static final int MARGIN = 40;
|
||||
private static final int GAP = 12;
|
||||
|
||||
protected static final int PADDING = 20;
|
||||
|
||||
/** How far a scroll reaches past its content area, into the padding, for its bar */
|
||||
private static final int SCROLL_OVERHANG = 14;
|
||||
|
||||
private final int sidebarItem;
|
||||
|
||||
protected int panelX, panelY, panelWidth, panelHeight;
|
||||
/** The panel's area inside its padding */
|
||||
protected int contentX, contentY, contentWidth, contentHeight;
|
||||
|
||||
/**
|
||||
* @param sidebarItem the {@link Sidebar} item to highlight
|
||||
*/
|
||||
public PanelScreen(String title, int sidebarItem) {
|
||||
super(title);
|
||||
this.sidebarItem = sidebarItem;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void ui() {
|
||||
panelWidth = Math.min(MAX_WIDTH, width - MARGIN * 2 - Sidebar.WIDTH - GAP);
|
||||
panelHeight = Math.min(MAX_HEIGHT, height - MARGIN * 2);
|
||||
|
||||
int left = (width - (Sidebar.WIDTH + GAP + panelWidth)) / 2;
|
||||
panelX = left + Sidebar.WIDTH + GAP;
|
||||
panelY = (height - panelHeight) / 2;
|
||||
|
||||
contentX = panelX + PADDING;
|
||||
contentY = panelY + PADDING;
|
||||
contentWidth = panelWidth - PADDING * 2;
|
||||
contentHeight = panelHeight - PADDING * 2;
|
||||
|
||||
draw(new Sidebar(sidebarItem, this.provider::close)
|
||||
.dimensions(Sidebar.WIDTH, panelHeight)
|
||||
.position(left, panelY));
|
||||
|
||||
draw(new Panel().dimensions(panelWidth, panelHeight).position(panelX, panelY));
|
||||
|
||||
panel();
|
||||
|
||||
draw(new ImageTexture(Textures.LOGO_TEXT_BIG).dimensions(180, 18).position(width - 180 - 20, height - 18 - 20));
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the panel's content.
|
||||
*/
|
||||
protected abstract void panel();
|
||||
|
||||
/**
|
||||
* The width of a scroll filling {@code width} of the content area.
|
||||
*/
|
||||
protected static int scrollWidth(int width) {
|
||||
return width + SCROLL_OVERHANG;
|
||||
}
|
||||
|
||||
/**
|
||||
* The width left for a scroll's children, clear of its scrollbar.
|
||||
*/
|
||||
protected static int scrollContentWidth(int width) {
|
||||
return scrollWidth(width) - Scroll.SCROLLBAR_SPACE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package org.saturnclient.ui;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A single element drawn above the rest of the screen, such as a dropdown's
|
||||
* options or a color picker. It gets mouse events before anything else, and a
|
||||
* click or scroll outside it closes it.
|
||||
*
|
||||
* Only one screen is open at a time, so the popup is kept here rather than on
|
||||
* the screen; {@link SaturnScreen} forwards to it and clears it when it's
|
||||
* rebuilt.
|
||||
*/
|
||||
public final class Popup {
|
||||
private static final int SCREEN_MARGIN = 8;
|
||||
private static final int OWNER_GAP = 4;
|
||||
|
||||
private static Element popup;
|
||||
private static Element owner;
|
||||
private static int screenWidth, screenHeight;
|
||||
|
||||
private Popup() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens {@code popup} with its top-left corner at ({@code x}, {@code y}) on
|
||||
* the screen, moved as needed to stay on it.
|
||||
*
|
||||
* @param owner the element that opened it, for {@link #isOpenFor}
|
||||
*/
|
||||
public static void open(Element owner, Element popup, int x, int y) {
|
||||
Popup.owner = owner;
|
||||
Popup.popup = popup;
|
||||
|
||||
popup.x = clamp(x, popup.width, screenWidth);
|
||||
popup.y = clamp(y, popup.height, screenHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens {@code popup} just below its owner if it fits in the part of the
|
||||
* screen where the owner can be seen, such as a scroll's box; otherwise
|
||||
* above it if it fits there, or else on the side with more room.
|
||||
*
|
||||
* @param ownerContext the owner's context from its last render
|
||||
*/
|
||||
public static void openBelow(Element owner, Element popup, ElementContext ownerContext) {
|
||||
openBelow(owner, popup, ownerContext, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #openBelow(Element, Element, ElementContext)}, with the popup
|
||||
* moved {@code offsetX} from the owner's left edge.
|
||||
*/
|
||||
public static void openBelow(Element owner, Element popup, ElementContext ownerContext, int offsetX) {
|
||||
int top = Math.max(ownerContext.visibleTop, SCREEN_MARGIN);
|
||||
int bottom = Math.min(ownerContext.visibleBottom, screenHeight - SCREEN_MARGIN);
|
||||
|
||||
int below = ownerContext.originY + owner.height + OWNER_GAP;
|
||||
int above = ownerContext.originY - OWNER_GAP - popup.height;
|
||||
|
||||
boolean fitsBelow = below + popup.height <= bottom;
|
||||
boolean fitsAbove = above >= top;
|
||||
boolean moreRoomBelow = bottom - below >= ownerContext.originY - OWNER_GAP - top;
|
||||
|
||||
int y = fitsBelow || (!fitsAbove && moreRoomBelow) ? below : above;
|
||||
open(owner, popup, ownerContext.originX + offsetX, y);
|
||||
}
|
||||
|
||||
public static void close() {
|
||||
popup = null;
|
||||
owner = null;
|
||||
}
|
||||
|
||||
public static boolean isOpenFor(Element element) {
|
||||
return popup != null && owner == element;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Called by SaturnScreen
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
static void reset(int width, int height) {
|
||||
close();
|
||||
screenWidth = width;
|
||||
screenHeight = height;
|
||||
}
|
||||
|
||||
static void render(RenderScope renderScope, long elapsed, int mouseX, int mouseY) {
|
||||
if (popup != null) {
|
||||
ElementRenderer.render(List.of(popup), elapsed, renderScope, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the popup took the click; a click outside it closes it
|
||||
* and is taken too, so it doesn't reach what's underneath
|
||||
*/
|
||||
static boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||
if (popup == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isInside(mouseX, mouseY)) {
|
||||
ElementRenderer.mouseClicked(List.of(popup), mouseX, mouseY, button);
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
|
||||
if (popup == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ElementRenderer.mouseDragged(List.of(popup), mouseX, mouseY, button, deltaX, deltaY);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void mouseReleased(double mouseX, double mouseY, int button) {
|
||||
if (popup != null) {
|
||||
ElementRenderer.mouseReleased(List.of(popup), mouseX, mouseY, button);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the popup took the scroll; scrolling outside it closes it,
|
||||
* since it wouldn't move with what's underneath
|
||||
*/
|
||||
static boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {
|
||||
if (popup == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isInside(mouseX, mouseY)) {
|
||||
ElementRenderer.mouseScrolled(List.of(popup), mouseX, mouseY, horizontalAmount, verticalAmount);
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isInside(double mouseX, double mouseY) {
|
||||
return mouseX >= popup.x && mouseX <= popup.x + popup.width
|
||||
&& mouseY >= popup.y && mouseY <= popup.y + popup.height;
|
||||
}
|
||||
|
||||
private static int clamp(int position, int size, int screenSize) {
|
||||
return Math.max(SCREEN_MARGIN, Math.min(position, screenSize - SCREEN_MARGIN - size));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.saturnclient.ui;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.common.ref.asset.SpriteRef;
|
||||
@@ -33,23 +34,116 @@ public interface RenderScope {
|
||||
if (color == 0)
|
||||
return;
|
||||
|
||||
drawTexture(getRoundedRectangle(width, height, radius), x, y, 0, 0, width, height, color);
|
||||
}
|
||||
|
||||
/**
|
||||
* The white rounded rectangle that {@link #drawRoundedRectangle} tints, as a
|
||||
* cached texture.
|
||||
*/
|
||||
default IdentifierRef getRoundedRectangle(int width, int height, int radius) {
|
||||
radius = Math.min(radius, width / 2);
|
||||
radius = Math.min(radius, height / 2);
|
||||
|
||||
String svg = String.format(
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' width='%d' height='%d'>" +
|
||||
"<rect width='%d' height='%d' rx='%d' ry='%d' fill='white'/>" +
|
||||
"</svg>",
|
||||
width, height,
|
||||
String rect = String.format(
|
||||
"<rect width='%d' height='%d' rx='%d' ry='%d' fill='white'/>",
|
||||
width, height,
|
||||
radius, radius);
|
||||
|
||||
return getSvgShape(rect, "rounded_" + width + "_" + height + "_" + radius, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Frosts a rounded rectangle for a translucent background to be drawn over:
|
||||
* the game behind the screen, blurred by {@code blur}, cut to exactly the
|
||||
* shape {@link #drawRoundedRectangle} draws. Render scopes without glass
|
||||
* draw nothing.
|
||||
*/
|
||||
default void drawGlass(int x, int y, int width, int height, int radius, int blur) {
|
||||
drawGlass(getRoundedRectangle(width, height, radius), x, y, width, height, blur);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the game behind the screen, blurred by {@code blur}, where
|
||||
* {@code mask}, stretched over the area, is opaque.
|
||||
*/
|
||||
default void drawGlass(IdentifierRef mask, int x, int y, int width, int height, int blur) {
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link #drawRoundedRectangle} with a border, frosted with glass blurred
|
||||
* by {@code blur} when the background isn't solid. A blur of 0 leaves it
|
||||
* plain, showing the screen's own background blur.
|
||||
*/
|
||||
default void drawGlassRectangle(int x, int y, int width, int height, int radius, int blur, int thickness,
|
||||
int background, int border) {
|
||||
if (blur > 0 && background >>> 24 < 0xFF) {
|
||||
drawGlass(x, y, width, height, radius, blur);
|
||||
}
|
||||
|
||||
drawRoundedRectangle(x, y, width, height, radius, thickness, background, border);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a rounded outline that stays inside the given bounds, so it lines up
|
||||
* with a {@link #drawRoundedRectangle} of the same size and radius.
|
||||
*/
|
||||
default void drawRoundedBorder(int x, int y, int width, int height, int radius, int thickness, int color) {
|
||||
if (color == 0 || thickness <= 0)
|
||||
return;
|
||||
|
||||
radius = Math.min(radius, width / 2);
|
||||
radius = Math.min(radius, height / 2);
|
||||
thickness = Math.min(thickness, Math.min(width, height) / 2);
|
||||
|
||||
// The stroke is centered on the path, so inset it by half its width.
|
||||
float inset = thickness / 2f;
|
||||
float innerRadius = Math.max(0, radius - inset);
|
||||
|
||||
String rect = String.format(Locale.ROOT,
|
||||
"<rect x='%.1f' y='%.1f' width='%.1f' height='%.1f' rx='%.1f' ry='%.1f' " +
|
||||
"fill='none' stroke='white' stroke-width='%d'/>",
|
||||
inset, inset,
|
||||
width - (float) thickness, height - (float) thickness,
|
||||
innerRadius, innerRadius,
|
||||
thickness);
|
||||
|
||||
drawSvgShape(rect, "rounded_border_" + width + "_" + height + "_" + radius + "_" + thickness,
|
||||
x, y, width, height, color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a rounded rectangle filled with {@code background} and outlined with
|
||||
* {@code border}. Either color can be 0 to skip that part.
|
||||
*/
|
||||
default void drawRoundedRectangle(int x, int y, int width, int height, int radius, int thickness,
|
||||
int background, int border) {
|
||||
drawRoundedRectangle(x, y, width, height, radius, background);
|
||||
drawRoundedBorder(x, y, width, height, radius, thickness, border);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders {@code shape} (SVG elements in a {@code width} x {@code height}
|
||||
* canvas, drawn in white) to a cached texture and draws it tinted with
|
||||
* {@code color}.
|
||||
*/
|
||||
private void drawSvgShape(String shape, String cacheKey, int x, int y, int width, int height, int color) {
|
||||
drawTexture(getSvgShape(shape, cacheKey, width, height), x, y, 0, 0, width, height, color);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code shape} (SVG elements in a {@code width} x {@code height} canvas,
|
||||
* drawn in white) rendered to a cached texture.
|
||||
*/
|
||||
private IdentifierRef getSvgShape(String shape, String cacheKey, int width, int height) {
|
||||
String svg = String.format(
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' width='%d' height='%d'>%s</svg>",
|
||||
width, height, shape);
|
||||
|
||||
InputStream svgStream = new ByteArrayInputStream(svg.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
drawTexture(SvgTexture.getSvg(svgStream,
|
||||
IdentifierRef.ofSaturn(
|
||||
"textures/gui/tmp_rect/rounded_" + width + "_" + height + "_" + radius),
|
||||
width * 4, height * 4), x, y, 0, 0, width, height, color);
|
||||
return SvgTexture.getSvg(svgStream, IdentifierRef.ofSaturn("textures/gui/tmp_rect/" + cacheKey),
|
||||
width * 4, height * 4);
|
||||
}
|
||||
|
||||
void drawBorder(int x, int y, int width, int height, int color);
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
package org.saturnclient.ui;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.saturnclient.config.Config;
|
||||
import org.saturnclient.config.property.FloatProperty;
|
||||
|
||||
public abstract class SaturnScreen {
|
||||
/** How long the background blur takes to fade in, in ms */
|
||||
private static final int BLUR_FADE_IN = 700;
|
||||
|
||||
public static interface ScreenProvider {
|
||||
public void close();
|
||||
|
||||
/** The screen's width in GUI pixels */
|
||||
public int getWidth();
|
||||
|
||||
/** The screen's height in GUI pixels */
|
||||
public int getHeight();
|
||||
}
|
||||
|
||||
@@ -17,12 +26,17 @@ public abstract class SaturnScreen {
|
||||
public int width;
|
||||
public int height;
|
||||
public String title;
|
||||
/** GUI pixels per Saturn unit, from {@link #getScale()} when the screen was built */
|
||||
public float scale = 0.5f;
|
||||
/** Whether a mouse button is held, so the screen isn't rebuilt under a drag */
|
||||
private boolean mouseDown = false;
|
||||
|
||||
// Saturn Screen
|
||||
public List<Element> elements = new ArrayList<>();
|
||||
public float backgroundOpacity = 1.0f;
|
||||
public Instant start = null;
|
||||
public int backgroundBlur = 10;
|
||||
/** Whether the game behind this screen is blurred, by the client's background blur setting */
|
||||
public boolean blurBackground = true;
|
||||
|
||||
// Provider
|
||||
public ScreenProvider provider;
|
||||
@@ -31,10 +45,53 @@ public abstract class SaturnScreen {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* How much to blur the game behind this screen right now: the client's
|
||||
* setting, faded in over the screen's first moments.
|
||||
*/
|
||||
public int getBackgroundBlur() {
|
||||
if (!blurBackground || start == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
long elapsed = Duration.between(start, Instant.now()).toMillis();
|
||||
return Math.round(Config.backgroundBlur.value * Math.min(elapsed / (float) BLUR_FADE_IN, 1f));
|
||||
}
|
||||
|
||||
/**
|
||||
* The GUI scale setting this screen follows; the menu scale unless the
|
||||
* screen is another kind.
|
||||
*/
|
||||
protected FloatProperty getScaleSetting() {
|
||||
return Config.menuScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* GUI pixels per Saturn unit. At 1× a Saturn unit is half a GUI pixel.
|
||||
* Enlarging stops where the screen would be smaller than 320x240 GUI
|
||||
* pixels, the least Minecraft's own GUI scale leaves.
|
||||
*/
|
||||
public float getScale() {
|
||||
float fit = Math.min(provider.getWidth() / 320f, provider.getHeight() / 240f);
|
||||
return 0.5f * Math.min(getScaleSetting().value, Math.max(1f, fit));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds the screen if its scale setting changed, once no drag is going
|
||||
* on (the slider being dragged would be replaced).
|
||||
*/
|
||||
public void updateScale() {
|
||||
if (!mouseDown && getScale() != scale) {
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
public void init() {
|
||||
elements.clear();
|
||||
width = provider.getWidth();
|
||||
height = provider.getHeight();
|
||||
scale = getScale();
|
||||
width = (int) (provider.getWidth() / scale);
|
||||
height = (int) (provider.getHeight() / scale);
|
||||
Popup.reset(width, height);
|
||||
ui(); // abstraction to render the saturn ui and also render extra stuff here
|
||||
}
|
||||
|
||||
@@ -51,9 +108,10 @@ public abstract class SaturnScreen {
|
||||
public void render(RenderScope renderScope, int mouseX, int mouseY, float delta, long elapsed) {
|
||||
renderScope.getMatrixStack().push();
|
||||
|
||||
renderScope.getMatrixStack().scale(0.5f, 0.5f);
|
||||
renderScope.getMatrixStack().scale(scale, scale);
|
||||
|
||||
ElementRenderer.render(new ArrayList<>(elements), elapsed, renderScope, mouseX, mouseY);
|
||||
Popup.render(renderScope, elapsed, mouseX, mouseY);
|
||||
|
||||
renderScope.getMatrixStack().pop();
|
||||
}
|
||||
@@ -63,27 +121,35 @@ public abstract class SaturnScreen {
|
||||
// --------------
|
||||
|
||||
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||
mouseX *= 2;
|
||||
mouseY *= 2;
|
||||
mouseDown = true;
|
||||
mouseX /= scale;
|
||||
mouseY /= scale;
|
||||
|
||||
ElementRenderer.mouseClicked(elements, mouseX, mouseY, button);
|
||||
if (!Popup.mouseClicked(mouseX, mouseY, button)) {
|
||||
ElementRenderer.mouseClicked(elements, mouseX, mouseY, button);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
|
||||
mouseX *= 2;
|
||||
mouseY *= 2;
|
||||
mouseX /= scale;
|
||||
mouseY /= scale;
|
||||
|
||||
ElementRenderer.mouseDragged(elements, mouseX, mouseY, button, deltaX, deltaY);
|
||||
if (!Popup.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) {
|
||||
ElementRenderer.mouseDragged(elements, mouseX, mouseY, button, deltaX, deltaY);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean mouseReleased(double mouseX, double mouseY, int button) {
|
||||
mouseX *= 2;
|
||||
mouseY *= 2;
|
||||
mouseDown = false;
|
||||
mouseX /= scale;
|
||||
mouseY /= scale;
|
||||
|
||||
// Everything hears the release, so no element is left thinking it's being dragged.
|
||||
Popup.mouseReleased(mouseX, mouseY, button);
|
||||
ElementRenderer.mouseReleased(elements, mouseX, mouseY, button);
|
||||
|
||||
return false;
|
||||
@@ -94,10 +160,12 @@ public abstract class SaturnScreen {
|
||||
double mouseY,
|
||||
double horizontalAmount,
|
||||
double verticalAmount) {
|
||||
mouseX *= 2;
|
||||
mouseY *= 2;
|
||||
mouseX /= scale;
|
||||
mouseY /= scale;
|
||||
|
||||
ElementRenderer.mouseScrolled(elements, mouseX, mouseY, horizontalAmount, verticalAmount);
|
||||
if (!Popup.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) {
|
||||
ElementRenderer.mouseScrolled(elements, mouseX, mouseY, horizontalAmount, verticalAmount);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.saturnclient.ui;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.saturnclient.common.provider.GLFWProvider;
|
||||
|
||||
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));
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -16,12 +16,12 @@ public class Fade extends Animation {
|
||||
|
||||
@Override
|
||||
public void tick(double progress, Element element) {
|
||||
element.opacity = Math.max(0.1f, targetOpacity * (float) progress);
|
||||
element.opacity = targetOpacity * (float) progress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Element element) {
|
||||
targetOpacity = element.opacity;
|
||||
element.opacity = 0.1f;
|
||||
element.opacity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,57 @@
|
||||
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.Config;
|
||||
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 = 32;
|
||||
|
||||
private static final int HEAD = 18;
|
||||
private static final int PADDING = 7;
|
||||
private static final int GAP = 8;
|
||||
private static final int DOT = 6;
|
||||
private static final float NAME_SCALE = 0.55f;
|
||||
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 + GAP + (int) (Fonts.getWidth(name, Theme.FONT.value) * NAME_SCALE) + GAP + DOT
|
||||
+ GAP + 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawGlassRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, Config.widgetBlur.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 + GAP;
|
||||
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) + GAP;
|
||||
renderScope.drawRoundedRectangle(dotX, (height - DOT) / 2, DOT, DOT, DOT / 2,
|
||||
ServiceClient.isConnected() ? CONNECTED : Theme.withAlpha(0.3f, Theme.FOREGROUND.value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.saturnclient.ui.components;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
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.components.inputs.ColorInput;
|
||||
import org.saturnclient.ui.elements.Scroll;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
|
||||
/**
|
||||
* Lays out the color properties of a config as a two-column grid of tiles,
|
||||
* each with the color's name and its input.
|
||||
*/
|
||||
public final class ColorGrid {
|
||||
private static final int COLUMNS = 2;
|
||||
private static final int TILE_HEIGHT = 48;
|
||||
private static final int GAP = 8;
|
||||
private static final int INSET = 8;
|
||||
|
||||
private ColorGrid() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tile for every {@link ColorProperty} in {@code properties} to
|
||||
* {@code scroll}, starting at {@code y}.
|
||||
*
|
||||
* @return the y below the grid
|
||||
*/
|
||||
public static int draw(Scroll scroll, Map<String, Property> properties, int width, int y) {
|
||||
int tileWidth = (width - GAP * (COLUMNS - 1)) / COLUMNS;
|
||||
int i = 0;
|
||||
|
||||
for (Map.Entry<String, Property> entry : properties.entrySet()) {
|
||||
if (!(entry.getValue() instanceof ColorProperty color)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int x = (i % COLUMNS) * (tileWidth + GAP);
|
||||
int tileY = y + (i / COLUMNS) * (TILE_HEIGHT + GAP);
|
||||
|
||||
ColorInput input = new ColorInput(color);
|
||||
scroll.draw(new Tile(entry.getKey(), tileWidth, input.width).position(x, tileY));
|
||||
scroll.draw(input.position(x + tileWidth - INSET - input.width, tileY + (TILE_HEIGHT - input.height) / 2));
|
||||
i++;
|
||||
}
|
||||
|
||||
int rows = (i + COLUMNS - 1) / COLUMNS;
|
||||
return y + rows * TILE_HEIGHT + Math.max(0, rows - 1) * GAP;
|
||||
}
|
||||
|
||||
private static class Tile extends Element {
|
||||
private static final float NAME_SCALE = 0.7f;
|
||||
|
||||
private final String name;
|
||||
private final int inputWidth;
|
||||
|
||||
Tile(String name, int width, int inputWidth) {
|
||||
this.name = name;
|
||||
this.inputWidth = inputWidth;
|
||||
this.width = width;
|
||||
this.height = TILE_HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
|
||||
Theme.withAlpha(0.72f, Theme.PRIMARY.value), Theme.withAlpha(0.06f, Theme.FOREGROUND.value));
|
||||
|
||||
String text = Fonts.ellipsize(name, Theme.FONT.value, NAME_SCALE, width - inputWidth - INSET * 2 - 12);
|
||||
int nameY = (height - (int) (Fonts.getHeight() * NAME_SCALE)) / 2;
|
||||
renderScope.drawText(NAME_SCALE, text, 12, nameY, Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package org.saturnclient.ui.components;
|
||||
|
||||
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 CosmeticPreview extends Element {
|
||||
static int padding = 14;
|
||||
|
||||
IdentifierRef sprite;
|
||||
private Runnable onClick;
|
||||
private boolean isSelected;
|
||||
|
||||
public CosmeticPreview(boolean isSelected, IdentifierRef sprite, Runnable onClick) {
|
||||
this.sprite = sprite;
|
||||
this.onClick = onClick;
|
||||
this.isSelected = isSelected;
|
||||
|
||||
this.width = 50;
|
||||
this.height = 111;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value,
|
||||
isSelected ? 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,
|
||||
Theme.FOREGROUND.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
if (onClick != null) {
|
||||
onClick.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package org.saturnclient.ui.components;
|
||||
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
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.resources.Fonts;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
|
||||
/**
|
||||
* One cosmetic in the cosmetics grid: its preview and name. It shows whether
|
||||
* it's equipped, and cosmetics not owned yet are dimmed with a "Get" label.
|
||||
*/
|
||||
public class CosmeticTile extends Element {
|
||||
public static final int HEIGHT = 168;
|
||||
|
||||
private static final int INSET = 8;
|
||||
private static final int IMAGE_AREA = 124;
|
||||
private static final float NAME_SCALE = 0.62f;
|
||||
private static final float BADGE_SCALE = 0.54f;
|
||||
private static final int BADGE_HEIGHT = 20;
|
||||
/** How long a claim may take before it can be tried again, in ms */
|
||||
private static final long CLAIM_TIMEOUT = 5000;
|
||||
|
||||
private final String name;
|
||||
private final IdentifierRef preview;
|
||||
private final float previewAspect;
|
||||
private final BooleanSupplier owned;
|
||||
private final BooleanSupplier equipped;
|
||||
private final Runnable onEquip;
|
||||
private final Runnable onClaim;
|
||||
|
||||
private long claimedAt = 0;
|
||||
|
||||
/**
|
||||
* @param previewAspect the preview texture's width divided by its height
|
||||
*/
|
||||
public CosmeticTile(String name, IdentifierRef preview, float previewAspect, int width, BooleanSupplier owned,
|
||||
BooleanSupplier equipped, Runnable onEquip, Runnable onClaim) {
|
||||
this.name = name;
|
||||
this.preview = preview;
|
||||
this.previewAspect = previewAspect;
|
||||
this.owned = owned;
|
||||
this.equipped = equipped;
|
||||
this.onEquip = onEquip;
|
||||
this.onClaim = onClaim;
|
||||
this.width = width;
|
||||
this.height = HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
boolean isOwned = owned.getAsBoolean();
|
||||
boolean isEquipped = equipped.getAsBoolean();
|
||||
boolean hovered = ctx.isHovering();
|
||||
|
||||
int border = isEquipped ? Theme.ACCENT.value
|
||||
: Theme.withAlpha(hovered ? 0.22f : 0.06f, Theme.FOREGROUND.value);
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, isEquipped ? 2 : 1,
|
||||
Theme.PRIMARY.value, border);
|
||||
|
||||
// Preview, fitted into its area
|
||||
int areaWidth = width - INSET * 2;
|
||||
renderScope.drawRoundedRectangle(INSET, INSET, areaWidth, IMAGE_AREA, Theme.WIDGET_RADIUS.value - 2,
|
||||
Theme.withAlpha(0.05f, Theme.FOREGROUND.value));
|
||||
|
||||
int imageHeight = IMAGE_AREA - 12;
|
||||
int imageWidth = (int) (imageHeight * previewAspect);
|
||||
if (imageWidth > areaWidth - 12) {
|
||||
imageWidth = areaWidth - 12;
|
||||
imageHeight = (int) (imageWidth / previewAspect);
|
||||
}
|
||||
|
||||
renderScope.drawTexture(preview, INSET + (areaWidth - imageWidth) / 2, INSET + (IMAGE_AREA - imageHeight) / 2,
|
||||
0, 0, imageWidth, imageHeight, isOwned ? 0xFFFFFFFF : Theme.withAlpha(0.35f, 0xFFFFFFFF));
|
||||
|
||||
// Name
|
||||
int nameY = INSET + IMAGE_AREA + (HEIGHT - INSET - IMAGE_AREA - (int) (Fonts.getHeight() * NAME_SCALE)) / 2;
|
||||
renderScope.drawText(NAME_SCALE, Fonts.ellipsize(name, Theme.FONT.value, NAME_SCALE, width - 24), 12, nameY,
|
||||
Theme.FONT.value, isOwned ? Theme.FOREGROUND.value : Theme.getMutedFg());
|
||||
|
||||
// Badge in the preview's corner
|
||||
if (isEquipped) {
|
||||
drawBadge(renderScope, "On", Theme.ACCENT.value, Theme.ACCENT_FG.value, true);
|
||||
} else if (!isOwned) {
|
||||
drawBadge(renderScope, isClaiming() ? "Claiming" : "Get", hovered ? Theme.ACCENT.value : Theme.PRIMARY.value,
|
||||
hovered ? Theme.ACCENT_FG.value : Theme.PRIMARY_FG.value, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawBadge(RenderScope renderScope, String text, int background, int foreground, boolean check) {
|
||||
int icon = check ? 10 : 0;
|
||||
int badgeWidth = 14 + icon + (check ? 4 : 0) + (int) (Fonts.getWidth(text, Theme.FONT.value) * BADGE_SCALE);
|
||||
int x = width - INSET - 6 - badgeWidth;
|
||||
int y = INSET + 6;
|
||||
|
||||
renderScope.drawRoundedRectangle(x, y, badgeWidth, BADGE_HEIGHT, 6, background);
|
||||
|
||||
if (check) {
|
||||
renderScope.drawTexture(Textures.ICON_CHECK, x + 7, y + (BADGE_HEIGHT - icon) / 2, 0, 0, icon, icon, foreground);
|
||||
}
|
||||
|
||||
int textY = y + (BADGE_HEIGHT - (int) (Fonts.getHeight() * BADGE_SCALE)) / 2;
|
||||
renderScope.drawText(BADGE_SCALE, text, x + 7 + icon + (check ? 4 : 0), textY, Theme.FONT.value, foreground);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
if (owned.getAsBoolean()) {
|
||||
if (!equipped.getAsBoolean()) {
|
||||
onEquip.run();
|
||||
}
|
||||
} else if (!isClaiming()) {
|
||||
claimedAt = System.currentTimeMillis();
|
||||
onClaim.run();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isClaiming() {
|
||||
return System.currentTimeMillis() - claimedAt < CLAIM_TIMEOUT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.saturnclient.ui.components;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.config.Config;
|
||||
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 = 40;
|
||||
|
||||
private static final int PADDING = 5;
|
||||
private static final int ITEM = HEIGHT - PADDING * 2;
|
||||
private static final int ICON = 15;
|
||||
private static final int GAP = 3;
|
||||
|
||||
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.drawGlassRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value + 4, Config.widgetBlur.value, 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.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
|
||||
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) {
|
||||
scope.drawRoundedRectangle(0, 0, layout.width, layout.height,
|
||||
layout.radius.value, layout.bgColor.value);
|
||||
}
|
||||
|
||||
feature.renderDummy(scope);
|
||||
mod.renderDummy(scope);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.saturnclient.ui.components;
|
||||
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.mod.HudMod;
|
||||
import org.saturnclient.mod.ModLayout;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
|
||||
/**
|
||||
* A card showing a HUD mod as it looks in game, following its settings live.
|
||||
* The mod is drawn at its in-game scale, shrunk if needed to fit the card.
|
||||
*/
|
||||
public class HudPreviewCard extends Element {
|
||||
private static final int PADDING = 16;
|
||||
private static final int TITLE_HEIGHT = 24;
|
||||
private static final float TITLE_SCALE = 0.6f;
|
||||
|
||||
private final HudMod mod;
|
||||
private final HudModPreview preview;
|
||||
|
||||
public HudPreviewCard(HudMod mod) {
|
||||
this.mod = mod;
|
||||
this.preview = new HudModPreview(mod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value + 4, 1,
|
||||
Theme.withAlpha(0.72f, Theme.PRIMARY.value), Theme.withAlpha(0.06f, Theme.FOREGROUND.value));
|
||||
|
||||
renderScope.drawText(TITLE_SCALE, "PREVIEW", PADDING, PADDING, Theme.FONT.value, Theme.getMutedFg());
|
||||
|
||||
ModLayout layout = mod.getDimensions();
|
||||
if (layout.width <= 0 || layout.height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// HUD mods are laid out in GUI pixels, which are two Saturn units.
|
||||
int areaWidth = width - PADDING * 2;
|
||||
int areaHeight = height - PADDING * 2 - TITLE_HEIGHT;
|
||||
float scale = Math.min(layout.scale.value * 2,
|
||||
Math.min((float) areaWidth / layout.width, (float) areaHeight / layout.height));
|
||||
|
||||
float x = PADDING + (areaWidth - layout.width * scale) / 2;
|
||||
float y = PADDING + TITLE_HEIGHT + (areaHeight - layout.height * scale) / 2;
|
||||
|
||||
renderScope.getMatrixStack().push();
|
||||
renderScope.getMatrixStack().translate(x, y);
|
||||
renderScope.getMatrixStack().scale(scale, scale);
|
||||
preview.render(renderScope, ctx);
|
||||
renderScope.getMatrixStack().pop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.saturnclient.ui.components;
|
||||
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.mod.Mod;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
|
||||
/**
|
||||
* The top of a mod's settings: its icon, name, version and tags, and description.
|
||||
*/
|
||||
public class ModHeader extends Element {
|
||||
public static final int HEIGHT = 60;
|
||||
|
||||
private static final int ICON = 30;
|
||||
private static final int TEXT_X = HEIGHT + 16;
|
||||
private static final float NAME_SCALE = 1.2f;
|
||||
private static final float DESCRIPTION_SCALE = 0.7f;
|
||||
|
||||
private static final int CHIP_HEIGHT = 22;
|
||||
private static final int CHIP_PADDING = 8;
|
||||
private static final int CHIP_GAP = 6;
|
||||
private static final float CHIP_SCALE = 0.58f;
|
||||
|
||||
private final Mod mod;
|
||||
|
||||
public ModHeader(Mod mod, int width) {
|
||||
this.mod = mod;
|
||||
this.width = width;
|
||||
this.height = HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
boolean enabled = mod.isEnabled();
|
||||
|
||||
// Icon
|
||||
renderScope.drawRoundedRectangle(0, 0, HEIGHT, HEIGHT, Theme.WIDGET_RADIUS.value + 4,
|
||||
enabled ? Theme.withAlpha(0.18f, Theme.ACCENT.value) : Theme.withAlpha(0.06f, Theme.FOREGROUND.value));
|
||||
renderScope.drawTexture(mod.getIconTexture(), (HEIGHT - ICON) / 2, (HEIGHT - ICON) / 2, 0, 0, ICON, ICON,
|
||||
Theme.PRIMARY_FG.value);
|
||||
|
||||
// Name, then the version and tags as chips
|
||||
int nameHeight = (int) (Fonts.getHeight() * NAME_SCALE);
|
||||
int nameY = 4;
|
||||
renderScope.drawText(NAME_SCALE, mod.getName(), TEXT_X, nameY, Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
|
||||
int chipX = TEXT_X + (int) (Fonts.getWidth(mod.getName(), Theme.FONT.value) * NAME_SCALE) + 12;
|
||||
int chipY = nameY + (nameHeight - CHIP_HEIGHT) / 2;
|
||||
|
||||
if (mod.getVersion() != null) {
|
||||
chipX = drawChip(renderScope, mod.getVersion(), chipX, chipY, false) + CHIP_GAP;
|
||||
}
|
||||
|
||||
for (String tag : mod.getTags()) {
|
||||
chipX = drawChip(renderScope, tag, chipX, chipY, true) + CHIP_GAP;
|
||||
}
|
||||
|
||||
// Description
|
||||
if (mod.getDescription() != null) {
|
||||
String description = Fonts.ellipsize(mod.getDescription(), Theme.getBodyFont(), DESCRIPTION_SCALE,
|
||||
width - TEXT_X);
|
||||
renderScope.drawText(DESCRIPTION_SCALE, description, TEXT_X, nameY + nameHeight + 8,
|
||||
Theme.getBodyFont(), Theme.getMutedFg());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the x after the chip
|
||||
*/
|
||||
private int drawChip(RenderScope renderScope, String text, int x, int y, boolean outlined) {
|
||||
int width = CHIP_PADDING * 2 + (int) (Fonts.getWidth(text, Theme.FONT.value) * CHIP_SCALE);
|
||||
|
||||
if (x + width > this.width) {
|
||||
return x;
|
||||
}
|
||||
|
||||
if (outlined) {
|
||||
renderScope.drawRoundedBorder(x, y, width, CHIP_HEIGHT, 6, 1, Theme.withAlpha(0.14f, Theme.FOREGROUND.value));
|
||||
} else {
|
||||
renderScope.drawRoundedRectangle(x, y, width, CHIP_HEIGHT, 6, Theme.withAlpha(0.07f, Theme.FOREGROUND.value));
|
||||
}
|
||||
|
||||
int textY = y + (CHIP_HEIGHT - (int) (Fonts.getHeight() * CHIP_SCALE)) / 2;
|
||||
renderScope.drawText(CHIP_SCALE, text, x + CHIP_PADDING, textY, Theme.FONT.value,
|
||||
outlined ? Theme.PRIMARY_FG.value : Theme.getMutedFg());
|
||||
|
||||
return x + width;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package org.saturnclient.ui.components;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.config.property.BoolProperty;
|
||||
import org.saturnclient.config.property.ColorProperty;
|
||||
import org.saturnclient.config.property.FloatProperty;
|
||||
import org.saturnclient.config.property.IntProperty;
|
||||
import org.saturnclient.config.property.KeybindingProperty;
|
||||
import org.saturnclient.config.property.NamespaceProperty;
|
||||
import org.saturnclient.config.property.Property;
|
||||
import org.saturnclient.config.property.SelectProperty;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.components.inputs.ColorInput;
|
||||
import org.saturnclient.ui.components.inputs.FloatInput;
|
||||
import org.saturnclient.ui.components.inputs.IntInput;
|
||||
import org.saturnclient.ui.components.inputs.KeybindingSelector;
|
||||
import org.saturnclient.ui.components.inputs.Select;
|
||||
import org.saturnclient.ui.components.inputs.Slider;
|
||||
import org.saturnclient.ui.components.inputs.Toggle;
|
||||
import org.saturnclient.ui.elements.Label;
|
||||
import org.saturnclient.ui.elements.Scroll;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
|
||||
/**
|
||||
* Lays out config properties as rows of name and control, one per property.
|
||||
* A nested namespace becomes a titled section.
|
||||
*/
|
||||
public final class PropertyList {
|
||||
public static final int ROW_HEIGHT = 56;
|
||||
|
||||
private static final int SECTION_GAP = 18;
|
||||
private static final float SECTION_SCALE = 0.6f;
|
||||
|
||||
private PropertyList() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds rows for {@code properties} to {@code scroll}, starting at {@code y}.
|
||||
*
|
||||
* @param skip names of top-level properties to leave out
|
||||
* @return the y below the last row
|
||||
*/
|
||||
public static int draw(Scroll scroll, Map<String, Property> properties, Set<String> skip, int width, int y) {
|
||||
for (Map.Entry<String, Property> entry : properties.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
Property prop = entry.getValue();
|
||||
|
||||
if (skip.contains(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prop instanceof NamespaceProperty namespace) {
|
||||
y = drawSection(scroll, name, y + SECTION_GAP) + 4;
|
||||
y = draw(scroll, namespace.value, Set.of(), width, y);
|
||||
continue;
|
||||
}
|
||||
|
||||
Element control = createControl(prop);
|
||||
if (control == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
scroll.draw(new Row(name, prop.description, width, control.width).position(0, y));
|
||||
scroll.draw(control.position(width - control.width, y + (ROW_HEIGHT - control.height) / 2));
|
||||
y += ROW_HEIGHT;
|
||||
}
|
||||
|
||||
return y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a section title, such as a namespace's name, at {@code y}.
|
||||
*
|
||||
* @return the y below the title
|
||||
*/
|
||||
public static int drawSection(Scroll scroll, String name, int y) {
|
||||
String title = name.toUpperCase(Locale.ROOT);
|
||||
scroll.draw(new Label(() -> title, SECTION_SCALE, () -> Theme.FONT.value, Theme::getMutedFg).position(0, y));
|
||||
return y + (int) (Fonts.getHeight() * SECTION_SCALE);
|
||||
}
|
||||
|
||||
/**
|
||||
* The input for a property, or null for types without one (strings).
|
||||
*/
|
||||
private static Element createControl(Property prop) {
|
||||
if (prop instanceof BoolProperty p) {
|
||||
return new Toggle(p);
|
||||
} else if (prop instanceof ColorProperty p) {
|
||||
return new ColorInput(p);
|
||||
} else if (prop instanceof IntProperty p) {
|
||||
return p.hasRange() ? Slider.of(p) : new IntInput(p);
|
||||
} else if (prop instanceof FloatProperty p) {
|
||||
return p.hasRange() ? Slider.of(p) : new FloatInput(p);
|
||||
} else if (prop instanceof SelectProperty p) {
|
||||
return new Select(p);
|
||||
} else if (prop instanceof KeybindingProperty p) {
|
||||
return new KeybindingSelector(p);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A row's name, its description under it, and the line below the row. The
|
||||
* control is a separate element on top, so it gets the clicks and key
|
||||
* presses.
|
||||
*/
|
||||
private static class Row extends Element {
|
||||
private static final float NAME_SCALE = 0.74f;
|
||||
private static final float DESCRIPTION_SCALE = 0.62f;
|
||||
private static final int LINE_GAP = 4;
|
||||
|
||||
private final String name;
|
||||
private final String description;
|
||||
private final int controlWidth;
|
||||
|
||||
Row(String name, String description, int width, int controlWidth) {
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.controlWidth = controlWidth;
|
||||
this.width = width;
|
||||
this.height = ROW_HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
int textWidth = width - controlWidth - 16;
|
||||
int nameHeight = (int) (Fonts.getHeight() * NAME_SCALE);
|
||||
int descriptionHeight = (int) (Fonts.getHeight() * DESCRIPTION_SCALE);
|
||||
boolean described = !description.isEmpty();
|
||||
|
||||
int blockHeight = described ? nameHeight + LINE_GAP + descriptionHeight : nameHeight;
|
||||
int nameY = (height - blockHeight) / 2;
|
||||
|
||||
renderScope.drawText(NAME_SCALE, Fonts.ellipsize(name, Theme.FONT.value, NAME_SCALE, textWidth),
|
||||
0, nameY, Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
|
||||
if (described) {
|
||||
int font = Theme.getBodyFont();
|
||||
renderScope.drawText(DESCRIPTION_SCALE,
|
||||
Fonts.ellipsize(description, font, DESCRIPTION_SCALE, textWidth),
|
||||
0, nameY + nameHeight + LINE_GAP, font, Theme.getMutedFg());
|
||||
}
|
||||
|
||||
renderScope.drawRectangle(0, height - 1, width, 1, Theme.withAlpha(0.06f, Theme.FOREGROUND.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,165 +2,119 @@ package org.saturnclient.ui.components;
|
||||
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.mod.HudMod;
|
||||
import org.saturnclient.mod.Mod;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.Utils;
|
||||
import org.saturnclient.ui.components.inputs.Toggle;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
import org.saturnclient.ui.screens.ConfigEditor;
|
||||
import org.saturnclient.ui.screens.ModSettings;
|
||||
|
||||
/**
|
||||
* A mod's card in the mod menu: icon, name, description, a settings button and
|
||||
* an on/off switch. Clicking anywhere but the settings button toggles the mod.
|
||||
*/
|
||||
public class SaturnModule extends Element {
|
||||
public static final int HEIGHT = 76;
|
||||
|
||||
private static final int PADDING = 12;
|
||||
private static final int ICON_TILE = 44;
|
||||
private static final int ICON = 22;
|
||||
private static final int SETTINGS = 32;
|
||||
private static final int SETTINGS_ICON = 17;
|
||||
private static final int GAP = 12;
|
||||
private static final float NAME_SCALE = 0.74f;
|
||||
private static final float DESCRIPTION_SCALE = 0.64f;
|
||||
|
||||
private final Mod mod;
|
||||
|
||||
static int p = 10; // padding
|
||||
static int s = 14; // settings icon size
|
||||
static int expand = 5; // settings icon click area
|
||||
|
||||
public SaturnModule(Mod mod) {
|
||||
public SaturnModule(Mod mod, int width) {
|
||||
this.mod = mod;
|
||||
this.width = 160;
|
||||
this.height = 50;
|
||||
this.width = width;
|
||||
this.height = HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
boolean hovered = ctx.isHovering();
|
||||
boolean enabled = mod.isEnabled();
|
||||
boolean settingsHovered = ctx.isHovering(getSettingsX(), getSettingsY(), SETTINGS, SETTINGS);
|
||||
boolean hovered = ctx.isHovering() && !settingsHovered;
|
||||
|
||||
int iconX = width - s - p;
|
||||
int iconY = height - s - p;
|
||||
int border = enabled ? Theme.withAlpha(hovered ? 0.7f : 0.4f, Theme.ACCENT.value)
|
||||
: Theme.withAlpha(hovered ? 0.22f : 0.06f, Theme.FOREGROUND.value);
|
||||
|
||||
boolean settingsHover = ctx.isHovering(
|
||||
getSettingsX(),
|
||||
getSettingsY(),
|
||||
getSettingsHitBox(),
|
||||
getSettingsHitBox());
|
||||
// Card
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
|
||||
Theme.PRIMARY.value, border);
|
||||
|
||||
// Resolve theme colors
|
||||
int bgColor = Theme.PRIMARY.value;
|
||||
int fgColor = hovered && !settingsHover ? Theme.ACCENT.value : Theme.PRIMARY_FG.value;
|
||||
|
||||
int iconBg = enabled ? Theme.ACCENT.value : Theme.PRIMARY.value;
|
||||
int iconFg = enabled ? Theme.ACCENT_FG.value : fgColor;
|
||||
|
||||
int settingsColor = settingsHover
|
||||
? Theme.ACCENT.value
|
||||
: Theme.withAlpha(160, fgColor);
|
||||
|
||||
int h = height - (p * 2);
|
||||
|
||||
// Main background
|
||||
renderScope.drawRoundedRectangle(
|
||||
0, 0,
|
||||
width, height,
|
||||
Theme.WIDGET_RADIUS.value,
|
||||
bgColor);
|
||||
|
||||
// Icon background
|
||||
renderScope.drawRoundedRectangle(
|
||||
p, p,
|
||||
h, h,
|
||||
Theme.WIDGET_RADIUS.value,
|
||||
iconBg);
|
||||
|
||||
// Icon
|
||||
renderScope.drawTexture(
|
||||
mod.getIconTexture(),
|
||||
p + (p / 2),
|
||||
p + (p / 2),
|
||||
0, 0,
|
||||
h - p,
|
||||
h - p,
|
||||
iconFg);
|
||||
|
||||
// Module name
|
||||
renderScope.drawText(
|
||||
0.6f,
|
||||
mod.getName(),
|
||||
p + h + 4,
|
||||
p + 1,
|
||||
Theme.FONT.value,
|
||||
fgColor);
|
||||
|
||||
// Version (right aligned)
|
||||
String version = mod.getVersion();
|
||||
int versionWidth = (int) (Fonts.getWidth(version, Theme.FONT.value) * 0.35f);
|
||||
|
||||
renderScope.drawText(
|
||||
0.35f,
|
||||
version,
|
||||
width - p - versionWidth,
|
||||
p + 1,
|
||||
Theme.FONT.value,
|
||||
fgColor);
|
||||
|
||||
// Tags
|
||||
int xt = p + h + 4;
|
||||
for (String tag : mod.getTags()) {
|
||||
int tagWidth = (int) (Fonts.getWidth(tag, Theme.FONT.value) * 0.4f);
|
||||
|
||||
renderScope.drawRoundedRectangle(
|
||||
xt,
|
||||
p + (h / 2),
|
||||
tagWidth + 6,
|
||||
12,
|
||||
5,
|
||||
Theme.ACCENT.value);
|
||||
|
||||
renderScope.drawText(
|
||||
0.4f,
|
||||
tag,
|
||||
xt + 2,
|
||||
p + (h / 2) + 3,
|
||||
Theme.FONT.value,
|
||||
Theme.ACCENT_FG.value);
|
||||
|
||||
xt += tagWidth + 9;
|
||||
if (enabled) {
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value,
|
||||
Theme.withAlpha(0.06f, Theme.ACCENT.value));
|
||||
}
|
||||
|
||||
// Settings icon
|
||||
renderScope.drawTexture(
|
||||
Textures.SETTINGS,
|
||||
iconX,
|
||||
iconY,
|
||||
0, 0,
|
||||
s, s,
|
||||
settingsColor);
|
||||
// Icon
|
||||
int tileY = (height - ICON_TILE) / 2;
|
||||
renderScope.drawRoundedRectangle(PADDING, tileY, ICON_TILE, ICON_TILE, Theme.WIDGET_RADIUS.value,
|
||||
enabled ? Theme.withAlpha(0.18f, Theme.ACCENT.value) : Theme.withAlpha(0.06f, Theme.FOREGROUND.value));
|
||||
|
||||
renderScope.drawTexture(mod.getIconTexture(),
|
||||
PADDING + (ICON_TILE - ICON) / 2, tileY + (ICON_TILE - ICON) / 2,
|
||||
0, 0, ICON, ICON, Theme.PRIMARY_FG.value);
|
||||
|
||||
// Name and description
|
||||
int textX = PADDING + ICON_TILE + GAP;
|
||||
int textWidth = getSettingsX() - GAP - textX;
|
||||
int nameHeight = (int) (Fonts.getHeight() * NAME_SCALE);
|
||||
int descriptionHeight = (int) (Fonts.getHeight() * DESCRIPTION_SCALE);
|
||||
int nameY = (height - nameHeight - 5 - descriptionHeight) / 2;
|
||||
|
||||
renderScope.drawText(NAME_SCALE,
|
||||
Fonts.ellipsize(mod.getName(), Theme.FONT.value, NAME_SCALE, textWidth),
|
||||
textX, nameY, Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
|
||||
String description = mod.getDescription() == null ? "" : mod.getDescription();
|
||||
renderScope.drawText(DESCRIPTION_SCALE,
|
||||
Fonts.ellipsize(description, Theme.getBodyFont(), DESCRIPTION_SCALE, textWidth),
|
||||
textX, nameY + nameHeight + 5, Theme.getBodyFont(), Theme.getMutedFg());
|
||||
|
||||
// Settings button
|
||||
if (settingsHovered) {
|
||||
renderScope.drawRoundedRectangle(getSettingsX(), getSettingsY(), SETTINGS, SETTINGS,
|
||||
Theme.WIDGET_RADIUS.value, Theme.withAlpha(0.08f, Theme.FOREGROUND.value));
|
||||
}
|
||||
|
||||
int settingsIconOffset = (SETTINGS - SETTINGS_ICON) / 2;
|
||||
renderScope.drawTexture(Textures.ICON_SLIDERS,
|
||||
getSettingsX() + settingsIconOffset, getSettingsY() + settingsIconOffset,
|
||||
0, 0, SETTINGS_ICON, SETTINGS_ICON,
|
||||
settingsHovered ? Theme.ACCENT.value : Theme.getMutedFg());
|
||||
|
||||
// Switch
|
||||
Toggle.drawSwitch(renderScope, getSwitchX(), (height - Toggle.SWITCH_HEIGHT) / 2, enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
boolean settingsHover = Utils.isHovering(
|
||||
mouseX - getSettingsX(),
|
||||
mouseY - getSettingsY(),
|
||||
getSettingsHitBox(),
|
||||
getSettingsHitBox(),
|
||||
1.0f);
|
||||
boolean settingsClicked = mouseX >= getSettingsX() && mouseX <= getSettingsX() + SETTINGS
|
||||
&& mouseY >= getSettingsY() && mouseY <= getSettingsY() + SETTINGS;
|
||||
|
||||
if (settingsHover) {
|
||||
if (mod instanceof HudMod h) {
|
||||
Providers.saturn.getClient().setScreen(new ConfigEditor(mod.getConfig(), h));
|
||||
} else {
|
||||
Providers.saturn.getClient().setScreen(new ConfigEditor(mod.getConfig()));
|
||||
}
|
||||
if (settingsClicked) {
|
||||
Providers.saturn.getClient().setScreen(new ModSettings(mod));
|
||||
} else {
|
||||
mod.setEnabled(!mod.isEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
private int getSwitchX() {
|
||||
return width - PADDING - Toggle.SWITCH_WIDTH;
|
||||
}
|
||||
|
||||
private int getSettingsX() {
|
||||
return width - s - p - expand;
|
||||
return getSwitchX() - 8 - SETTINGS;
|
||||
}
|
||||
|
||||
private int getSettingsY() {
|
||||
return height - s - p - expand;
|
||||
return (height - SETTINGS) / 2;
|
||||
}
|
||||
|
||||
private int getSettingsHitBox() {
|
||||
return s + expand * 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.saturnclient.ui.components;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
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 vertical list of a screen's sections under a small heading, with the
|
||||
* current one highlighted.
|
||||
*/
|
||||
public class SectionNav extends Element {
|
||||
public record Item(IdentifierRef icon, String name) {
|
||||
}
|
||||
|
||||
private static final float HEADING_SCALE = 0.6f;
|
||||
private static final int HEADING_HEIGHT = 26;
|
||||
private static final int ITEM_HEIGHT = 40;
|
||||
private static final int ITEM_GAP = 4;
|
||||
private static final int ICON = 17;
|
||||
private static final float TEXT_SCALE = 0.7f;
|
||||
|
||||
private final String heading;
|
||||
private final List<Item> items;
|
||||
private final int active;
|
||||
private final IntConsumer onSelect;
|
||||
|
||||
public SectionNav(String heading, List<Item> items, int active, int width, IntConsumer onSelect) {
|
||||
this.heading = heading;
|
||||
this.items = items;
|
||||
this.active = active;
|
||||
this.onSelect = onSelect;
|
||||
this.width = width;
|
||||
this.height = HEADING_HEIGHT + items.size() * (ITEM_HEIGHT + ITEM_GAP);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawText(HEADING_SCALE, heading, 10, 0, Theme.FONT.value, Theme.getMutedFg());
|
||||
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
Item item = items.get(i);
|
||||
int y = getItemY(i);
|
||||
boolean selected = i == active;
|
||||
boolean hovered = ctx.isHovering(0, y, width, ITEM_HEIGHT);
|
||||
|
||||
if (selected || hovered) {
|
||||
renderScope.drawRoundedRectangle(0, y, width, ITEM_HEIGHT, Theme.WIDGET_RADIUS.value,
|
||||
Theme.withAlpha(selected ? 0.08f : 0.04f, Theme.FOREGROUND.value));
|
||||
}
|
||||
|
||||
renderScope.drawTexture(item.icon(), 12, y + (ITEM_HEIGHT - ICON) / 2, 0, 0, ICON, ICON,
|
||||
selected ? Theme.ACCENT.value : Theme.getMutedFg());
|
||||
|
||||
String name = Fonts.ellipsize(item.name(), Theme.FONT.value, TEXT_SCALE, width - 12 - ICON - 10 - 8);
|
||||
renderScope.drawText(TEXT_SCALE, name, 12 + ICON + 10, y + Field.textY(ITEM_HEIGHT, TEXT_SCALE),
|
||||
Theme.FONT.value, selected ? Theme.FOREGROUND.value : Theme.PRIMARY_FG.value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
int y = getItemY(i);
|
||||
|
||||
if (mouseY >= y && mouseY <= y + ITEM_HEIGHT) {
|
||||
if (i != active) {
|
||||
onSelect.accept(i);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getItemY(int index) {
|
||||
return HEADING_HEIGHT + index * (ITEM_HEIGHT + ITEM_GAP);
|
||||
}
|
||||
}
|
||||
@@ -2,56 +2,52 @@ package org.saturnclient.ui.components;
|
||||
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.config.Config;
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.Utils;
|
||||
import org.saturnclient.ui.elements.Panel;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
import org.saturnclient.ui.screens.ConfigEditor;
|
||||
import org.saturnclient.ui.screens.ClientSettings;
|
||||
import org.saturnclient.ui.screens.HudEditor;
|
||||
import org.saturnclient.ui.screens.ModMenu;
|
||||
import org.saturnclient.ui.screens.cosmetics.CloakMenu;
|
||||
import org.saturnclient.ui.screens.store.CloakStore;
|
||||
import org.saturnclient.ui.screens.CosmeticsMenu;
|
||||
|
||||
/**
|
||||
* The navigation rail beside Saturn's screens: the logo, one button per screen,
|
||||
* and a close button at the bottom.
|
||||
*/
|
||||
public class Sidebar extends Element {
|
||||
public static class SidebarComponent {
|
||||
Runnable onClick;
|
||||
boolean end;
|
||||
IdentifierRef sprite;
|
||||
public static final int WIDTH = 60;
|
||||
|
||||
public SidebarComponent(IdentifierRef sprite, Runnable onClick, boolean end) {
|
||||
this.onClick = onClick;
|
||||
this.end = end;
|
||||
this.sprite = sprite;
|
||||
}
|
||||
// Values for the constructor's `active`
|
||||
public static final int MODS = 0;
|
||||
public static final int SETTINGS = 1;
|
||||
public static final int COSMETICS = 2;
|
||||
public static final int HUD_EDITOR = 3;
|
||||
|
||||
private static final int LOGO = 32;
|
||||
private static final int BUTTON = 44;
|
||||
private static final int ICON = 22;
|
||||
private static final int GAP = 8;
|
||||
private static final int PADDING = 12;
|
||||
|
||||
private record Item(IdentifierRef icon, Runnable onClick) {
|
||||
}
|
||||
|
||||
Runnable onClose;
|
||||
SidebarComponent[] components = {
|
||||
new Sidebar.SidebarComponent(Textures.MODS_TAB, () -> {
|
||||
Providers.saturn.getClient().setScreen(new ModMenu());
|
||||
}, false),
|
||||
new Sidebar.SidebarComponent(Textures.SETTINGS, () -> {
|
||||
Providers.saturn.getClient().setScreen(new ConfigEditor(Config.config));
|
||||
}, false),
|
||||
|
||||
new Sidebar.SidebarComponent(Textures.SHIRT, () -> {
|
||||
Providers.saturn.getClient().setScreen(new CloakMenu());
|
||||
}, false),
|
||||
|
||||
new Sidebar.SidebarComponent(Textures.CLOSE, () -> {
|
||||
onClose.run();
|
||||
}, true),
|
||||
|
||||
new Sidebar.SidebarComponent(Textures.STORE, () -> {
|
||||
Providers.saturn.getClient().setScreen(new CloakStore());
|
||||
}, true),
|
||||
// In the order of the constants above
|
||||
private final Item[] items = {
|
||||
new Item(Textures.ICON_MODS, () -> Providers.saturn.getClient().setScreen(new ModMenu())),
|
||||
new Item(Textures.ICON_SETTINGS, () -> Providers.saturn.getClient().setScreen(new ClientSettings())),
|
||||
new Item(Textures.ICON_COSMETICS, () -> Providers.saturn.getClient().setScreen(new CosmeticsMenu())),
|
||||
new Item(Textures.ICON_HUD_EDITOR, () -> Providers.saturn.getClient().setScreen(new HudEditor())),
|
||||
};
|
||||
int active = 0;
|
||||
|
||||
private final int active;
|
||||
private final Runnable onClose;
|
||||
|
||||
public Sidebar(int active, Runnable onClose) {
|
||||
this.width = 30;
|
||||
this.width = WIDTH;
|
||||
this.height = 350;
|
||||
this.active = active;
|
||||
this.onClose = onClose;
|
||||
@@ -59,63 +55,75 @@ public class Sidebar extends Element {
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
int topRow = 0;
|
||||
int bottomRow = 0;
|
||||
Panel.drawBackground(renderScope, width, height);
|
||||
|
||||
int gap = 5; // 5px gap between icons
|
||||
int padding = 10;
|
||||
int sp = 30 - padding + gap;
|
||||
int s = 30 - (padding * 2);
|
||||
int s2 = s + padding;
|
||||
renderScope.drawTexture(Textures.LOGO, (width - LOGO) / 2, PADDING, 0, 0, LOGO, LOGO);
|
||||
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.BG_RADIUS.value, Theme.BACKGROUND.value);
|
||||
int x = getButtonX();
|
||||
|
||||
int i = 0;
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
int y = getItemY(i);
|
||||
boolean selected = i == active;
|
||||
boolean hovered = ctx.isHovering(x, y, BUTTON, BUTTON);
|
||||
|
||||
for (SidebarComponent component : components) {
|
||||
int y = component.end ? ((height - (sp * bottomRow)) - (s * 2)) : (padding + (sp * topRow));
|
||||
boolean selected = ctx.isHovering(0, y - (padding / 2), s2, s2) || active == i;
|
||||
|
||||
renderScope.drawRoundedRectangle(padding / 2, y - (padding / 2), s + padding,
|
||||
s + padding, Theme.WIDGET_RADIUS.value, Theme.getBg(selected));
|
||||
|
||||
renderScope.drawTexture(component.sprite, padding, y, 0, 0, s, s, Theme.getFg(selected));
|
||||
|
||||
if (component.end) {
|
||||
bottomRow += 1;
|
||||
} else {
|
||||
topRow += 1;
|
||||
if (selected) {
|
||||
renderScope.drawRoundedRectangle(x, y, BUTTON, BUTTON, Theme.WIDGET_RADIUS.value, Theme.ACCENT.value);
|
||||
} else if (hovered) {
|
||||
renderScope.drawRoundedRectangle(x, y, BUTTON, BUTTON, Theme.WIDGET_RADIUS.value,
|
||||
Theme.withAlpha(0.08f, Theme.FOREGROUND.value));
|
||||
}
|
||||
|
||||
i++;
|
||||
drawIcon(renderScope, items[i].icon(), x, y,
|
||||
selected ? Theme.ACCENT_FG.value
|
||||
: hovered ? Theme.FOREGROUND.value : Theme.withAlpha(0.8f, Theme.FOREGROUND.value));
|
||||
}
|
||||
|
||||
int closeY = getCloseY();
|
||||
boolean closeHovered = ctx.isHovering(x, closeY, BUTTON, BUTTON);
|
||||
|
||||
renderScope.drawRoundedRectangle(x, closeY, BUTTON, BUTTON, Theme.WIDGET_RADIUS.value,
|
||||
Theme.withAlpha(closeHovered ? 0.12f : 0.06f, Theme.FOREGROUND.value));
|
||||
|
||||
drawIcon(renderScope, Textures.ICON_CLOSE, x, closeY,
|
||||
closeHovered ? Theme.FOREGROUND.value : Theme.withAlpha(0.8f, Theme.FOREGROUND.value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
int topRow = 0;
|
||||
int bottomRow = 0;
|
||||
int x = getButtonX();
|
||||
|
||||
int gap = 5;
|
||||
int padding = 10;
|
||||
int sp = 30 - padding + gap;
|
||||
int s = 30 - (padding * 2);
|
||||
int s2 = s + padding;
|
||||
if (mouseX < x || mouseX > x + BUTTON) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (SidebarComponent component : components) {
|
||||
int y = component.end ? ((height - (sp * bottomRow)) - (s * 2)) : (padding + (sp * topRow));
|
||||
boolean hovering = Utils.isHovering(mouseX - (padding / 2), mouseY - (y - (padding / 2)), s2, s2, scale);
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
int y = getItemY(i);
|
||||
|
||||
if (hovering) {
|
||||
component.onClick.run();
|
||||
break;
|
||||
}
|
||||
|
||||
if (component.end) {
|
||||
bottomRow += 1;
|
||||
} else {
|
||||
topRow += 1;
|
||||
if (mouseY >= y && mouseY <= y + BUTTON) {
|
||||
items[i].onClick().run();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mouseY >= getCloseY() && mouseY <= getCloseY() + BUTTON) {
|
||||
onClose.run();
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawIcon(RenderScope renderScope, IdentifierRef icon, int x, int y, int color) {
|
||||
int offset = (BUTTON - ICON) / 2;
|
||||
renderScope.drawTexture(icon, x + offset, y + offset, 0, 0, ICON, ICON, color);
|
||||
}
|
||||
|
||||
private int getButtonX() {
|
||||
return (width - BUTTON) / 2;
|
||||
}
|
||||
|
||||
private int getItemY(int index) {
|
||||
return PADDING + LOGO + 14 + index * (BUTTON + GAP);
|
||||
}
|
||||
|
||||
private int getCloseY() {
|
||||
return height - PADDING - BUTTON;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,68 @@
|
||||
package org.saturnclient.ui.components;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* The player, wearing their cosmetics, filling the element. Dragging it turns
|
||||
* the player around.
|
||||
*/
|
||||
public class SkinPreview extends Element {
|
||||
public static EntityDrawer DRAWER;
|
||||
private boolean negativeAngle = false;
|
||||
private float angle = 0.0f;
|
||||
|
||||
public SkinPreview(float angle, boolean negativeAngle) {
|
||||
this.angle = angle;
|
||||
this.negativeAngle = negativeAngle;
|
||||
/** Degrees turned per GUI pixel dragged */
|
||||
private static final float DRAG_SPEED = 1.2f;
|
||||
private static final float HINT_SCALE = 0.66f;
|
||||
|
||||
private float yaw;
|
||||
private boolean dragging = false;
|
||||
|
||||
/**
|
||||
* @param yaw degrees the player starts turned by: 0 faces the viewer, 180
|
||||
* shows their back
|
||||
*/
|
||||
public SkinPreview(float yaw) {
|
||||
this.yaw = yaw;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
DRAWER.render(renderScope, negativeAngle, angle, scale, ctx);
|
||||
if (DRAWER != null && DRAWER.render(renderScope, width, height, yaw)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String hint = "Join a world to preview";
|
||||
int x = (width - (int) (Fonts.getWidth(hint, Theme.getBodyFont()) * HINT_SCALE)) / 2;
|
||||
renderScope.drawText(HINT_SCALE, hint, x, height / 2, Theme.getBodyFont(), Theme.getMutedFg());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(double mouseX, double mouseY, int button) {
|
||||
dragging = mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
|
||||
if (dragging) {
|
||||
yaw = (yaw - (float) deltaX * DRAG_SPEED) % 360;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(double mouseX, double mouseY, int button) {
|
||||
dragging = false;
|
||||
}
|
||||
|
||||
public static interface EntityDrawer {
|
||||
public void render(RenderScope renderScope, boolean negativeAngle, float angle, float scale,
|
||||
ElementContext ctx);
|
||||
/**
|
||||
* Draws the client's player filling a {@code width} x {@code height} box,
|
||||
* turned by {@code yaw} degrees (0 faces the viewer).
|
||||
*
|
||||
* @return false if there's no player to draw, such as on the title screen
|
||||
*/
|
||||
public boolean render(RenderScope renderScope, int width, int height, float yaw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.saturnclient.ui.components;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* A theme preset: a small mock-up of a panel in its colors, and its name.
|
||||
* Clicking it applies the preset.
|
||||
*/
|
||||
public class ThemePresetCard extends Element {
|
||||
public static final int HEIGHT = 92;
|
||||
|
||||
private static final int PADDING = 10;
|
||||
private static final int PREVIEW_HEIGHT = 44;
|
||||
private static final float NAME_SCALE = 0.68f;
|
||||
|
||||
private final Theme.Preset preset;
|
||||
private final Runnable onApply;
|
||||
|
||||
/**
|
||||
* @param onApply runs after the preset is applied, to refresh what shows the
|
||||
* old colors
|
||||
*/
|
||||
public ThemePresetCard(Theme.Preset preset, int width, Runnable onApply) {
|
||||
this.preset = preset;
|
||||
this.onApply = onApply;
|
||||
this.width = width;
|
||||
this.height = HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
boolean active = preset.isActive();
|
||||
int border = active ? Theme.ACCENT.value
|
||||
: Theme.withAlpha(ctx.isHovering() ? 0.22f : 0.06f, Theme.FOREGROUND.value);
|
||||
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, active ? 2 : 1,
|
||||
Theme.PRIMARY.value, border);
|
||||
|
||||
// A panel in the preset's colors: a line of text over an accent and a widget
|
||||
int previewWidth = width - PADDING * 2;
|
||||
renderScope.drawRoundedRectangle(PADDING, PADDING, previewWidth, PREVIEW_HEIGHT, 8, preset.background());
|
||||
renderScope.drawRoundedRectangle(PADDING + 8, PADDING + 8, previewWidth * 6 / 10, 5, 3,
|
||||
Theme.withAlpha(0.8f, preset.foreground()));
|
||||
renderScope.drawRoundedRectangle(PADDING + 8, PADDING + PREVIEW_HEIGHT - 20, 34, 12, 4, preset.accent());
|
||||
renderScope.drawRoundedRectangle(PADDING + 47, PADDING + PREVIEW_HEIGHT - 20, 22, 12, 4, preset.primary());
|
||||
|
||||
int nameY = PADDING + PREVIEW_HEIGHT + (height - PADDING - PREVIEW_HEIGHT - (int) (Fonts.getHeight() * NAME_SCALE)) / 2;
|
||||
renderScope.drawText(NAME_SCALE, preset.name(), PADDING, nameY, Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
preset.apply();
|
||||
onApply.run();
|
||||
}
|
||||
}
|
||||
@@ -1,183 +1,62 @@
|
||||
package org.saturnclient.ui.components.inputs;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.config.property.ColorProperty;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.Popup;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.elements.Field;
|
||||
|
||||
/**
|
||||
* Shows a color as a swatch and its hex code, and opens a {@link ColorPicker}
|
||||
* beside it when clicked.
|
||||
*/
|
||||
public class ColorInput extends Element {
|
||||
private static final int SWATCH = 20;
|
||||
private static final int PICKER_GAP = 8;
|
||||
|
||||
public ColorProperty prop;
|
||||
|
||||
// Picker state
|
||||
private boolean open = false;
|
||||
private float hue = 0f;
|
||||
private float opacity = 1f;
|
||||
private int selectedX = 0;
|
||||
private int selectedY = 0;
|
||||
|
||||
private boolean draggingOpacity = false;
|
||||
|
||||
// Dimensions
|
||||
private static final int PICKER_WIDTH = 100;
|
||||
private static final int PICKER_HEIGHT = 100;
|
||||
private static final int SLIDER_WIDTH = 100;
|
||||
private static final int SLIDER_HEIGHT = 10;
|
||||
private static final int HUE_SLIDER_HEIGHT = 10;
|
||||
/** Where this was last drawn on the screen, to open the picker beside it */
|
||||
private int originX, originY;
|
||||
|
||||
public ColorInput(ColorProperty prop) {
|
||||
this.prop = prop;
|
||||
this.width = 58;
|
||||
this.height = 20;
|
||||
syncFromColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
open = !open;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(double mouseX, double mouseY, int button) {
|
||||
if (open) {
|
||||
handlePickerClick(mouseX - width - 8, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
|
||||
if (open) {
|
||||
handlePickerDrag(mouseX - width - 8, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(double mouseX, double mouseY, int button) {
|
||||
draggingOpacity = false;
|
||||
this.width = 116;
|
||||
this.height = Field.HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
// Draw the main color rectangle
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, 6, prop.value);
|
||||
originX = ctx.originX;
|
||||
originY = ctx.originY;
|
||||
|
||||
if (!open)
|
||||
return;
|
||||
Field.drawBox(renderScope, width, height, ctx.isHovering(), Popup.isOpenFor(this));
|
||||
|
||||
// Draw the picker relative to (width + 8, 0)
|
||||
int pickerX = width + 8;
|
||||
int pickerY = 0;
|
||||
int swatchInset = (height - SWATCH) / 2;
|
||||
renderScope.drawRoundedRectangle(swatchInset, swatchInset, SWATCH, SWATCH, 5, 1, prop.value,
|
||||
Theme.withAlpha(0.3f, Theme.FOREGROUND.value));
|
||||
|
||||
renderColorSquare(renderScope, pickerX, pickerY);
|
||||
renderOpacitySlider(renderScope, pickerX, pickerY - SLIDER_HEIGHT - 10);
|
||||
renderHueSlider(renderScope, pickerX, pickerY + PICKER_HEIGHT + 10);
|
||||
renderScope.drawText(Field.TEXT_SCALE, toHex(prop.value), swatchInset + SWATCH + 8,
|
||||
Field.textY(height, Field.TEXT_SCALE), Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
}
|
||||
|
||||
private void renderColorSquare(RenderScope renderScope, int xStart, int yStart) {
|
||||
for (int x = 0; x < PICKER_WIDTH; x++) {
|
||||
for (int y = 0; y < PICKER_HEIGHT; y++) {
|
||||
float saturation = x / (float) PICKER_WIDTH;
|
||||
float value = 1f - y / (float) PICKER_HEIGHT;
|
||||
int color = Color.HSBtoRGB(hue, saturation, value);
|
||||
int alpha = (int) (opacity * 255) & 0xFF;
|
||||
color = (alpha << 24) | (color & 0xFFFFFF);
|
||||
renderScope.fill(xStart + x, yStart + y, xStart + x + 1, yStart + y + 1, color);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw selection square
|
||||
int selX = xStart + selectedX - 4;
|
||||
int selY = yStart + selectedY - 4;
|
||||
renderScope.fill(selX, selY, selX + 8, selY + 8, 0xFF000000 | prop.value);
|
||||
renderScope.drawBorder(selX, selY, 8, 8, 0xFFFFFFFF);
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
// The input sits at the right of its row, so the picker opens to its left,
|
||||
// centered on it. While it's open, a click here closes it instead.
|
||||
ColorPicker picker = new ColorPicker(prop);
|
||||
Popup.open(this, picker, originX - PICKER_GAP - picker.width, originY + (height - picker.height) / 2);
|
||||
}
|
||||
|
||||
private void renderOpacitySlider(RenderScope renderScope, int xStart, int yStart) {
|
||||
for (int x = 0; x < SLIDER_WIDTH; x++) {
|
||||
float localOpacity = x / (float) SLIDER_WIDTH;
|
||||
int alpha = (int) (localOpacity * 255) & 0xFF;
|
||||
int color = (alpha << 24) | (prop.value & 0xFFFFFF);
|
||||
renderScope.fill(xStart + x, yStart, xStart + x + 1, yStart + SLIDER_HEIGHT, color);
|
||||
}
|
||||
|
||||
int knobX = xStart + (int) (opacity * SLIDER_WIDTH);
|
||||
renderScope.fill(knobX - 1, yStart - 2, knobX + 1, yStart + SLIDER_HEIGHT + 2, 0xFF000000);
|
||||
/**
|
||||
* #RRGGBB, or #AARRGGBB when the color is translucent.
|
||||
*/
|
||||
private static String toHex(int color) {
|
||||
int alpha = (color >>> 24) & 0xFF;
|
||||
return alpha == 0xFF
|
||||
? String.format("#%06X", color & 0xFFFFFF)
|
||||
: String.format("#%08X", color);
|
||||
}
|
||||
|
||||
private void renderHueSlider(RenderScope renderScope, int xStart, int yStart) {
|
||||
for (int x = 0; x < SLIDER_WIDTH; x++) {
|
||||
float localHue = x / (float) SLIDER_WIDTH;
|
||||
int color = Color.HSBtoRGB(localHue, 1f, 1f);
|
||||
renderScope.fill(xStart + x, yStart, xStart + x + 1, yStart + HUE_SLIDER_HEIGHT, 0xFF000000 | color);
|
||||
}
|
||||
|
||||
int knobX = xStart + (int) (hue * SLIDER_WIDTH);
|
||||
renderScope.fill(knobX - 1, yStart - 2, knobX + 1, yStart + HUE_SLIDER_HEIGHT + 2, 0xFF000000);
|
||||
}
|
||||
|
||||
private void handlePickerClick(double mouseX, double mouseY) {
|
||||
// Color square
|
||||
if (mouseX >= 0 && mouseX < PICKER_WIDTH && mouseY >= 0 && mouseY < PICKER_HEIGHT) {
|
||||
selectedX = (int) mouseX;
|
||||
selectedY = (int) mouseY;
|
||||
updateColorFromSelection();
|
||||
}
|
||||
|
||||
// Opacity slider
|
||||
int opY = -SLIDER_HEIGHT - 10;
|
||||
if (mouseX >= 0 && mouseX < SLIDER_WIDTH && mouseY >= opY && mouseY <= opY + SLIDER_HEIGHT) {
|
||||
updateOpacity(mouseX);
|
||||
draggingOpacity = true;
|
||||
}
|
||||
|
||||
// Hue slider
|
||||
int hueY = PICKER_HEIGHT + 10;
|
||||
if (mouseX >= 0 && mouseX < SLIDER_WIDTH && mouseY >= hueY && mouseY <= hueY + HUE_SLIDER_HEIGHT) {
|
||||
updateHue(mouseX);
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePickerDrag(double mouseX, double mouseY) {
|
||||
// Opacity
|
||||
if (draggingOpacity)
|
||||
updateOpacity(mouseX);
|
||||
|
||||
// Color square
|
||||
if (mouseX >= 0 && mouseX < PICKER_WIDTH && mouseY >= 0 && mouseY < PICKER_HEIGHT) {
|
||||
selectedX = (int) mouseX;
|
||||
selectedY = (int) mouseY;
|
||||
updateColorFromSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateColorFromSelection() {
|
||||
float saturation = selectedX / (float) PICKER_WIDTH;
|
||||
float value = 1f - selectedY / (float) PICKER_HEIGHT;
|
||||
int color = Color.HSBtoRGB(hue, saturation, value);
|
||||
int alpha = (int) (opacity * 255) & 0xFF;
|
||||
prop.value = (alpha << 24) | (color & 0xFFFFFF);
|
||||
}
|
||||
|
||||
private void updateHue(double mouseX) {
|
||||
hue = (float) Math.max(0, Math.min(1, mouseX / SLIDER_WIDTH));
|
||||
updateColorFromSelection();
|
||||
}
|
||||
|
||||
private void updateOpacity(double mouseX) {
|
||||
opacity = (float) Math.max(0, Math.min(1, mouseX / SLIDER_WIDTH));
|
||||
updateColorFromSelection();
|
||||
}
|
||||
|
||||
private void syncFromColor() {
|
||||
int rgb = prop.value & 0xFFFFFF;
|
||||
float[] hsb = Color.RGBtoHSB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF, null);
|
||||
hue = hsb[0];
|
||||
selectedX = (int) (hsb[1] * PICKER_WIDTH);
|
||||
selectedY = (int) ((1f - hsb[2]) * PICKER_HEIGHT);
|
||||
int alpha = (prop.value >> 24) & 0xFF;
|
||||
if (alpha == 0)
|
||||
alpha = 255;
|
||||
opacity = alpha / 255f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
package org.saturnclient.ui.components.inputs;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.config.property.ColorProperty;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
|
||||
/**
|
||||
* A color picker shown as a popup: opacity across the top, a
|
||||
* saturation/brightness square, and hue across the bottom. Each part can be
|
||||
* clicked or dragged.
|
||||
*/
|
||||
public class ColorPicker extends Element {
|
||||
private enum Part {
|
||||
OPACITY, SQUARE, HUE
|
||||
}
|
||||
|
||||
private static final int PADDING = 10;
|
||||
private static final int SIZE = 100;
|
||||
private static final int SLIDER_HEIGHT = 10;
|
||||
private static final int GAP = 10;
|
||||
|
||||
private static final int OPACITY_Y = PADDING;
|
||||
private static final int SQUARE_Y = OPACITY_Y + SLIDER_HEIGHT + GAP;
|
||||
private static final int HUE_Y = SQUARE_Y + SIZE + GAP;
|
||||
|
||||
// Gradient strips, made once and stretched over each part, so a frame is a
|
||||
// few draws instead of one per pixel. Tinting the white ones colors them.
|
||||
private static final int STRIP = 256;
|
||||
/** White fading out to the right: saturation, over the hue */
|
||||
private static final IdentifierRef SATURATION = IdentifierRef.ofSaturn("textures/gui/color_picker/saturation");
|
||||
/** Black fading in downwards: brightness, over the saturation */
|
||||
private static final IdentifierRef BRIGHTNESS = IdentifierRef.ofSaturn("textures/gui/color_picker/brightness");
|
||||
/** White fading in to the right, tinted with the color */
|
||||
private static final IdentifierRef OPACITY = IdentifierRef.ofSaturn("textures/gui/color_picker/opacity");
|
||||
private static final IdentifierRef HUE = IdentifierRef.ofSaturn("textures/gui/color_picker/hue");
|
||||
private static boolean stripsRegistered = false;
|
||||
|
||||
private final ColorProperty prop;
|
||||
|
||||
private float hue;
|
||||
private float saturation;
|
||||
private float brightness;
|
||||
private float opacity;
|
||||
|
||||
/** The part being dragged, or null */
|
||||
private Part dragging = null;
|
||||
|
||||
public ColorPicker(ColorProperty prop) {
|
||||
this.prop = prop;
|
||||
this.width = SIZE + PADDING * 2;
|
||||
this.height = HUE_Y + SLIDER_HEIGHT + PADDING;
|
||||
syncFromColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
registerStrips();
|
||||
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
|
||||
Theme.getPopupBg(), Theme.withAlpha(0.14f, Theme.FOREGROUND.value));
|
||||
|
||||
renderOpacity(renderScope);
|
||||
renderSquare(renderScope);
|
||||
renderHue(renderScope);
|
||||
}
|
||||
|
||||
private void renderOpacity(RenderScope renderScope) {
|
||||
renderScope.drawTexture(OPACITY, PADDING, OPACITY_Y, 0, 0, SIZE, SLIDER_HEIGHT,
|
||||
0xFF000000 | prop.value);
|
||||
|
||||
drawMarker(renderScope, PADDING + (int) (opacity * SIZE), OPACITY_Y);
|
||||
}
|
||||
|
||||
private void renderSquare(RenderScope renderScope) {
|
||||
renderScope.fill(PADDING, SQUARE_Y, PADDING + SIZE, SQUARE_Y + SIZE, 0xFF000000 | Color.HSBtoRGB(hue, 1f, 1f));
|
||||
renderScope.drawTexture(SATURATION, PADDING, SQUARE_Y, 0, 0, SIZE, SIZE, 0xFFFFFFFF);
|
||||
renderScope.drawTexture(BRIGHTNESS, PADDING, SQUARE_Y, 0, 0, SIZE, SIZE, 0xFFFFFFFF);
|
||||
|
||||
int selX = PADDING + (int) (saturation * SIZE) - 4;
|
||||
int selY = SQUARE_Y + (int) ((1f - brightness) * SIZE) - 4;
|
||||
renderScope.fill(selX, selY, selX + 8, selY + 8, 0xFF000000 | prop.value);
|
||||
renderScope.drawBorder(selX, selY, 8, 8, 0xFFFFFFFF);
|
||||
}
|
||||
|
||||
private void renderHue(RenderScope renderScope) {
|
||||
renderScope.drawTexture(HUE, PADDING, HUE_Y, 0, 0, SIZE, SLIDER_HEIGHT, 0xFFFFFFFF);
|
||||
|
||||
drawMarker(renderScope, PADDING + (int) (hue * SIZE), HUE_Y);
|
||||
}
|
||||
|
||||
private static void registerStrips() {
|
||||
if (stripsRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
BufferedImage saturation = new BufferedImage(STRIP, 1, BufferedImage.TYPE_INT_ARGB);
|
||||
BufferedImage brightness = new BufferedImage(1, STRIP, BufferedImage.TYPE_INT_ARGB);
|
||||
BufferedImage opacity = new BufferedImage(STRIP, 1, BufferedImage.TYPE_INT_ARGB);
|
||||
BufferedImage hue = new BufferedImage(STRIP, 1, BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
for (int i = 0; i < STRIP; i++) {
|
||||
int alpha = i * 255 / (STRIP - 1);
|
||||
saturation.setRGB(i, 0, ((255 - alpha) << 24) | 0xFFFFFF);
|
||||
brightness.setRGB(0, i, alpha << 24);
|
||||
opacity.setRGB(i, 0, (alpha << 24) | 0xFFFFFF);
|
||||
hue.setRGB(i, 0, 0xFF000000 | Color.HSBtoRGB(i / (float) STRIP, 1f, 1f));
|
||||
}
|
||||
|
||||
Providers.saturn.registerBufferedImageTexture(SATURATION, saturation);
|
||||
Providers.saturn.registerBufferedImageTexture(BRIGHTNESS, brightness);
|
||||
Providers.saturn.registerBufferedImageTexture(OPACITY, opacity);
|
||||
Providers.saturn.registerBufferedImageTexture(HUE, hue);
|
||||
stripsRegistered = true;
|
||||
}
|
||||
|
||||
private static void drawMarker(RenderScope renderScope, int x, int sliderY) {
|
||||
renderScope.fill(x - 2, sliderY - 2, x + 2, sliderY + SLIDER_HEIGHT + 2, 0xFFFFFFFF);
|
||||
renderScope.fill(x - 1, sliderY - 1, x + 1, sliderY + SLIDER_HEIGHT + 1, 0xFF000000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(double mouseX, double mouseY, int button) {
|
||||
dragging = getPartAt(mouseX, mouseY);
|
||||
update(mouseX, mouseY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
|
||||
update(mouseX, mouseY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(double mouseX, double mouseY, int button) {
|
||||
dragging = null;
|
||||
}
|
||||
|
||||
private Part getPartAt(double mouseX, double mouseY) {
|
||||
if (mouseX < PADDING - 4 || mouseX > PADDING + SIZE + 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mouseY >= OPACITY_Y - 4 && mouseY <= OPACITY_Y + SLIDER_HEIGHT + 4) {
|
||||
return Part.OPACITY;
|
||||
} else if (mouseY >= SQUARE_Y && mouseY <= SQUARE_Y + SIZE) {
|
||||
return Part.SQUARE;
|
||||
} else if (mouseY >= HUE_Y - 4 && mouseY <= HUE_Y + SLIDER_HEIGHT + 4) {
|
||||
return Part.HUE;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the dragged part to the mouse, even when the mouse has left it.
|
||||
*/
|
||||
private void update(double mouseX, double mouseY) {
|
||||
if (dragging == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
float x = clamp01((float) (mouseX - PADDING) / SIZE);
|
||||
|
||||
switch (dragging) {
|
||||
case OPACITY -> opacity = x;
|
||||
case HUE -> hue = x;
|
||||
case SQUARE -> {
|
||||
saturation = x;
|
||||
brightness = 1f - clamp01((float) (mouseY - SQUARE_Y) / SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
int rgb = Color.HSBtoRGB(hue, saturation, brightness) & 0xFFFFFF;
|
||||
prop.value = ((int) (opacity * 255) << 24) | rgb;
|
||||
}
|
||||
|
||||
private void syncFromColor() {
|
||||
int rgb = prop.value & 0xFFFFFF;
|
||||
float[] hsb = Color.RGBtoHSB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF, null);
|
||||
hue = hsb[0];
|
||||
saturation = hsb[1];
|
||||
brightness = hsb[2];
|
||||
|
||||
int alpha = (prop.value >>> 24) & 0xFF;
|
||||
opacity = (alpha == 0 ? 255 : alpha) / 255f;
|
||||
}
|
||||
|
||||
private static float clamp01(float value) {
|
||||
return Math.max(0f, Math.min(1f, value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.saturnclient.ui.components.inputs;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.ElementRenderer;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
|
||||
/**
|
||||
* A curve dropdown's popup: a {@link CurvePreview} beside the list of curves,
|
||||
* showing the hovered curve, or the current one when none is hovered.
|
||||
*/
|
||||
public class CurveOptions extends Element {
|
||||
public static final int GAP = 8;
|
||||
|
||||
private final List<Element> children;
|
||||
|
||||
public CurveOptions(OptionList list, int current) {
|
||||
CurvePreview preview = new CurvePreview(() -> list.getHovered() >= 0 ? list.getHovered() : current);
|
||||
|
||||
preview.position(0, 0);
|
||||
list.position(preview.width + GAP, 0);
|
||||
this.children = List.of(preview, list);
|
||||
|
||||
this.width = list.x + list.width;
|
||||
this.height = Math.max(preview.height, list.height);
|
||||
}
|
||||
|
||||
/**
|
||||
* How far left of the list this popup starts, so the list can line up
|
||||
* under the dropdown.
|
||||
*/
|
||||
public int getListX() {
|
||||
return children.get(1).x;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
ElementRenderer.render(children, ctx.elapsed, renderScope, ctx.mouseX, ctx.mouseY, ctx.childFrame());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(double mouseX, double mouseY, int button) {
|
||||
ElementRenderer.mouseClicked(children, mouseX, mouseY, button);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scroll(int mouseX, int mouseY, double horizontalAmount, double verticalAmount) {
|
||||
ElementRenderer.mouseScrolled(children, mouseX, mouseY, horizontalAmount, verticalAmount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.saturnclient.ui.components.inputs;
|
||||
|
||||
import java.util.function.Function;
|
||||
import java.util.function.IntSupplier;
|
||||
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.anim.Animation;
|
||||
|
||||
/**
|
||||
* A graph of an easing curve, with a dot running along it on a loop to show
|
||||
* how the animation moves.
|
||||
*/
|
||||
public class CurvePreview extends Element {
|
||||
private static final int PADDING = 14;
|
||||
private static final int TITLE_HEIGHT = 20;
|
||||
private static final float TITLE_SCALE = 0.56f;
|
||||
private static final int DOT = 7;
|
||||
|
||||
/** How long the dot takes to run the curve, then how long it rests at the end, in ms */
|
||||
private static final int RUN = 1200;
|
||||
private static final int REST = 500;
|
||||
|
||||
private final IntSupplier curve;
|
||||
|
||||
/**
|
||||
* @param curve the curve to show, as an index {@link Animation#getCurve}
|
||||
* takes; read every frame
|
||||
*/
|
||||
public CurvePreview(IntSupplier curve) {
|
||||
this.curve = curve;
|
||||
this.width = 190;
|
||||
this.height = 150;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
|
||||
Theme.getPopupBg(), Theme.withAlpha(0.14f, Theme.FOREGROUND.value));
|
||||
|
||||
renderScope.drawText(TITLE_SCALE, "PREVIEW", PADDING, PADDING - 2, Theme.FONT.value, Theme.getMutedFg());
|
||||
|
||||
Function<Double, Double> ease = Animation.getCurve(curve.getAsInt());
|
||||
|
||||
int graphX = PADDING;
|
||||
int graphY = PADDING + TITLE_HEIGHT;
|
||||
int graphWidth = width - PADDING * 2;
|
||||
int graphHeight = height - PADDING * 2 - TITLE_HEIGHT;
|
||||
|
||||
// Curves like "back" overshoot, so fit the graph to the curve's own range.
|
||||
double min = 0, max = 1;
|
||||
for (int i = 0; i <= graphWidth; i++) {
|
||||
double value = ease.apply(i / (double) graphWidth);
|
||||
min = Math.min(min, value);
|
||||
max = Math.max(max, value);
|
||||
}
|
||||
|
||||
double range = max - min;
|
||||
int startY = toY(0, min, range, graphY, graphHeight);
|
||||
int endY = toY(1, min, range, graphY, graphHeight);
|
||||
|
||||
// Where the animation starts and ends
|
||||
int guide = Theme.withAlpha(0.1f, Theme.FOREGROUND.value);
|
||||
renderScope.fill(graphX, startY, graphX + graphWidth, startY + 1, guide);
|
||||
renderScope.fill(graphX, endY, graphX + graphWidth, endY + 1, guide);
|
||||
|
||||
// The curve, as small squares close enough to read as a line
|
||||
int steps = graphWidth * 3;
|
||||
for (int i = 0; i <= steps; i++) {
|
||||
double t = i / (double) steps;
|
||||
int x = graphX + (int) (t * graphWidth);
|
||||
int y = toY(ease.apply(t), min, range, graphY, graphHeight);
|
||||
renderScope.fill(x - 1, y - 1, x + 1, y + 1, Theme.ACCENT.value);
|
||||
}
|
||||
|
||||
// The dot, looping along it
|
||||
double t = Math.min(1.0, (ctx.elapsed % (RUN + REST)) / (double) RUN);
|
||||
int dotX = graphX + (int) (t * graphWidth);
|
||||
int dotY = toY(ease.apply(t), min, range, graphY, graphHeight);
|
||||
renderScope.drawRoundedRectangle(dotX - DOT / 2, dotY - DOT / 2, DOT, DOT, DOT / 2, 2,
|
||||
Theme.FOREGROUND.value, Theme.BACKGROUND.value);
|
||||
}
|
||||
|
||||
private static int toY(double value, double min, double range, int graphY, int graphHeight) {
|
||||
return graphY + graphHeight - (int) ((value - min) / range * graphHeight);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,29 @@
|
||||
package org.saturnclient.ui.components.inputs;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
import org.saturnclient.config.ConfigManager;
|
||||
import org.saturnclient.config.property.FloatProperty;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
import org.saturnclient.ui.elements.Field;
|
||||
|
||||
public class FloatInput extends Input {
|
||||
public FloatProperty prop;
|
||||
|
||||
public FloatInput(FloatProperty prop) {
|
||||
this.width = 120;
|
||||
this.height = Fonts.getHeight();
|
||||
this.width = 96;
|
||||
this.height = Field.HEIGHT;
|
||||
this.prop = prop;
|
||||
this.text = String.valueOf(prop.value);
|
||||
this.text = format(prop.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The value with at most three decimals and no trailing zeros, since floats
|
||||
* like 1.2000000476837158 can't be stored exactly.
|
||||
*/
|
||||
private static String format(float value) {
|
||||
return new BigDecimal(Float.toString(value)).setScale(3, RoundingMode.HALF_UP).stripTrailingZeros()
|
||||
.toPlainString();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,7 +63,7 @@ public class FloatInput extends Input {
|
||||
@Override
|
||||
public void checkReset() {
|
||||
if (prop.isReset) {
|
||||
this.text = String.valueOf(prop.value);
|
||||
this.text = format(prop.value);
|
||||
this.cursorPosition = 0;
|
||||
prop.isReset = false;
|
||||
}
|
||||
|
||||
@@ -5,12 +5,18 @@ 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;
|
||||
|
||||
public abstract class Input extends Element {
|
||||
String text = "";
|
||||
public int cursorPosition = 0;
|
||||
|
||||
/** Scale the text is drawn at */
|
||||
protected float textScale = Field.TEXT_SCALE;
|
||||
/** Horizontal inset of the text on both sides */
|
||||
protected int textX = 10;
|
||||
|
||||
public abstract void checkReset();
|
||||
|
||||
public abstract void backspace();
|
||||
@@ -41,6 +47,7 @@ public abstract class Input extends Element {
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
mouseX -= textX;
|
||||
int scrollOffset = getScrollOffset();
|
||||
String visibleText = text.substring(scrollOffset);
|
||||
|
||||
@@ -48,7 +55,7 @@ public abstract class Input extends Element {
|
||||
float textWidth = 0;
|
||||
|
||||
for (int i = 0; i < visibleText.length(); i++) {
|
||||
textWidth = Fonts.getWidth(visibleText.substring(0, i + 1), Theme.FONT.value) * 0.6f;
|
||||
textWidth = Fonts.getWidth(visibleText.substring(0, i + 1), Theme.FONT.value) * textScale;
|
||||
|
||||
if (textWidth > mouseX) {
|
||||
break;
|
||||
@@ -64,28 +71,44 @@ public abstract class Input extends Element {
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
checkReset();
|
||||
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, 10, Theme.PRIMARY.value);
|
||||
Field.drawBox(renderScope, width, height, ctx.isHovering(), focused);
|
||||
drawText(renderScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the visible part of the text, and the cursor while focused.
|
||||
*/
|
||||
protected void drawText(RenderScope renderScope) {
|
||||
int textHeight = (int) (Fonts.getHeight() * textScale);
|
||||
int textY = Field.textY(height, textScale);
|
||||
|
||||
int scrollOffset = getScrollOffset();
|
||||
String visibleText = getVisibleText(scrollOffset);
|
||||
renderScope.drawText(0.6f, visibleText, 4, 4, Theme.FONT.value, Theme.PRIMARY_FG.value);
|
||||
renderScope.drawText(textScale, visibleText, textX, textY, Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
|
||||
if (focused) {
|
||||
int cursorX = 4 +
|
||||
(int) (Fonts.getWidth(visibleText.substring(0, cursorPosition - scrollOffset), Theme.FONT.value)
|
||||
* 0.6f);
|
||||
|
||||
renderScope.drawRectangle(cursorX, 2, 1, height - 4, Theme.ACCENT.value);
|
||||
renderScope.drawRectangle(getCursorX(visibleText, scrollOffset), textY - 1, 1, textHeight + 2,
|
||||
Theme.ACCENT.value);
|
||||
}
|
||||
}
|
||||
|
||||
protected int getCursorX(String visibleText, int scrollOffset) {
|
||||
return textX + (int) (Fonts.getWidth(visibleText.substring(0, cursorPosition - scrollOffset),
|
||||
Theme.FONT.value) * textScale);
|
||||
}
|
||||
|
||||
/** Width available to the text, in unscaled font units */
|
||||
private float getTextSpace() {
|
||||
return (width - textX * 2) / textScale;
|
||||
}
|
||||
|
||||
protected int getScrollOffset() {
|
||||
if (text.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int offset = 0;
|
||||
int maxWidth = width - 4; // Available width for text
|
||||
float maxWidth = getTextSpace();
|
||||
while (offset < cursorPosition) { // Ensure cursor remains visible
|
||||
String visibleText = text.substring(offset, cursorPosition);
|
||||
if (Fonts.getWidth(visibleText, Theme.FONT.value) > maxWidth) {
|
||||
@@ -102,7 +125,7 @@ public abstract class Input extends Element {
|
||||
|
||||
// Ensure text does not overflow past the available width
|
||||
for (int i = 1; i <= visibleText.length(); i++) {
|
||||
if (Fonts.getWidth(visibleText.substring(0, i), Theme.FONT.value) > width - 4) {
|
||||
if (Fonts.getWidth(visibleText.substring(0, i), Theme.FONT.value) > getTextSpace()) {
|
||||
return visibleText.substring(0, i - 1); // Trim overflow
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ package org.saturnclient.ui.components.inputs;
|
||||
|
||||
import org.saturnclient.config.ConfigManager;
|
||||
import org.saturnclient.config.property.IntProperty;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
import org.saturnclient.ui.elements.Field;
|
||||
|
||||
public class IntInput extends Input {
|
||||
public IntProperty prop;
|
||||
|
||||
public IntInput(IntProperty prop) {
|
||||
this.width = 120;
|
||||
this.height = Fonts.getHeight();
|
||||
this.width = 96;
|
||||
this.height = Field.HEIGHT;
|
||||
this.prop = prop;
|
||||
this.text = String.valueOf(prop.value);
|
||||
}
|
||||
|
||||
@@ -7,27 +7,41 @@ import org.saturnclient.config.property.KeybindingProperty;
|
||||
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.elements.Field;
|
||||
|
||||
/**
|
||||
* Shows a key binding. Click it, then press a key to bind it, or backspace to
|
||||
* unbind.
|
||||
*/
|
||||
public class KeybindingSelector extends Element {
|
||||
KeybindingProperty prop;
|
||||
|
||||
public KeybindingSelector(KeybindingProperty prop) {
|
||||
this.prop = prop;
|
||||
this.width = 120;
|
||||
this.height = Fonts.getHeight();
|
||||
this.width = 96;
|
||||
this.height = Field.HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
int textColor = focused ? 0xFFFFFF : 0xAAAAAA;
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, 10, 0xFF000000);
|
||||
String name = Providers.GLFW.getKeyName(prop.value);
|
||||
Field.drawBox(renderScope, width, height, ctx.isHovering(), focused);
|
||||
|
||||
renderScope.drawText(0.6f,
|
||||
prop.value == -1 ? "<NONE>"
|
||||
: name != null ? name.toUpperCase() : "<Invalid-Key>",
|
||||
4, 4, Theme.FONT.value, textColor);
|
||||
if (focused) {
|
||||
Field.drawCenteredText(renderScope, "Press a key", width, height, Theme.getBodyFont(),
|
||||
Theme.getMutedFg());
|
||||
return;
|
||||
}
|
||||
|
||||
Field.drawCenteredText(renderScope, getKeyLabel(), width, height, Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
}
|
||||
|
||||
private String getKeyLabel() {
|
||||
if (prop.value == -1) {
|
||||
return "None";
|
||||
}
|
||||
|
||||
String name = Providers.GLFW.getKeyName(prop.value);
|
||||
return name != null ? name.toUpperCase() : "Key " + prop.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package org.saturnclient.ui.components.inputs;
|
||||
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
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;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
|
||||
/**
|
||||
* A dropdown's list of options, shown as a popup. Long lists scroll, and the
|
||||
* current option is ticked.
|
||||
*/
|
||||
public class OptionList extends Element {
|
||||
private static final int ITEM_HEIGHT = 30;
|
||||
private static final int PADDING = 4;
|
||||
private static final int MAX_VISIBLE = 8;
|
||||
private static final int CHECK = 14;
|
||||
|
||||
private final String[] options;
|
||||
private final int selected;
|
||||
private final IntConsumer onPick;
|
||||
|
||||
/** Index of the first visible option */
|
||||
private int first;
|
||||
/** Wheel movement not yet turned into whole options, so trackpads scroll smoothly */
|
||||
private double pendingScroll = 0;
|
||||
/** The option under the mouse on the last frame, or -1 */
|
||||
private int hovered = -1;
|
||||
|
||||
public OptionList(String[] options, int selected, int width, IntConsumer onPick) {
|
||||
this.options = options;
|
||||
this.selected = selected;
|
||||
this.onPick = onPick;
|
||||
this.width = width;
|
||||
this.height = Math.min(options.length, MAX_VISIBLE) * ITEM_HEIGHT + PADDING * 2;
|
||||
|
||||
// Start with the current option in view, a few rows down if possible
|
||||
this.first = clampFirst(selected - MAX_VISIBLE / 2 + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
|
||||
Theme.getPopupBg(), Theme.withAlpha(0.14f, Theme.FOREGROUND.value));
|
||||
|
||||
int visible = Math.min(options.length, MAX_VISIBLE);
|
||||
hovered = getIndexAt(ctx.mouseX, ctx.mouseY);
|
||||
|
||||
for (int row = 0; row < visible; row++) {
|
||||
int index = first + row;
|
||||
int y = PADDING + row * ITEM_HEIGHT;
|
||||
boolean isSelected = index == selected;
|
||||
|
||||
if (index == hovered) {
|
||||
renderScope.drawRoundedRectangle(PADDING, y, width - PADDING * 2, ITEM_HEIGHT,
|
||||
Theme.WIDGET_RADIUS.value - 3, Theme.withAlpha(0.07f, Theme.FOREGROUND.value));
|
||||
}
|
||||
|
||||
int textWidth = width - PADDING * 2 - 12 - CHECK - 10;
|
||||
String text = Fonts.ellipsize(options[index], Theme.FONT.value, Field.TEXT_SCALE, textWidth);
|
||||
renderScope.drawText(Field.TEXT_SCALE, text, PADDING + 10, y + Field.textY(ITEM_HEIGHT, Field.TEXT_SCALE),
|
||||
Theme.FONT.value, isSelected ? Theme.ACCENT.value : Theme.FOREGROUND.value);
|
||||
|
||||
if (isSelected) {
|
||||
renderScope.drawTexture(Textures.ICON_CHECK, width - PADDING - 10 - CHECK,
|
||||
y + (ITEM_HEIGHT - CHECK) / 2, 0, 0, CHECK, CHECK, Theme.ACCENT.value);
|
||||
}
|
||||
}
|
||||
|
||||
// A thin bar showing where in a long list this is
|
||||
if (options.length > MAX_VISIBLE) {
|
||||
int track = height - PADDING * 2;
|
||||
int thumb = Math.max(16, track * MAX_VISIBLE / options.length);
|
||||
int thumbY = PADDING + (track - thumb) * first / (options.length - MAX_VISIBLE);
|
||||
renderScope.drawRoundedRectangle(width - 4, thumbY, 2, thumb, 1,
|
||||
Theme.withAlpha(0.3f, Theme.FOREGROUND.value));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
int index = getIndexAt(mouseX, mouseY);
|
||||
if (index >= 0) {
|
||||
onPick.accept(index);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scroll(int mouseX, int mouseY, double horizontalAmount, double verticalAmount) {
|
||||
pendingScroll -= verticalAmount;
|
||||
int whole = (int) pendingScroll;
|
||||
pendingScroll -= whole;
|
||||
first = clampFirst(first + whole);
|
||||
}
|
||||
|
||||
/**
|
||||
* The option under the mouse on the last frame, or -1.
|
||||
*/
|
||||
public int getHovered() {
|
||||
return hovered;
|
||||
}
|
||||
|
||||
/**
|
||||
* The option under the mouse, or -1.
|
||||
*/
|
||||
private int getIndexAt(int mouseX, int mouseY) {
|
||||
if (mouseX < PADDING || mouseX > width - PADDING || mouseY < PADDING || mouseY >= height - PADDING) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int index = first + (mouseY - PADDING) / ITEM_HEIGHT;
|
||||
return index < options.length ? index : -1;
|
||||
}
|
||||
|
||||
private int clampFirst(int value) {
|
||||
return Math.max(0, Math.min(value, options.length - Math.min(options.length, MAX_VISIBLE)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.saturnclient.ui.components.inputs;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.elements.Field;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
|
||||
/**
|
||||
* A free-text search box with an icon and placeholder, reporting every change.
|
||||
*/
|
||||
public class SearchInput extends Input {
|
||||
private static final int ICON_SIZE = 16;
|
||||
private static final int ICON_X = 12;
|
||||
|
||||
private final String placeholder;
|
||||
private final Consumer<String> onChange;
|
||||
|
||||
public SearchInput(String placeholder, Consumer<String> onChange) {
|
||||
this.placeholder = placeholder;
|
||||
this.onChange = onChange;
|
||||
this.textX = ICON_X + ICON_SIZE + 8;
|
||||
this.width = 240;
|
||||
this.height = 36;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void charTyped(char chr) {
|
||||
if (text.length() >= 64) {
|
||||
return;
|
||||
}
|
||||
|
||||
text = text.substring(0, cursorPosition) + chr + text.substring(cursorPosition);
|
||||
cursorPosition++;
|
||||
onChange.accept(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void backspace() {
|
||||
onChange.accept(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkReset() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
Field.drawBox(renderScope, width, height, ctx.isHovering(), focused);
|
||||
|
||||
renderScope.drawTexture(Textures.ICON_SEARCH, ICON_X, (height - ICON_SIZE) / 2, 0, 0,
|
||||
ICON_SIZE, ICON_SIZE, Theme.getMutedFg());
|
||||
|
||||
if (text.isEmpty() && !focused) {
|
||||
renderScope.drawText(textScale, placeholder, textX, Field.textY(height, textScale),
|
||||
Theme.getBodyFont(), Theme.getMutedFg());
|
||||
return;
|
||||
}
|
||||
|
||||
drawText(renderScope);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,66 @@
|
||||
package org.saturnclient.ui.components.inputs;
|
||||
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.config.property.CurveProperty;
|
||||
import org.saturnclient.config.property.SelectProperty;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.Popup;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.Utils;
|
||||
import org.saturnclient.ui.elements.Field;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
|
||||
/**
|
||||
* A dropdown for a {@link SelectProperty}: the current value, opening a list
|
||||
* of every value when clicked.
|
||||
*/
|
||||
public class Select extends Element {
|
||||
private SelectProperty prop;
|
||||
private static final int TEXT_X = 12;
|
||||
private static final int ARROW = 16;
|
||||
private static final int ARROW_INSET = 10;
|
||||
|
||||
private final SelectProperty prop;
|
||||
|
||||
/** The context this was last drawn with, to open the list next to it */
|
||||
private ElementContext lastContext;
|
||||
|
||||
public Select(SelectProperty prop) {
|
||||
this.prop = prop;
|
||||
this.width = 150;
|
||||
this.height = 15;
|
||||
this.width = 200;
|
||||
this.height = Field.HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawText(0.8f, "<", 0, 0, Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
lastContext = ctx;
|
||||
boolean open = Popup.isOpenFor(this);
|
||||
|
||||
String text = prop.getSelection();
|
||||
Field.drawBox(renderScope, width, height, ctx.isHovering(), open);
|
||||
|
||||
renderScope.drawText(0.6f, text, (int) (width - ((float) Fonts.getWidth(text, Theme.FONT.value) * 0.6f)) / 2, 1,
|
||||
Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
int textWidth = width - TEXT_X - ARROW_INSET - ARROW - 8;
|
||||
String text = Fonts.ellipsize(prop.getSelection(), Theme.FONT.value, Field.TEXT_SCALE, textWidth);
|
||||
renderScope.drawText(Field.TEXT_SCALE, text, TEXT_X, Field.textY(height, Field.TEXT_SCALE), Theme.FONT.value,
|
||||
Theme.FOREGROUND.value);
|
||||
|
||||
renderScope.drawText(0.8f, ">", width - 10, 0, Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
renderScope.drawTexture(Textures.ICON_CHEVRON_DOWN, width - ARROW_INSET - ARROW, (height - ARROW) / 2, 0, 0,
|
||||
ARROW, ARROW, open ? Theme.ACCENT.value : Theme.getMutedFg());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
if (Utils.isHovering(mouseX - (width - 10), mouseY, 10, height, 1.0f)) {
|
||||
prop.next();
|
||||
} else if (Utils.isHovering(mouseX, mouseY, 10, height, 1.0f)) {
|
||||
prop.prev();
|
||||
// While the list is open, a click here goes to the popup, which closes it.
|
||||
OptionList list = new OptionList(prop.availableValues, prop.value, width, index -> {
|
||||
prop.setSelection(index);
|
||||
Popup.close();
|
||||
});
|
||||
|
||||
if (prop instanceof CurveProperty) {
|
||||
// The preview goes left of the list, so the list still lines up under this.
|
||||
CurveOptions options = new CurveOptions(list, prop.value);
|
||||
Popup.openBelow(this, options, lastContext, -options.getListX());
|
||||
} else {
|
||||
Popup.openBelow(this, list, lastContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package org.saturnclient.ui.components.inputs;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Locale;
|
||||
import java.util.function.DoubleConsumer;
|
||||
import java.util.function.DoubleSupplier;
|
||||
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.config.property.FloatProperty;
|
||||
import org.saturnclient.config.property.IntProperty;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.elements.Field;
|
||||
|
||||
/**
|
||||
* Edits a number within a range by clicking or dragging along a track, with
|
||||
* the value shown beside it.
|
||||
*/
|
||||
public class Slider extends Element {
|
||||
private static final int TRACK_HEIGHT = 6;
|
||||
private static final int KNOB = 16;
|
||||
private static final int VALUE_WIDTH = 72;
|
||||
private static final int VALUE_GAP = 14;
|
||||
private static final int MAX_DECIMALS = 3;
|
||||
|
||||
private final double min, max, step;
|
||||
private final int decimals;
|
||||
private final String unit;
|
||||
private final DoubleSupplier value;
|
||||
private final DoubleConsumer onChange;
|
||||
|
||||
private boolean dragging = false;
|
||||
|
||||
/**
|
||||
* @param step as text, so a float step like 0.1 isn't read as 0.10000000149
|
||||
*/
|
||||
private Slider(double min, double max, String step, String unit, DoubleSupplier value, DoubleConsumer onChange) {
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.step = Double.parseDouble(step);
|
||||
this.unit = unit;
|
||||
this.value = value;
|
||||
this.onChange = onChange;
|
||||
this.decimals = Math.min(MAX_DECIMALS, Math.max(0, new BigDecimal(step).stripTrailingZeros().scale()));
|
||||
this.width = 280;
|
||||
this.height = Field.HEIGHT;
|
||||
}
|
||||
|
||||
public static Slider of(IntProperty prop) {
|
||||
return new Slider(prop.min, prop.max, Integer.toString(prop.step), prop.unit, () -> prop.value,
|
||||
v -> prop.value = (int) Math.round(v));
|
||||
}
|
||||
|
||||
public static Slider of(FloatProperty prop) {
|
||||
return new Slider(prop.min, prop.max, Float.toString(prop.step), prop.unit, () -> prop.value,
|
||||
v -> prop.value = (float) v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
double current = value.getAsDouble();
|
||||
float progress = (float) Math.max(0, Math.min(1, (current - min) / (max - min)));
|
||||
int trackWidth = getTrackWidth();
|
||||
int trackY = (height - TRACK_HEIGHT) / 2;
|
||||
int fillWidth = (int) (trackWidth * progress);
|
||||
|
||||
renderScope.drawRoundedRectangle(KNOB / 2, trackY, trackWidth, TRACK_HEIGHT, TRACK_HEIGHT / 2,
|
||||
Theme.withAlpha(0.14f, Theme.FOREGROUND.value));
|
||||
|
||||
if (fillWidth > 0) {
|
||||
renderScope.drawRoundedRectangle(KNOB / 2, trackY, fillWidth, TRACK_HEIGHT, TRACK_HEIGHT / 2,
|
||||
Theme.ACCENT.value);
|
||||
}
|
||||
|
||||
boolean knobHovered = dragging || ctx.isHovering(fillWidth, 0, KNOB, height);
|
||||
renderScope.drawRoundedRectangle(fillWidth, (height - KNOB) / 2, KNOB, KNOB, KNOB / 2, 3,
|
||||
knobHovered ? Theme.ACCENT.value : Theme.FOREGROUND.value, Theme.BACKGROUND.value);
|
||||
|
||||
// Value
|
||||
int valueX = width - VALUE_WIDTH;
|
||||
renderScope.drawRoundedRectangle(valueX, 0, VALUE_WIDTH, height, Theme.WIDGET_RADIUS.value,
|
||||
Theme.withAlpha(0.07f, Theme.FOREGROUND.value));
|
||||
|
||||
renderScope.getMatrixStack().push();
|
||||
renderScope.getMatrixStack().translate(valueX, 0);
|
||||
Field.drawCenteredText(renderScope, format(current), VALUE_WIDTH, height, Theme.FONT.value,
|
||||
Theme.FOREGROUND.value);
|
||||
renderScope.getMatrixStack().pop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(double mouseX, double mouseY, int button) {
|
||||
dragging = mouseX >= 0 && mouseX <= getTrackWidth() + KNOB && mouseY >= 0 && mouseY <= height;
|
||||
|
||||
if (dragging) {
|
||||
setFromMouse(mouseX);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
|
||||
if (dragging) {
|
||||
setFromMouse(mouseX);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(double mouseX, double mouseY, int button) {
|
||||
dragging = false;
|
||||
}
|
||||
|
||||
private void setFromMouse(double mouseX) {
|
||||
double progress = Math.max(0, Math.min(1, (mouseX - KNOB / 2.0) / getTrackWidth()));
|
||||
double raw = min + progress * (max - min);
|
||||
double snapped = min + Math.round((raw - min) / step) * step;
|
||||
|
||||
// Snapping in floating point can leave 2.9999999, so round to the step's decimals.
|
||||
double rounded = BigDecimal.valueOf(snapped).setScale(decimals, RoundingMode.HALF_UP).doubleValue();
|
||||
onChange.accept(Math.max(min, Math.min(max, rounded)));
|
||||
}
|
||||
|
||||
private String format(double current) {
|
||||
String number = String.format(Locale.ROOT, "%." + decimals + "f", current);
|
||||
|
||||
if (unit.isEmpty()) {
|
||||
return number;
|
||||
}
|
||||
|
||||
// Word units get a space ("700 ms"); symbols don't ("3.0×").
|
||||
return Character.isLetter(unit.charAt(0)) ? number + " " + unit : number + unit;
|
||||
}
|
||||
|
||||
private int getTrackWidth() {
|
||||
return width - VALUE_WIDTH - VALUE_GAP - KNOB;
|
||||
}
|
||||
}
|
||||
@@ -7,20 +7,37 @@ import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
|
||||
public class Toggle extends Element {
|
||||
public static final int SWITCH_WIDTH = 42;
|
||||
public static final int SWITCH_HEIGHT = 24;
|
||||
|
||||
private BoolProperty prop;
|
||||
|
||||
public Toggle(BoolProperty prop) {
|
||||
this.prop = prop;
|
||||
this.width = 60;
|
||||
this.height = 30;
|
||||
this.width = SWITCH_WIDTH;
|
||||
this.height = SWITCH_HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawRoundedRectangle(0, 0, 60, 30, Theme.WIDGET_RADIUS.value,
|
||||
Theme.withAlpha(0.5f, prop.value ? Theme.ACCENT.value : Theme.PRIMARY.value));
|
||||
renderScope.drawRoundedRectangle(prop.value ? 30 : 0, 0, 30, 30, Theme.WIDGET_RADIUS.value,
|
||||
prop.value ? Theme.ACCENT.value : Theme.PRIMARY.value);
|
||||
drawSwitch(renderScope, 0, 0, prop.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws an on/off switch: a pill track with a round knob on the right when on.
|
||||
*/
|
||||
public static void drawSwitch(RenderScope renderScope, int x, int y, boolean on) {
|
||||
int inset = 3;
|
||||
int knob = SWITCH_HEIGHT - inset * 2;
|
||||
|
||||
renderScope.drawRoundedRectangle(x, y, SWITCH_WIDTH, SWITCH_HEIGHT, SWITCH_HEIGHT / 2,
|
||||
on ? Theme.ACCENT.value : Theme.withAlpha(0.16f, Theme.FOREGROUND.value));
|
||||
|
||||
renderScope.drawRoundedRectangle(
|
||||
on ? x + SWITCH_WIDTH - inset - knob : x + inset,
|
||||
y + inset,
|
||||
knob, knob, knob / 2,
|
||||
on ? Theme.ACCENT_FG.value : Theme.withAlpha(0.8f, Theme.PRIMARY_FG.value));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
|
||||
/**
|
||||
* A secondary button with an icon and a label: outlined, or plain text that
|
||||
* lights up on hover (for links like "back").
|
||||
*/
|
||||
public class ActionButton extends Element {
|
||||
private static final int HEIGHT = 34;
|
||||
private static final int PADDING = 12;
|
||||
private static final int ICON = 15;
|
||||
private static final int ICON_GAP = 7;
|
||||
private static final float TEXT_SCALE = 0.66f;
|
||||
|
||||
private final IdentifierRef icon;
|
||||
private final String text;
|
||||
private final boolean outlined;
|
||||
private final Runnable onClick;
|
||||
|
||||
public ActionButton(IdentifierRef icon, String text, boolean outlined, Runnable onClick) {
|
||||
this.icon = icon;
|
||||
this.text = text;
|
||||
this.outlined = outlined;
|
||||
this.onClick = onClick;
|
||||
this.height = HEIGHT;
|
||||
this.width = PADDING * 2 + ICON + ICON_GAP + (int) (Fonts.getWidth(text, Theme.FONT.value) * TEXT_SCALE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
boolean hovered = ctx.isHovering();
|
||||
|
||||
if (outlined) {
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
|
||||
hovered ? Theme.withAlpha(0.06f, Theme.FOREGROUND.value) : 0,
|
||||
Theme.withAlpha(hovered ? 0.25f : 0.12f, Theme.FOREGROUND.value));
|
||||
}
|
||||
|
||||
int color = hovered ? Theme.FOREGROUND.value : outlined ? Theme.PRIMARY_FG.value : Theme.getMutedFg();
|
||||
|
||||
renderScope.drawTexture(icon, PADDING, (height - ICON) / 2, 0, 0, ICON, ICON, color);
|
||||
renderScope.drawText(TEXT_SCALE, text, PADDING + ICON + ICON_GAP, Field.textY(height, TEXT_SCALE),
|
||||
Theme.FONT.value, color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
onClick.run();
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ import org.saturnclient.ui.RenderScope;
|
||||
public class AnimationStagger extends Element {
|
||||
protected List<Element> children = new ArrayList<>();
|
||||
public int delay;
|
||||
/** When the first child starts, in ms */
|
||||
public int start = 0;
|
||||
|
||||
public AnimationStagger(int delay) {
|
||||
this.delay = delay;
|
||||
@@ -22,13 +24,21 @@ public class AnimationStagger extends Element {
|
||||
this.delay = config.stagger.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the children back until {@code start} ms after the screen opens.
|
||||
*/
|
||||
public AnimationStagger after(int start) {
|
||||
this.start = start;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void draw(Element element) {
|
||||
if (!Config.stagger.value) {
|
||||
element.animation = null;
|
||||
}
|
||||
|
||||
if (element.animation != null) {
|
||||
element.animation.delay = delay * children.size();
|
||||
element.animation.delay = start + delay * children.size();
|
||||
}
|
||||
|
||||
ElementRenderer.draw(children, element);
|
||||
@@ -41,7 +51,8 @@ public class AnimationStagger extends Element {
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
ElementRenderer.render(children, ctx.elapsed, renderScope,
|
||||
ctx.mouseX,
|
||||
ctx.mouseY);
|
||||
ctx.mouseY,
|
||||
ctx.childFrame());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,29 +1,47 @@
|
||||
package org.saturnclient.ui.elements;
|
||||
|
||||
import org.saturnclient.config.Config;
|
||||
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;
|
||||
|
||||
/**
|
||||
* A text button: a translucent, outlined surface that brightens on hover.
|
||||
*/
|
||||
public class Button extends Element {
|
||||
private String text;
|
||||
private Runnable onClick;
|
||||
public static final int HEIGHT = 40;
|
||||
|
||||
private static final float TEXT_SCALE = 0.72f;
|
||||
|
||||
private final String text;
|
||||
private final Runnable onClick;
|
||||
|
||||
public Button(String text, Runnable onClick) {
|
||||
this.text = text;
|
||||
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, 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,
|
||||
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.drawGlassRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, Config.widgetBlur.value, 1,
|
||||
hovered ? Theme.PRIMARY.value : Theme.getPanelBg(),
|
||||
Theme.withAlpha(hovered ? 0.25f : 0.1f, Theme.FOREGROUND.value));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.saturnclient.ui.elements;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* A row of pill buttons where exactly one is selected, such as category filters.
|
||||
*/
|
||||
public class ChipGroup extends Element {
|
||||
private static final int HEIGHT = 28;
|
||||
private static final int PADDING = 14;
|
||||
private static final int GAP = 8;
|
||||
private static final float TEXT_SCALE = 0.62f;
|
||||
|
||||
private final List<String> options;
|
||||
private final Consumer<String> onSelect;
|
||||
private String selected;
|
||||
|
||||
public ChipGroup(List<String> options, String selected, Consumer<String> onSelect) {
|
||||
this.options = options;
|
||||
this.selected = selected;
|
||||
this.onSelect = onSelect;
|
||||
this.height = HEIGHT;
|
||||
this.width = chipX(options.size()) - GAP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
int font = Theme.FONT.value;
|
||||
int textY = (HEIGHT - (int) (Fonts.getHeight() * TEXT_SCALE)) / 2;
|
||||
|
||||
for (int i = 0; i < options.size(); i++) {
|
||||
String option = options.get(i);
|
||||
int x = chipX(i);
|
||||
int w = chipWidth(option);
|
||||
boolean active = option.equals(selected);
|
||||
boolean hovered = ctx.isHovering(x, 0, w, HEIGHT);
|
||||
|
||||
if (active) {
|
||||
renderScope.drawRoundedRectangle(x, 0, w, HEIGHT, HEIGHT / 2, Theme.ACCENT.value);
|
||||
} else {
|
||||
renderScope.drawRoundedRectangle(x, 0, w, HEIGHT, HEIGHT / 2, 1,
|
||||
Theme.withAlpha(0.05f, Theme.FOREGROUND.value),
|
||||
hovered ? Theme.withAlpha(0.25f, Theme.FOREGROUND.value) : Theme.getBorder());
|
||||
}
|
||||
|
||||
renderScope.drawText(TEXT_SCALE, option, x + PADDING, textY, font,
|
||||
active ? Theme.ACCENT_FG.value : Theme.PRIMARY_FG.value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
for (int i = 0; i < options.size(); i++) {
|
||||
String option = options.get(i);
|
||||
int x = chipX(i);
|
||||
|
||||
if (mouseX >= x && mouseX <= x + chipWidth(option)) {
|
||||
if (!option.equals(selected)) {
|
||||
selected = option;
|
||||
onSelect.accept(option);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int chipX(int index) {
|
||||
int x = 0;
|
||||
for (int i = 0; i < index; i++) {
|
||||
x += chipWidth(options.get(i)) + GAP;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
private static int chipWidth(String option) {
|
||||
return (int) (Fonts.getWidth(option, Theme.FONT.value) * TEXT_SCALE) + PADDING * 2;
|
||||
}
|
||||
}
|
||||
@@ -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,23 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* A faint line filling the element: horizontal by default, or vertical when
|
||||
* given a width of 1 and a height.
|
||||
*/
|
||||
public class Divider extends Element {
|
||||
|
||||
public Divider(int width) {
|
||||
this.width = width;
|
||||
this.height = 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawRectangle(0, 0, width, height, Theme.getBorder());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.saturnclient.ui.elements;
|
||||
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
|
||||
/**
|
||||
* The shared look of input fields: a faint box whose outline brightens on hover
|
||||
* and turns to the accent color while focused.
|
||||
*/
|
||||
public final class Field {
|
||||
/** Height of the fields in settings rows */
|
||||
public static final int HEIGHT = 32;
|
||||
public static final float TEXT_SCALE = 0.68f;
|
||||
|
||||
private Field() {
|
||||
}
|
||||
|
||||
public static void drawBox(RenderScope renderScope, int width, int height, boolean hovered, boolean focused) {
|
||||
int border = focused ? Theme.withAlpha(0.6f, Theme.ACCENT.value)
|
||||
: hovered ? Theme.withAlpha(0.25f, Theme.FOREGROUND.value)
|
||||
: Theme.withAlpha(0.1f, Theme.FOREGROUND.value);
|
||||
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
|
||||
Theme.withAlpha(0.06f, Theme.FOREGROUND.value), border);
|
||||
}
|
||||
|
||||
/**
|
||||
* The y at which text drawn at {@code scale} is vertically centered in a
|
||||
* field of the given height.
|
||||
*/
|
||||
public static int textY(int height, float scale) {
|
||||
return (height - (int) (Fonts.getHeight() * scale)) / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws {@code text} centered in a field of the given size.
|
||||
*/
|
||||
public static void drawCenteredText(RenderScope renderScope, String text, int width, int height, int font,
|
||||
int color) {
|
||||
int x = (width - (int) (Fonts.getWidth(text, font) * TEXT_SCALE)) / 2;
|
||||
renderScope.drawText(TEXT_SCALE, text, x, textY(height, TEXT_SCALE), font, color);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,9 @@ public class ImageTexture extends Element {
|
||||
if (sprite.toString().endsWith(".svg")) {
|
||||
MinecraftClientRef client = Providers.saturn.getClient();
|
||||
|
||||
int renderWidth = (int) (width * client.getWindow().getFramebufferWidth() / client.getWindow().getWidth());
|
||||
int renderWidth = (int) (width * client.getWindow().getFramebufferWidth() / client.getWindow().getWindowWidth());
|
||||
int renderHeight = (int) (height * client.getWindow().getFramebufferHeight()
|
||||
/ client.getWindow().getHeight());
|
||||
/ client.getWindow().getWindowHeight());
|
||||
|
||||
IdentifierRef pngId = SvgTexture.getSvg(sprite, renderWidth * 2, renderHeight * 2);
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.saturnclient.ui.elements;
|
||||
|
||||
import java.util.function.IntSupplier;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
|
||||
/**
|
||||
* A line of text whose content, font and color are read on every frame, so it
|
||||
* follows state and theme changes without rebuilding the screen.
|
||||
*/
|
||||
public class Label extends Element {
|
||||
private final Supplier<String> text;
|
||||
private final float textScale;
|
||||
private final IntSupplier font;
|
||||
private final IntSupplier color;
|
||||
|
||||
public Label(Supplier<String> text, float textScale, IntSupplier font, IntSupplier color) {
|
||||
this.text = text;
|
||||
this.textScale = textScale;
|
||||
this.font = font;
|
||||
this.color = color;
|
||||
this.height = (int) (Fonts.getHeight() * textScale);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawText(textScale, text.get(), 0, 0, font.getAsInt(), color.getAsInt());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.saturnclient.ui.elements;
|
||||
|
||||
import org.saturnclient.config.Config;
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
|
||||
/**
|
||||
* The translucent, outlined surface that screens lay their content on,
|
||||
* frosted over the game where glass is supported.
|
||||
*/
|
||||
public class Panel extends Element {
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
drawBackground(renderScope, width, height);
|
||||
}
|
||||
|
||||
public static void drawBackground(RenderScope renderScope, int width, int height) {
|
||||
renderScope.drawGlassRectangle(0, 0, width, height, Theme.BG_RADIUS.value, Config.menuBlur.value, 1,
|
||||
Theme.getPanelBg(), Theme.getBorder());
|
||||
}
|
||||
}
|
||||
@@ -4,23 +4,40 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.config.property.IntProperty;
|
||||
import org.saturnclient.config.property.Property;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.ElementRenderer;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
|
||||
/**
|
||||
* A vertically scrolling container. It scrolls with the mouse wheel or by
|
||||
* dragging its scrollbar, which shows along its right edge when the children
|
||||
* don't fit.
|
||||
*/
|
||||
public class Scroll extends Element {
|
||||
private static IntProperty scrollBarRadius = Property.integer(10);
|
||||
private static IntProperty scrollBarWidth = Property.integer(5);
|
||||
private static IntProperty scrollBarPadding = Property.integer(5);
|
||||
/** Units scrolled per mouse wheel notch */
|
||||
private static final double SCROLL_SPEED = 24;
|
||||
/** Space left below the last child when scrolled to the end */
|
||||
private static final int END_SPACE = 10;
|
||||
|
||||
private static final int BAR_WIDTH = 5;
|
||||
private static final int BAR_INSET = 5;
|
||||
private static final int MIN_THUMB = 20;
|
||||
|
||||
/** Width to keep clear at the right of the content for the scrollbar and its click area */
|
||||
public static final int SCROLLBAR_SPACE = BAR_WIDTH + BAR_INSET * 2 + 4;
|
||||
|
||||
int padding = 0;
|
||||
boolean background = true;
|
||||
|
||||
protected List<Element> children = new ArrayList<>();
|
||||
int scroll = 0;
|
||||
int maxScroll = 0;
|
||||
|
||||
/** Kept fractional so small trackpad deltas add up instead of being dropped */
|
||||
private double scroll = 0;
|
||||
private int maxScroll = 0;
|
||||
|
||||
/** Where in the thumb it was grabbed, while it's being dragged */
|
||||
private Integer thumbGrab = null;
|
||||
|
||||
public Scroll(int padding) {
|
||||
this.padding = padding;
|
||||
@@ -30,84 +47,57 @@ public class Scroll extends Element {
|
||||
ElementRenderer.draw(children, element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips the scroll's own background, for when it sits on a {@link Panel}.
|
||||
*/
|
||||
public Scroll noBackground() {
|
||||
this.background = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes every child and scrolls back to the top.
|
||||
*/
|
||||
public void clear() {
|
||||
children.clear();
|
||||
scroll = 0;
|
||||
maxScroll = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
calculateMaxScroll();
|
||||
updateMaxScroll();
|
||||
int offset = getOffset();
|
||||
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height,
|
||||
Theme.BG_RADIUS.value, Theme.BACKGROUND.value);
|
||||
if (background) {
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height,
|
||||
Theme.BG_RADIUS.value, Theme.BACKGROUND.value);
|
||||
}
|
||||
|
||||
renderScope.enableScissor(padding, padding, width - padding, height - padding);
|
||||
renderScope.getMatrixStack().push();
|
||||
renderScope.getMatrixStack().translate(padding, -scroll + padding);
|
||||
renderScope.getMatrixStack().translate(padding, -offset + padding);
|
||||
|
||||
ElementRenderer.render(children, ctx.elapsed, renderScope,
|
||||
ctx.mouseX - padding,
|
||||
ctx.mouseY - padding + scroll);
|
||||
ctx.mouseY - padding + offset,
|
||||
ctx.childFrame(padding, padding - offset, padding, height - padding));
|
||||
|
||||
renderScope.getMatrixStack().pop();
|
||||
renderScope.disableScissor();
|
||||
|
||||
if (maxScroll > 0) {
|
||||
renderScope.drawRoundedRectangle(
|
||||
width - scrollBarWidth.value - scrollBarPadding.value,
|
||||
calculateScrollBarY(),
|
||||
scrollBarWidth.value,
|
||||
calculateScrollBarHeight(),
|
||||
scrollBarRadius.value,
|
||||
Theme.SCROLL.value);
|
||||
boolean active = thumbGrab != null || ctx.isHovering(getBarX() - BAR_INSET, 0, BAR_WIDTH + BAR_INSET * 2,
|
||||
height);
|
||||
|
||||
renderScope.drawRoundedRectangle(getBarX(), getThumbY(), BAR_WIDTH, getThumbHeight(), BAR_WIDTH / 2,
|
||||
active ? Theme.SCROLL.value : Theme.withAlpha(0.7f, Theme.SCROLL.value));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scroll(int mouseX, int mouseY, double horizontalAmount, double verticalAmount) {
|
||||
scroll -= verticalAmount;
|
||||
if (scroll < 0) {
|
||||
scroll = 0;
|
||||
} else if (scroll > maxScroll) {
|
||||
scroll = maxScroll;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Element dimensions(int width, int height) {
|
||||
super.dimensions(width, height);
|
||||
|
||||
maxScroll = 0;
|
||||
for (Element element : children) {
|
||||
maxScroll = Math.max(maxScroll, (element.y + element.height + padding) - (height - padding * 2));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
int calculateScrollBarHeight() {
|
||||
if (maxScroll <= 0)
|
||||
return height - (scrollBarPadding.value * 2);
|
||||
return Math.max(20, (height * height) / (height + maxScroll + 10)) - (scrollBarPadding.value * 2);
|
||||
}
|
||||
|
||||
int calculateScrollBarY() {
|
||||
int scrollBarHeight = calculateScrollBarHeight();
|
||||
if (maxScroll <= 0)
|
||||
return scrollBarPadding.value;
|
||||
int y = (scroll * (height - scrollBarHeight - scrollBarPadding.value * 2)) / maxScroll + scrollBarPadding.value;
|
||||
return Math.min(y, height - scrollBarHeight - scrollBarPadding.value);
|
||||
}
|
||||
|
||||
private void calculateMaxScroll() {
|
||||
maxScroll = 0;
|
||||
|
||||
for (Element element : children) {
|
||||
int bottom = element.y + element.height;
|
||||
maxScroll = Math.max(maxScroll, bottom - height + padding);
|
||||
}
|
||||
|
||||
if (maxScroll > 0) {
|
||||
maxScroll = maxScroll + 10;
|
||||
} else {
|
||||
maxScroll = 0;
|
||||
}
|
||||
setScroll(scroll - verticalAmount * SCROLL_SPEED);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -117,17 +107,89 @@ public class Scroll extends Element {
|
||||
|
||||
@Override
|
||||
public void mouseClicked(double mouseX, double mouseY, int button) {
|
||||
ElementRenderer.mouseClicked(children, mouseX - padding, mouseY - padding + scroll, button);
|
||||
// Children scrolled out of view still have positions under the rest of the
|
||||
// screen, so only clicks inside the scroll's own bounds may reach them.
|
||||
if (mouseX < 0 || mouseY < 0 || mouseX > width || mouseY > height) {
|
||||
for (Element child : children) {
|
||||
child.focused = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxScroll > 0 && mouseX >= getBarX() - BAR_INSET) {
|
||||
int thumbY = getThumbY();
|
||||
int thumbHeight = getThumbHeight();
|
||||
|
||||
// Grab the thumb where it was clicked, or center it on a click in the track.
|
||||
boolean onThumb = mouseY >= thumbY && mouseY <= thumbY + thumbHeight;
|
||||
thumbGrab = onThumb ? (int) mouseY - thumbY : thumbHeight / 2;
|
||||
dragThumb(mouseY);
|
||||
return;
|
||||
}
|
||||
|
||||
ElementRenderer.mouseClicked(children, mouseX - padding, mouseY - padding + getOffset(), button);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
|
||||
ElementRenderer.mouseDragged(children, mouseX - padding, mouseY - padding + scroll, button, deltaX,
|
||||
if (thumbGrab != null) {
|
||||
dragThumb(mouseY);
|
||||
return;
|
||||
}
|
||||
|
||||
ElementRenderer.mouseDragged(children, mouseX - padding, mouseY - padding + getOffset(), button, deltaX,
|
||||
deltaY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(double mouseX, double mouseY, int button) {
|
||||
ElementRenderer.mouseReleased(children, mouseX - padding, mouseY - padding + scroll, button);
|
||||
thumbGrab = null;
|
||||
ElementRenderer.mouseReleased(children, mouseX - padding, mouseY - padding + getOffset(), button);
|
||||
}
|
||||
|
||||
private void dragThumb(double mouseY) {
|
||||
int travel = getTrackHeight() - getThumbHeight();
|
||||
if (travel > 0) {
|
||||
setScroll((mouseY - thumbGrab - BAR_INSET) / travel * maxScroll);
|
||||
}
|
||||
}
|
||||
|
||||
private void setScroll(double value) {
|
||||
scroll = Math.max(0, Math.min(maxScroll, value));
|
||||
}
|
||||
|
||||
private int getOffset() {
|
||||
return (int) Math.round(scroll);
|
||||
}
|
||||
|
||||
private void updateMaxScroll() {
|
||||
int bottom = 0;
|
||||
for (Element element : children) {
|
||||
bottom = Math.max(bottom, element.y + element.height);
|
||||
}
|
||||
|
||||
int overflow = bottom + padding * 2 - height;
|
||||
maxScroll = overflow > 0 ? overflow + END_SPACE : 0;
|
||||
|
||||
// The content may have shrunk, such as after filtering.
|
||||
setScroll(scroll);
|
||||
}
|
||||
|
||||
private int getBarX() {
|
||||
return width - BAR_WIDTH - BAR_INSET;
|
||||
}
|
||||
|
||||
private int getTrackHeight() {
|
||||
return height - BAR_INSET * 2;
|
||||
}
|
||||
|
||||
private int getThumbHeight() {
|
||||
int track = getTrackHeight();
|
||||
return Math.max(MIN_THUMB, (int) ((long) track * height / (height + maxScroll)));
|
||||
}
|
||||
|
||||
private int getThumbY() {
|
||||
int travel = getTrackHeight() - getThumbHeight();
|
||||
return BAR_INSET + (int) (travel * (scroll / maxScroll));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.saturnclient.ui.elements;
|
||||
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.components.inputs.Toggle;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
|
||||
/**
|
||||
* A labelled on/off switch ("Enabled" / "Disabled") that tints itself with the
|
||||
* accent color while on.
|
||||
*/
|
||||
public class SwitchButton extends Element {
|
||||
private static final int HEIGHT = 34;
|
||||
private static final int PADDING_LEFT = 14;
|
||||
private static final int PADDING_RIGHT = 5;
|
||||
private static final int GAP = 10;
|
||||
private static final float TEXT_SCALE = 0.66f;
|
||||
|
||||
private final String onText;
|
||||
private final String offText;
|
||||
private final BooleanSupplier value;
|
||||
private final Runnable onToggle;
|
||||
|
||||
public SwitchButton(String onText, String offText, BooleanSupplier value, Runnable onToggle) {
|
||||
this.onText = onText;
|
||||
this.offText = offText;
|
||||
this.value = value;
|
||||
this.onToggle = onToggle;
|
||||
this.height = HEIGHT;
|
||||
|
||||
// Sized for the longer label, so the button doesn't change width when toggled
|
||||
int textWidth = Math.max(Fonts.getWidth(onText, Theme.FONT.value), Fonts.getWidth(offText, Theme.FONT.value));
|
||||
this.width = PADDING_LEFT + (int) (textWidth * TEXT_SCALE) + GAP + Toggle.SWITCH_WIDTH + PADDING_RIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
boolean on = value.getAsBoolean();
|
||||
float tint = ctx.isHovering() ? 0.1f : 0.06f;
|
||||
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value,
|
||||
on ? Theme.withAlpha(tint * 2.5f, Theme.ACCENT.value) : Theme.withAlpha(tint, Theme.FOREGROUND.value));
|
||||
|
||||
renderScope.drawText(TEXT_SCALE, on ? onText : offText, PADDING_LEFT, Field.textY(height, TEXT_SCALE),
|
||||
Theme.FONT.value, on ? Theme.ACCENT.value : Theme.PRIMARY_FG.value);
|
||||
|
||||
Toggle.drawSwitch(renderScope, width - PADDING_RIGHT - Toggle.SWITCH_WIDTH,
|
||||
(height - Toggle.SWITCH_HEIGHT) / 2, on);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
onToggle.run();
|
||||
}
|
||||
}
|
||||
@@ -1,63 +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;
|
||||
import org.saturnclient.ui.Utils;
|
||||
|
||||
public class TabMenu extends Element {
|
||||
public static class TabMenuComponent {
|
||||
Runnable onClick;
|
||||
IdentifierRef sprite;
|
||||
|
||||
public TabMenuComponent(IdentifierRef sprite, Runnable onClick) {
|
||||
this.onClick = onClick;
|
||||
this.sprite = sprite;
|
||||
}
|
||||
}
|
||||
|
||||
TabMenuComponent[] components;
|
||||
public int selectedScreen = 0;
|
||||
|
||||
public TabMenu(int active, TabMenuComponent... components) {
|
||||
this.components = components;
|
||||
width = 25 * components.length + 6;
|
||||
height = 30;
|
||||
this.selectedScreen = active;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, Theme.PRIMARY.value);
|
||||
|
||||
int col = 0;
|
||||
for (TabMenuComponent component : components) {
|
||||
int x = 25 * col + 5;
|
||||
|
||||
boolean hovering = ctx.isHovering(x, 5, 20, 20) || selectedScreen == col;
|
||||
|
||||
if (hovering) {
|
||||
renderScope.drawRoundedRectangle(x, 5, 20, 20, Theme.WIDGET_RADIUS.value, Theme.ACCENT.value);
|
||||
}
|
||||
|
||||
renderScope.drawTexture(component.sprite, x + 5, 10, 0, 0, 10, 10,
|
||||
hovering ? Theme.ACCENT_FG.value : Theme.PRIMARY_FG.value);
|
||||
|
||||
col++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
int col = 0;
|
||||
for (TabMenuComponent component : components) {
|
||||
int x = 25 * col + 5;
|
||||
if (Utils.isHovering(mouseX - x, mouseY - 5, 20, 20, 1.0f)) {
|
||||
component.onClick.run();
|
||||
}
|
||||
col++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,24 @@ public class Fonts {
|
||||
return 18;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortens {@code text} with a trailing "…" so it fits in {@code maxWidth}
|
||||
* when drawn at {@code scale}.
|
||||
*/
|
||||
public static String ellipsize(String text, int font, float scale, int maxWidth) {
|
||||
if (getWidth(text, font) * scale <= maxWidth) {
|
||||
return text;
|
||||
}
|
||||
|
||||
String ellipsis = "…";
|
||||
int end = text.length();
|
||||
while (end > 0 && getWidth(text.substring(0, end).stripTrailing() + ellipsis, font) * scale > maxWidth) {
|
||||
end--;
|
||||
}
|
||||
|
||||
return text.substring(0, end).stripTrailing() + ellipsis;
|
||||
}
|
||||
|
||||
public static int centerX(int w, String text, int font) {
|
||||
return (w - Fonts.getWidth(text, font)) / 2;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,35 @@
|
||||
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 {
|
||||
public static final IdentifierRef CROSSHAIR_RANGE = IdentifierRef
|
||||
.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)
|
||||
public static final IdentifierRef ICON_MODS = IdentifierRef.ofSaturn("textures/gui/components/mods.svg");
|
||||
public static final IdentifierRef ICON_COSMETICS = IdentifierRef.ofSaturn("textures/gui/components/cosmetics.svg");
|
||||
public static final IdentifierRef ICON_HUD_EDITOR = IdentifierRef.ofSaturn("textures/gui/components/hud_editor.svg");
|
||||
public static final IdentifierRef ICON_SETTINGS = IdentifierRef.ofSaturn("textures/gui/components/settings.svg");
|
||||
public static final IdentifierRef ICON_CLOSE = IdentifierRef.ofSaturn("textures/gui/components/close.svg");
|
||||
public static final IdentifierRef ICON_SEARCH = IdentifierRef.ofSaturn("textures/gui/components/search.svg");
|
||||
public static final IdentifierRef ICON_SLIDERS = IdentifierRef.ofSaturn("textures/gui/components/sliders.svg");
|
||||
public static final IdentifierRef ICON_PALETTE = IdentifierRef.ofSaturn("textures/gui/components/palette.svg");
|
||||
public static final IdentifierRef ICON_ANIMATIONS = IdentifierRef
|
||||
.ofSaturn("textures/gui/components/animations.svg");
|
||||
public static final IdentifierRef ICON_RESET = IdentifierRef.ofSaturn("textures/gui/components/reset.svg");
|
||||
public static final IdentifierRef ICON_CHEVRON_LEFT = IdentifierRef
|
||||
.ofSaturn("textures/gui/components/chevron_left.svg");
|
||||
public static final IdentifierRef ICON_CHEVRON_RIGHT = IdentifierRef
|
||||
.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");
|
||||
public static final IdentifierRef REALISTIC_LOGO = IdentifierRef.ofSaturn("textures/logo/realistic.png");
|
||||
@@ -31,15 +37,17 @@ public class Textures {
|
||||
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");
|
||||
|
||||
// Widgets
|
||||
public static final IdentifierRef BUTTON_BORDER = IdentifierRef.ofSaturn("button_border");
|
||||
public static final IdentifierRef BUTTON = IdentifierRef.ofSaturn("button");
|
||||
public static final IdentifierRef BUTTON_OP = IdentifierRef.ofSaturn("button_op");
|
||||
public static final IdentifierRef SETTINGS_BG = IdentifierRef.ofSaturn("settings_bg");
|
||||
public static final IdentifierRef RECT = IdentifierRef.ofSaturn("rect");
|
||||
public static final IdentifierRef RECT_BORDER = IdentifierRef.ofSaturn("rect_border");
|
||||
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");
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package org.saturnclient.ui.screens;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
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.config.property.ColorProperty;
|
||||
import org.saturnclient.config.property.NamespaceProperty;
|
||||
import org.saturnclient.config.property.Property;
|
||||
import org.saturnclient.ui.PanelScreen;
|
||||
import org.saturnclient.ui.components.ColorGrid;
|
||||
import org.saturnclient.ui.components.PropertyList;
|
||||
import org.saturnclient.ui.components.SectionNav;
|
||||
import org.saturnclient.ui.components.Sidebar;
|
||||
import org.saturnclient.ui.components.ThemePresetCard;
|
||||
import org.saturnclient.ui.elements.ActionButton;
|
||||
import org.saturnclient.ui.elements.Divider;
|
||||
import org.saturnclient.ui.elements.Label;
|
||||
import org.saturnclient.ui.elements.Scroll;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
|
||||
/**
|
||||
* The client's own settings, split into sections picked from a list on the left.
|
||||
*/
|
||||
public class ClientSettings extends PanelScreen {
|
||||
private enum Section {
|
||||
GENERAL("General", "Title screen, emotes and cosmetics", Textures.ICON_SLIDERS),
|
||||
UI_STYLE("UI Style", "Colors, corners and font for every Saturn screen", Textures.ICON_PALETTE),
|
||||
ANIMATIONS("Animations", "How long menu animations take, and their easing", Textures.ICON_ANIMATIONS);
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final IdentifierRef icon;
|
||||
|
||||
Section(String title, String description, IdentifierRef icon) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.icon = icon;
|
||||
}
|
||||
}
|
||||
|
||||
private static final int NAV_WIDTH = 170;
|
||||
private static final int NAV_GAP = 16;
|
||||
private static final int LIST_Y = 58;
|
||||
private static final int SECTION_GAP = 22;
|
||||
private static final int PRESET_GAP = 10;
|
||||
|
||||
private Section section = Section.GENERAL;
|
||||
|
||||
public ClientSettings() {
|
||||
super("Settings", Sidebar.SETTINGS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void panel() {
|
||||
List<SectionNav.Item> items = Stream.of(Section.values())
|
||||
.map(s -> new SectionNav.Item(s.icon, s.title))
|
||||
.toList();
|
||||
|
||||
draw(new SectionNav("SATURN CLIENT", items, section.ordinal(), NAV_WIDTH, i -> {
|
||||
section = Section.values()[i];
|
||||
init();
|
||||
}).position(contentX, contentY + 4));
|
||||
|
||||
int dividerX = contentX + NAV_WIDTH + NAV_GAP;
|
||||
draw(new Divider(1).dimensions(1, panelHeight).position(dividerX, panelY));
|
||||
|
||||
int x = dividerX + 1 + NAV_GAP + 4;
|
||||
int width = contentX + contentWidth - x;
|
||||
|
||||
// Header
|
||||
draw(new Label(() -> section.title, 1.15f, () -> Theme.FONT.value, () -> Theme.FOREGROUND.value)
|
||||
.position(x, contentY + 2));
|
||||
draw(new Label(() -> section.description, 0.66f, Theme::getBodyFont, Theme::getMutedFg)
|
||||
.position(x, contentY + 28));
|
||||
|
||||
ActionButton reset = new ActionButton(Textures.ICON_RESET, "Reset", true, this::reset);
|
||||
draw(reset.position(x + width - reset.width, contentY + 4));
|
||||
|
||||
// The section's settings
|
||||
Scroll scroll = new Scroll(0).noBackground();
|
||||
int listWidth = scrollContentWidth(width);
|
||||
|
||||
switch (section) {
|
||||
case GENERAL -> PropertyList.draw(scroll, getGeneralProperties(), Set.of(), listWidth, 0);
|
||||
case UI_STYLE -> drawUiStyle(scroll, listWidth);
|
||||
case ANIMATIONS -> drawAnimations(scroll, listWidth);
|
||||
}
|
||||
|
||||
draw(scroll.dimensions(scrollWidth(width), contentHeight - LIST_Y).position(x, contentY + LIST_Y));
|
||||
}
|
||||
|
||||
private void drawUiStyle(Scroll scroll, int width) {
|
||||
Map<String, Property> properties = Theme.config.getProperties();
|
||||
|
||||
int y = PropertyList.drawSection(scroll, "Presets", 0) + 10;
|
||||
int cardWidth = (width - PRESET_GAP * (Theme.PRESETS.size() - 1)) / Theme.PRESETS.size();
|
||||
|
||||
for (int i = 0; i < Theme.PRESETS.size(); i++) {
|
||||
// Rebuild so the color inputs show the preset's colors
|
||||
scroll.draw(new ThemePresetCard(Theme.PRESETS.get(i), cardWidth, this::init)
|
||||
.position(i * (cardWidth + PRESET_GAP), y));
|
||||
}
|
||||
|
||||
y += ThemePresetCard.HEIGHT + SECTION_GAP;
|
||||
y = PropertyList.drawSection(scroll, "Colors", y) + 10;
|
||||
y = ColorGrid.draw(scroll, properties, width, y) + SECTION_GAP;
|
||||
|
||||
y = PropertyList.drawSection(scroll, "Shape & type", y) + 4;
|
||||
PropertyList.draw(scroll, properties, getNames(properties, p -> p instanceof ColorProperty), width, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Each menu's animation settings under the menu's name.
|
||||
*/
|
||||
private static void drawAnimations(Scroll scroll, int width) {
|
||||
int y = 0;
|
||||
|
||||
for (AnimationConfig animation : AnimationConfig.ALL) {
|
||||
y = PropertyList.drawSection(scroll, animation.name, y) + 4;
|
||||
y = PropertyList.draw(scroll, animation.getProperties(), Set.of(), width, y) + SECTION_GAP;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The top-level settings. The namespaces have sections of their own.
|
||||
*/
|
||||
private static Map<String, Property> getGeneralProperties() {
|
||||
return Config.config.getProperties().entrySet().stream()
|
||||
.filter(e -> !(e.getValue() instanceof NamespaceProperty))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a,
|
||||
LinkedHashMap::new));
|
||||
}
|
||||
|
||||
private static Set<String> getNames(Map<String, Property> properties, Predicate<Property> filter) {
|
||||
return properties.entrySet().stream()
|
||||
.filter(e -> filter.test(e.getValue()))
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the current section's settings back to their defaults.
|
||||
*/
|
||||
private void reset() {
|
||||
Map<String, Property> properties = switch (section) {
|
||||
case GENERAL -> getGeneralProperties();
|
||||
case UI_STYLE -> Theme.config.getProperties();
|
||||
case ANIMATIONS -> AnimationConfig.config.getProperties();
|
||||
};
|
||||
|
||||
properties.values().forEach(Property::reset);
|
||||
init();
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
package org.saturnclient.ui.screens;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.saturnclient.config.ConfigManager;
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.config.property.BoolProperty;
|
||||
import org.saturnclient.config.property.ColorProperty;
|
||||
import org.saturnclient.config.property.FloatProperty;
|
||||
import org.saturnclient.config.property.IntProperty;
|
||||
import org.saturnclient.config.property.KeybindingProperty;
|
||||
import org.saturnclient.config.property.NamespaceProperty;
|
||||
import org.saturnclient.config.property.Property;
|
||||
import org.saturnclient.config.property.SelectProperty;
|
||||
import org.saturnclient.mod.HudMod;
|
||||
import org.saturnclient.mod.ModLayout;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.SaturnScreen;
|
||||
import org.saturnclient.ui.components.HudModPreview;
|
||||
import org.saturnclient.ui.components.Sidebar;
|
||||
import org.saturnclient.ui.components.inputs.FloatInput;
|
||||
import org.saturnclient.ui.components.inputs.ColorInput;
|
||||
import org.saturnclient.ui.components.inputs.IntInput;
|
||||
import org.saturnclient.ui.components.inputs.KeybindingSelector;
|
||||
import org.saturnclient.ui.components.inputs.Select;
|
||||
import org.saturnclient.ui.components.inputs.Toggle;
|
||||
import org.saturnclient.ui.elements.Scroll;
|
||||
import org.saturnclient.ui.elements.Text;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
|
||||
public class ConfigEditor extends SaturnScreen {
|
||||
private ConfigManager config;
|
||||
private HudMod hudMod;
|
||||
private HudModPreview preview;
|
||||
|
||||
public ConfigEditor(ConfigManager config) {
|
||||
super("Config Editor");
|
||||
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public ConfigEditor(ConfigManager config, HudMod hudMod) {
|
||||
super("Config Editor");
|
||||
|
||||
this.config = config;
|
||||
this.hudMod = hudMod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ui() {
|
||||
int p = 10;
|
||||
int g = 10;
|
||||
|
||||
Scroll scroll = new Scroll(p);
|
||||
|
||||
drawProperties(scroll, config.getProperties(), 0, 0, 480 + 10 + (g * 2));
|
||||
|
||||
int scrollWidth = 480 + 10 + (g * 2) + (p * 2);
|
||||
|
||||
if (this.hudMod != null) {
|
||||
this.preview = new HudModPreview(this.hudMod);
|
||||
}
|
||||
|
||||
draw(scroll.dimensions(scrollWidth, 350).center(width, height));
|
||||
|
||||
draw(new Sidebar(1, this.provider::close).center(width, height, -(scrollWidth / 2 + 20), 0));
|
||||
}
|
||||
|
||||
private int drawProperties(Scroll configScroll, Map<String, Property> properties, int row, int col, int w) {
|
||||
for (Map.Entry<String, Property> propEntry : properties.entrySet()) {
|
||||
Property prop = propEntry.getValue();
|
||||
String propName = propEntry.getKey();
|
||||
boolean full = isFull(prop);
|
||||
|
||||
// Always reset to col 0 if full-width component needs to be drawn mid-row
|
||||
if (full && col != 0) {
|
||||
col = 0;
|
||||
row++;
|
||||
}
|
||||
|
||||
int modX = (w / 2) * col; // two-column layout
|
||||
int modY = 25 * row;
|
||||
|
||||
if (prop instanceof NamespaceProperty) {
|
||||
configScroll.draw(
|
||||
new Text(propName)
|
||||
.position(Fonts.centerX(520, propName, Theme.FONT.value), modY + 1)
|
||||
.scale(0.7f));
|
||||
} else {
|
||||
configScroll.draw(
|
||||
new Text(propName)
|
||||
.position(modX, modY + 1)
|
||||
.scale(0.7f));
|
||||
}
|
||||
|
||||
// instanceof dispatch
|
||||
if (prop instanceof BoolProperty p) {
|
||||
configScroll.draw(
|
||||
new Toggle(p)
|
||||
.position(modX + (w / 2) - 40, modY)
|
||||
.scale(0.5f));
|
||||
}
|
||||
|
||||
else if (prop instanceof ColorProperty p) {
|
||||
configScroll.draw(
|
||||
new ColorInput(p)
|
||||
.position(w / 2, modY));
|
||||
}
|
||||
|
||||
else if (prop instanceof IntProperty p) {
|
||||
configScroll.draw(
|
||||
new IntInput(p)
|
||||
.position(w / 2, modY));
|
||||
}
|
||||
|
||||
else if (prop instanceof FloatProperty p) {
|
||||
configScroll.draw(
|
||||
new FloatInput(p)
|
||||
.position(w / 2, modY));
|
||||
}
|
||||
|
||||
else if (prop instanceof NamespaceProperty p) {
|
||||
Map<String, Property> nestedProperties = p.value;
|
||||
row = drawProperties(configScroll, nestedProperties, row + 1, col, w);
|
||||
}
|
||||
|
||||
else if (prop instanceof SelectProperty p) {
|
||||
configScroll.draw(
|
||||
new Select(p)
|
||||
.position(w / 2, modY));
|
||||
}
|
||||
|
||||
else if (prop instanceof KeybindingProperty p) {
|
||||
configScroll.draw(
|
||||
new KeybindingSelector(p)
|
||||
.position(w / 2, modY));
|
||||
}
|
||||
|
||||
// STRING → intentionally no component (same as before)
|
||||
|
||||
if (full) {
|
||||
row++; // full-width takes a whole row
|
||||
col = 0;
|
||||
} else {
|
||||
col++;
|
||||
if (col > 1) { // max 2 columns
|
||||
col = 0;
|
||||
row++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
public static boolean isFull(Property prop) {
|
||||
return !(prop instanceof BoolProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, int mouseX, int mouseY, float delta, long elapsed) {
|
||||
if (this.preview != null) {
|
||||
ModLayout layout = hudMod.getDimensions();
|
||||
draw(this.preview
|
||||
.dimensions(layout.width, (int) (layout.height * layout.scale.value * 2.0f))
|
||||
.scale(1.0f) // Because the centering won't work properly
|
||||
.center(width, height, (530 / 2) + 40, 0)
|
||||
.scale(layout.scale.value * 2.0f));
|
||||
}
|
||||
|
||||
super.render(renderScope, mouseX, mouseY, delta, elapsed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package org.saturnclient.ui.screens;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.saturnclient.client.ServiceClient;
|
||||
import org.saturnclient.client.player.SaturnPlayer;
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.config.AnimationConfig;
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.cosmetics.Cloaks;
|
||||
import org.saturnclient.cosmetics.Hats;
|
||||
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;
|
||||
import org.saturnclient.ui.components.SkinPreview;
|
||||
import org.saturnclient.ui.elements.AnimationStagger;
|
||||
import org.saturnclient.ui.elements.ChipGroup;
|
||||
import org.saturnclient.ui.elements.Label;
|
||||
import org.saturnclient.ui.elements.Scroll;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
|
||||
/**
|
||||
* Cloaks and hats: the player on the left, turned by dragging, and a grid of
|
||||
* every cosmetic on the right. Clicking an owned cosmetic equips it; clicking
|
||||
* one not owned yet claims it, which is free.
|
||||
*/
|
||||
public class CosmeticsMenu extends PanelScreen {
|
||||
private enum Kind {
|
||||
CLOAKS("Cloaks", "Cloak", Cloaks.ALL_CLOAKS, () -> Cloaks.availableCloaks, player -> player.cloak,
|
||||
Cloaks::setCloak, ServiceClient::buyCloak, Textures::getCloakPreview, 0.5f, 150f),
|
||||
HATS("Hats", "Hat", Hats.ALL_HATS, () -> Hats.availableHats, player -> player.hat,
|
||||
Hats::setHat, ServiceClient::buyHat, Textures::getHatPreview, 1f, -25f);
|
||||
|
||||
final String title;
|
||||
final String singular;
|
||||
final String[] all;
|
||||
final Supplier<List<String>> owned;
|
||||
final Function<SaturnPlayer, String> equipped;
|
||||
final Consumer<String> equip;
|
||||
final Consumer<String> claim;
|
||||
final Function<String, IdentifierRef> preview;
|
||||
/** Width divided by height of the preview textures */
|
||||
final float previewAspect;
|
||||
/** The angle the player starts at, showing off this kind of cosmetic */
|
||||
final float yaw;
|
||||
|
||||
Kind(String title, String singular, String[] all, Supplier<List<String>> owned,
|
||||
Function<SaturnPlayer, String> equipped, Consumer<String> equip, Consumer<String> claim,
|
||||
Function<String, IdentifierRef> preview, float previewAspect, float yaw) {
|
||||
this.title = title;
|
||||
this.singular = singular;
|
||||
this.all = all;
|
||||
this.owned = owned;
|
||||
this.equipped = equipped;
|
||||
this.equip = equip;
|
||||
this.claim = claim;
|
||||
this.preview = preview;
|
||||
this.previewAspect = previewAspect;
|
||||
this.yaw = yaw;
|
||||
}
|
||||
|
||||
/**
|
||||
* "" (nothing), then every cosmetic of this kind.
|
||||
*/
|
||||
List<String> ids() {
|
||||
List<String> ids = new ArrayList<>();
|
||||
ids.add("");
|
||||
ids.addAll(List.of(all));
|
||||
return ids;
|
||||
}
|
||||
|
||||
String getEquipped() {
|
||||
SaturnPlayer player = SaturnPlayer.get();
|
||||
return player == null ? "" : equipped.apply(player);
|
||||
}
|
||||
}
|
||||
|
||||
/** Names that aren't just the id with each word capitalized */
|
||||
private static final Map<String, String> NAMES = Map.of(
|
||||
"", "None",
|
||||
"amg", "AMG",
|
||||
"amg_petronas", "AMG Petronas",
|
||||
"bmw", "BMW",
|
||||
"redbull", "Red Bull");
|
||||
|
||||
private static final int STAGE_WIDTH = 300;
|
||||
private static final int STAGE_PADDING = 16;
|
||||
private static final int STAGE_FOOTER = 70;
|
||||
private static final int COLUMN_GAP = 20;
|
||||
private static final int COLUMNS = 4;
|
||||
private static final int TILE_GAP = 10;
|
||||
private static final int GRID_Y = 58;
|
||||
private static final float SMALL_SCALE = 0.6f;
|
||||
|
||||
private Kind kind = Kind.CLOAKS;
|
||||
|
||||
public CosmeticsMenu() {
|
||||
super("Cosmetics", Sidebar.COSMETICS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void panel() {
|
||||
drawStage();
|
||||
|
||||
int x = contentX + STAGE_WIDTH + COLUMN_GAP;
|
||||
int width = contentX + contentWidth - x;
|
||||
|
||||
// Header
|
||||
draw(new Label(() -> "Cosmetics", 1.15f, () -> Theme.FONT.value, () -> Theme.FOREGROUND.value)
|
||||
.position(x, contentY + 2));
|
||||
draw(new Label(this::getOwnedSummary, 0.66f, Theme::getBodyFont, Theme::getMutedFg)
|
||||
.position(x, contentY + 28));
|
||||
|
||||
ChipGroup tabs = new ChipGroup(List.of(Kind.CLOAKS.title, Kind.HATS.title), kind.title, selected -> {
|
||||
kind = selected.equals(Kind.HATS.title) ? Kind.HATS : Kind.CLOAKS;
|
||||
init();
|
||||
});
|
||||
draw(tabs.position(x + width - tabs.width, contentY + 4));
|
||||
|
||||
// Grid
|
||||
Scroll scroll = new Scroll(0).noBackground();
|
||||
int gridHeight = contentHeight - GRID_Y;
|
||||
|
||||
if (SaturnPlayer.get() == null) {
|
||||
String message = "Connect to Saturn to change your cosmetics";
|
||||
int messageX = (width - (int) (Fonts.getWidth(message, Theme.getBodyFont()) * 0.7f)) / 2;
|
||||
scroll.draw(new Label(() -> message, 0.7f, Theme::getBodyFont, Theme::getMutedFg)
|
||||
.position(messageX, 60));
|
||||
} else {
|
||||
scroll.draw(createGrid(scrollContentWidth(width)));
|
||||
}
|
||||
|
||||
draw(scroll.dimensions(scrollWidth(width), gridHeight).position(x, contentY + GRID_Y));
|
||||
}
|
||||
|
||||
/**
|
||||
* The left column: the player, and what they have equipped.
|
||||
*/
|
||||
private void drawStage() {
|
||||
draw(new Stage().dimensions(STAGE_WIDTH, contentHeight).position(contentX, contentY));
|
||||
|
||||
int previewY = contentY + STAGE_PADDING + 24;
|
||||
int previewHeight = contentHeight - STAGE_PADDING - 24 - STAGE_FOOTER;
|
||||
draw(new SkinPreview(kind.yaw).dimensions(STAGE_WIDTH, previewHeight).position(contentX, previewY));
|
||||
|
||||
int footerY = contentY + contentHeight - STAGE_FOOTER + 8;
|
||||
draw(new Label(() -> getName(kind.getEquipped()), 1f, () -> Theme.FONT.value, () -> Theme.FOREGROUND.value)
|
||||
.position(contentX + STAGE_PADDING + 4, footerY));
|
||||
draw(new Label(() -> kind.singular + " · Equipped", 0.64f, Theme::getBodyFont, Theme::getMutedFg)
|
||||
.position(contentX + STAGE_PADDING + 4, footerY + 26));
|
||||
}
|
||||
|
||||
private Element createGrid(int width) {
|
||||
int tileWidth = (width - TILE_GAP * (COLUMNS - 1)) / COLUMNS;
|
||||
AnimationStagger grid = new AnimationStagger(AnimationConfig.cosmeticsMenu);
|
||||
List<String> ids = kind.ids();
|
||||
|
||||
for (int i = 0; i < ids.size(); i++) {
|
||||
String id = ids.get(i);
|
||||
Kind k = kind;
|
||||
|
||||
CosmeticTile tile = new CosmeticTile(getName(id), k.preview.apply(id), k.previewAspect, tileWidth,
|
||||
() -> k.owned.get().contains(id),
|
||||
() -> k.getEquipped().equals(id),
|
||||
() -> k.equip.accept(id),
|
||||
() -> k.claim.accept(id));
|
||||
|
||||
tile.position((i % COLUMNS) * (tileWidth + TILE_GAP), (i / COLUMNS) * (CosmeticTile.HEIGHT + TILE_GAP));
|
||||
tile.animation(new SlideFade(AnimationConfig.cosmeticsMenu, -12));
|
||||
grid.draw(tile);
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
private String getOwnedSummary() {
|
||||
// The owned list also holds "", for nothing equipped.
|
||||
long owned = List.of(kind.all).stream().filter(kind.owned.get()::contains).count();
|
||||
return owned + " of " + kind.all.length + " " + kind.title.toLowerCase(Locale.ROOT)
|
||||
+ " owned · every cosmetic is free to claim";
|
||||
}
|
||||
|
||||
private static String getName(String id) {
|
||||
String name = NAMES.get(id);
|
||||
return name != null ? name : Utils.nameFromId(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* The darker card behind the player, with its heading and a hint that it
|
||||
* can be turned.
|
||||
*/
|
||||
private static class Stage extends Element {
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value + 4, 1,
|
||||
Theme.withAlpha(0.45f, 0xFF000000), Theme.withAlpha(0.05f, Theme.FOREGROUND.value));
|
||||
|
||||
renderScope.drawText(SMALL_SCALE, "PREVIEW", STAGE_PADDING, STAGE_PADDING, Theme.FONT.value,
|
||||
Theme.getMutedFg());
|
||||
|
||||
String hint = "Drag to rotate";
|
||||
int hintWidth = (int) (Fonts.getWidth(hint, Theme.getBodyFont()) * SMALL_SCALE);
|
||||
renderScope.drawText(SMALL_SCALE, hint, width - STAGE_PADDING - hintWidth, STAGE_PADDING,
|
||||
Theme.getBodyFont(), Theme.getMutedFg());
|
||||
|
||||
renderScope.drawRectangle(STAGE_PADDING, height - STAGE_FOOTER, width - STAGE_PADDING * 2, 1,
|
||||
Theme.getBorder());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,109 +1,181 @@
|
||||
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.property.FloatProperty;
|
||||
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");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FloatProperty getScaleSetting() {
|
||||
return Config.hudMenuScale;
|
||||
}
|
||||
|
||||
@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);
|
||||
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.drawGlassRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, Config.widgetBlur.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,76 @@
|
||||
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.Theme;
|
||||
import org.saturnclient.mod.HudMod;
|
||||
import org.saturnclient.mod.Mod;
|
||||
import org.saturnclient.mod.ModLayout;
|
||||
import org.saturnclient.mod.ModManager;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
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,
|
||||
* whatever Saturn's GUI scale.
|
||||
*/
|
||||
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 HudMod draggingMod = null;
|
||||
private boolean resizing = false;
|
||||
private int offsetX, offsetY;
|
||||
private static final int RESIZE_MARGIN = 3;
|
||||
|
||||
public HudEditor() {
|
||||
super("HUD Editor");
|
||||
for (Mod m : ModManager.ENABLED_MODS) {
|
||||
if (m.isEnabled() && m instanceof HudMod) {
|
||||
hudMods.add((HudMod) m);
|
||||
if (m.isEnabled() && m instanceof HudMod hudMod) {
|
||||
hudMods.add(hudMod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getScale() {
|
||||
return 1f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ui() {
|
||||
// Placing HUDs needs a clear view of the game.
|
||||
blurBackground = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||
if (button == 0) {
|
||||
for (int m = hudMods.size() - 1; m >= 0; m--) {
|
||||
HudMod mod = hudMods.get(m);
|
||||
HudMod mod = getModAt(mouseX, mouseY);
|
||||
|
||||
if (mod != null) {
|
||||
ModLayout dim = mod.getDimensions();
|
||||
|
||||
// Check if clicking the corner for resizing
|
||||
if (isCorner(mouseX, mouseY, dim)) {
|
||||
draggingMod = mod;
|
||||
resizing = true;
|
||||
offsetX = (int) (mouseX - (dim.x.value + dim.width * dim.scale.value));
|
||||
offsetY = (int) (mouseY - (dim.y.value + dim.height * dim.scale.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;
|
||||
}
|
||||
draggingMod = mod;
|
||||
resizing = isOnHandle(mouseX, mouseY, dim);
|
||||
offsetX = (int) (mouseX - (resizing ? right(dim) : dim.x.value));
|
||||
offsetY = (int) (mouseY - (resizing ? bottom(dim) : dim.y.value));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return super.mouseClicked(mouseX, mouseY, button);
|
||||
}
|
||||
|
||||
@@ -72,57 +89,115 @@ public class HudEditor extends SaturnScreen {
|
||||
ModLayout dim = draggingMod.getDimensions();
|
||||
|
||||
if (resizing) {
|
||||
// Scale based on cursor movement
|
||||
float newScale = Math.max(0.5f, ((float) (mouseX - dim.x.value) / dim.width));
|
||||
dim.scale.value = newScale;
|
||||
// The handle follows the cursor: the scale is the new width over the unscaled width.
|
||||
float scale = (float) (mouseX - offsetX - dim.x.value) / dim.width;
|
||||
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 {
|
||||
// Move the mod
|
||||
dim.x.value = (int) mouseX - offsetX;
|
||||
dim.y.value = (int) mouseY - offsetY;
|
||||
}
|
||||
|
||||
keepOnScreen(dim);
|
||||
}
|
||||
|
||||
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ui() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, int mouseX, int mouseY, float delta, long elapsed) {
|
||||
HudMod hovered = draggingMod != null ? draggingMod : getModAt(mouseX, mouseY);
|
||||
|
||||
for (HudMod mod : hudMods) {
|
||||
ModLayout dim = mod.getDimensions();
|
||||
MatrixStackRef matrices = renderScope.getMatrixStack();
|
||||
matrices.push();
|
||||
boolean active = mod == hovered;
|
||||
|
||||
matrices.translate(dim.x.value, dim.y.value);
|
||||
matrices.scale(dim.scale.value, dim.scale.value);
|
||||
renderScope.getMatrixStack().push();
|
||||
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
|
||||
mod.renderDummy(renderScope);
|
||||
drawFrame(renderScope, mod, dim, active);
|
||||
}
|
||||
|
||||
matrices.pop();
|
||||
drawHint(renderScope);
|
||||
}
|
||||
|
||||
// Draw border around mod
|
||||
int borderX = dim.x.value;
|
||||
int borderY = dim.y.value;
|
||||
int borderW = (int) (dim.width * dim.scale.value);
|
||||
int borderH = (int) (dim.height * dim.scale.value);
|
||||
/**
|
||||
* The outline around a HUD, its resize handle, and its name while it's
|
||||
* hovered or dragged.
|
||||
*/
|
||||
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) {
|
||||
return mouseX >= dim.x.value && mouseX <= dim.x.value + (dim.width * dim.scale.value) &&
|
||||
mouseY >= dim.y.value && mouseY <= dim.y.value + (dim.height * dim.scale.value);
|
||||
private void drawHint(RenderScope renderScope) {
|
||||
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 = (width - 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;
|
||||
float bottomY = dim.y.value + dim.height * dim.scale.value;
|
||||
/**
|
||||
* The HUD under the mouse, preferring the one drawn on top, or null.
|
||||
*/
|
||||
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 w = right(dim) - dim.x.value;
|
||||
int h = bottom(dim) - dim.y.value;
|
||||
|
||||
dim.x.value = Math.max(0, Math.min(dim.x.value, width - w));
|
||||
dim.y.value = Math.max(0, Math.min(dim.y.value, height - 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
|
||||
|
||||
@@ -1,56 +1,147 @@
|
||||
package org.saturnclient.ui.screens;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.saturnclient.config.AnimationConfig;
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.mod.Mod;
|
||||
import org.saturnclient.mod.ModManager;
|
||||
import org.saturnclient.ui.SaturnScreen;
|
||||
import org.saturnclient.ui.PanelScreen;
|
||||
import org.saturnclient.ui.anim.SlideFade;
|
||||
import org.saturnclient.ui.components.SaturnModule;
|
||||
import org.saturnclient.ui.components.Sidebar;
|
||||
import org.saturnclient.ui.components.inputs.SearchInput;
|
||||
import org.saturnclient.ui.elements.AnimationStagger;
|
||||
import org.saturnclient.ui.elements.ImageTexture;
|
||||
import org.saturnclient.ui.elements.ChipGroup;
|
||||
import org.saturnclient.ui.elements.Label;
|
||||
import org.saturnclient.ui.elements.Scroll;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
|
||||
public class ModMenu extends PanelScreen {
|
||||
private static final String ALL = "All";
|
||||
|
||||
private static final int COLUMNS = 3;
|
||||
private static final int CARD_GAP = 10;
|
||||
|
||||
private String category = ALL;
|
||||
private String query = "";
|
||||
|
||||
private Scroll scroll;
|
||||
private int gridWidth;
|
||||
|
||||
public class ModMenu extends SaturnScreen {
|
||||
public ModMenu() {
|
||||
super("Mod Menu");
|
||||
super("Mod Menu", Sidebar.MODS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ui() {
|
||||
int p = 10;
|
||||
int g = 10;
|
||||
protected void panel() {
|
||||
// The search box starts empty after a resize, so the query does too.
|
||||
query = "";
|
||||
gridWidth = scrollContentWidth(contentWidth);
|
||||
|
||||
Scroll scroll = new Scroll(p);
|
||||
// Header
|
||||
draw(new Label(() -> "Mods", 1.15f, () -> Theme.FONT.value, () -> Theme.FOREGROUND.value)
|
||||
.position(contentX, contentY + 2));
|
||||
|
||||
int row = 0;
|
||||
int col = 0;
|
||||
draw(new Label(this::getEnabledSummary, 0.66f, Theme::getBodyFont, Theme::getMutedFg)
|
||||
.position(contentX, contentY + 28));
|
||||
|
||||
AnimationStagger moduleStagger = new AnimationStagger(AnimationConfig.modMenu);
|
||||
SearchInput search = new SearchInput("Search mods", text -> {
|
||||
query = text;
|
||||
refresh(false);
|
||||
});
|
||||
draw(search.position(contentX + contentWidth - search.width, contentY + 2));
|
||||
|
||||
// Categories
|
||||
int chipsY = contentY + 56;
|
||||
ChipGroup categories = new ChipGroup(getCategories(), category, selected -> {
|
||||
category = selected;
|
||||
refresh(false);
|
||||
});
|
||||
draw(categories.position(contentX, chipsY));
|
||||
|
||||
// Mods
|
||||
int scrollY = chipsY + categories.height + 14;
|
||||
scroll = new Scroll(0).noBackground();
|
||||
scroll.dimensions(scrollWidth(contentWidth), panelY + panelHeight - 12 - scrollY);
|
||||
refresh(true);
|
||||
draw(scroll.position(contentX, scrollY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the grid with the mods that match the category and search query.
|
||||
*/
|
||||
private void refresh(boolean animate) {
|
||||
scroll.clear();
|
||||
|
||||
List<Mod> mods = ModManager.MODS.stream().filter(this::matches).toList();
|
||||
|
||||
if (mods.isEmpty()) {
|
||||
String message = "No mods match “" + query + "”";
|
||||
float scale = 0.7f;
|
||||
int x = (gridWidth - (int) (Fonts.getWidth(message, Theme.getBodyFont()) * scale)) / 2;
|
||||
|
||||
scroll.draw(new Label(() -> message, scale, Theme::getBodyFont, Theme::getMutedFg).position(x, 60));
|
||||
return;
|
||||
}
|
||||
|
||||
int cardWidth = (gridWidth - CARD_GAP * (COLUMNS - 1)) / COLUMNS;
|
||||
AnimationStagger grid = new AnimationStagger(AnimationConfig.modMenu);
|
||||
|
||||
for (int i = 0; i < mods.size(); i++) {
|
||||
int col = i % COLUMNS;
|
||||
int row = i / COLUMNS;
|
||||
|
||||
SaturnModule card = new SaturnModule(mods.get(i), cardWidth);
|
||||
card.position((cardWidth + CARD_GAP) * col, (SaturnModule.HEIGHT + CARD_GAP) * row);
|
||||
|
||||
if (animate) {
|
||||
card.animation(new SlideFade(AnimationConfig.modMenu, -12));
|
||||
}
|
||||
|
||||
grid.draw(card);
|
||||
}
|
||||
|
||||
scroll.draw(grid);
|
||||
}
|
||||
|
||||
private boolean matches(Mod mod) {
|
||||
if (!category.equals(ALL) && !List.of(mod.getTags()).contains(category)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String q = query.trim().toLowerCase(Locale.ROOT);
|
||||
if (q.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String description = mod.getDescription() == null ? "" : mod.getDescription();
|
||||
return mod.getName().toLowerCase(Locale.ROOT).contains(q)
|
||||
|| description.toLowerCase(Locale.ROOT).contains(q);
|
||||
}
|
||||
|
||||
/**
|
||||
* "All", then every mod tag in the order it first appears.
|
||||
*/
|
||||
private static List<String> getCategories() {
|
||||
List<String> categories = new ArrayList<>();
|
||||
categories.add(ALL);
|
||||
|
||||
for (Mod mod : ModManager.MODS) {
|
||||
moduleStagger.draw(
|
||||
new SaturnModule(mod)
|
||||
.position((160 + g) * col, (50 + g) * row)
|
||||
.animation(new SlideFade(AnimationConfig.modMenu, -12)));
|
||||
|
||||
if (col == 2) {
|
||||
col = 0;
|
||||
row++;
|
||||
} else {
|
||||
col++;
|
||||
for (String tag : mod.getTags()) {
|
||||
if (!categories.contains(tag)) {
|
||||
categories.add(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scroll.draw(moduleStagger);
|
||||
return categories;
|
||||
}
|
||||
|
||||
int scrollWidth = 480 + 10 + (g * 2) + (p * 2);
|
||||
|
||||
draw(scroll.dimensions(scrollWidth, 350).center(width, height));
|
||||
|
||||
draw(new Sidebar(0, this.provider::close).center(width, height, -(scrollWidth / 2 + 20), 0));
|
||||
|
||||
draw(new ImageTexture(Textures.LOGO_TEXT_BIG).dimensions(180, 18).position(width - 180 - 20, height - 18 - 20));
|
||||
private String getEnabledSummary() {
|
||||
long enabled = ModManager.MODS.stream().filter(Mod::isEnabled).count();
|
||||
return enabled + " of " + ModManager.MODS.size() + " enabled";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package org.saturnclient.ui.screens;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.config.property.Property;
|
||||
import org.saturnclient.mod.HudMod;
|
||||
import org.saturnclient.mod.Mod;
|
||||
import org.saturnclient.ui.PanelScreen;
|
||||
import org.saturnclient.ui.components.HudPreviewCard;
|
||||
import org.saturnclient.ui.components.ModHeader;
|
||||
import org.saturnclient.ui.components.PropertyList;
|
||||
import org.saturnclient.ui.components.Sidebar;
|
||||
import org.saturnclient.ui.elements.ActionButton;
|
||||
import org.saturnclient.ui.elements.Divider;
|
||||
import org.saturnclient.ui.elements.Scroll;
|
||||
import org.saturnclient.ui.elements.SwitchButton;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
|
||||
/**
|
||||
* One mod's settings: a header with the mod's details and its on/off switch,
|
||||
* the rest of its properties below, and a live preview for HUD mods.
|
||||
*/
|
||||
public class ModSettings extends PanelScreen {
|
||||
/**
|
||||
* Every mod's first property. It's shown as the header's switch, so the list
|
||||
* leaves it out.
|
||||
*/
|
||||
private static final String ENABLED = "Enabled";
|
||||
|
||||
private static final int HEADER_Y = 40;
|
||||
private static final int PREVIEW_WIDTH = 280;
|
||||
private static final int PREVIEW_MAX_HEIGHT = 220;
|
||||
private static final int COLUMN_GAP = 22;
|
||||
|
||||
private final Mod mod;
|
||||
|
||||
public ModSettings(Mod mod) {
|
||||
super(mod.getName() + " Settings", Sidebar.MODS);
|
||||
this.mod = mod;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void panel() {
|
||||
draw(new ActionButton(Textures.ICON_CHEVRON_LEFT, "Mods", false,
|
||||
() -> Providers.saturn.getClient().setScreen(new ModMenu()))
|
||||
.position(contentX - 12, contentY - 4));
|
||||
|
||||
// Header: the mod's details, then Reset and the on/off switch on the right
|
||||
int headerY = contentY + HEADER_Y;
|
||||
|
||||
SwitchButton enabled = new SwitchButton("Enabled", "Disabled", mod::isEnabled,
|
||||
() -> mod.setEnabled(!mod.isEnabled()));
|
||||
int buttonY = headerY + (ModHeader.HEIGHT - enabled.height) / 2;
|
||||
draw(enabled.position(contentX + contentWidth - enabled.width, buttonY));
|
||||
|
||||
ActionButton reset = new ActionButton(Textures.ICON_RESET, "Reset", true, this::reset);
|
||||
draw(reset.position(enabled.x - 10 - reset.width, buttonY));
|
||||
|
||||
draw(new ModHeader(mod, reset.x - 16 - contentX).position(contentX, headerY));
|
||||
|
||||
int dividerY = headerY + ModHeader.HEIGHT + 16;
|
||||
draw(new Divider(contentWidth).position(contentX, dividerY));
|
||||
|
||||
// Body: the properties, with a preview beside them for HUD mods
|
||||
int bodyY = dividerY + 1;
|
||||
int bodyHeight = contentY + contentHeight - bodyY;
|
||||
int listWidth = contentWidth;
|
||||
|
||||
if (mod instanceof HudMod hudMod) {
|
||||
listWidth -= PREVIEW_WIDTH + COLUMN_GAP;
|
||||
draw(new HudPreviewCard(hudMod)
|
||||
.dimensions(PREVIEW_WIDTH, Math.min(PREVIEW_MAX_HEIGHT, bodyHeight - 16))
|
||||
.position(contentX + contentWidth - PREVIEW_WIDTH, bodyY + 16));
|
||||
}
|
||||
|
||||
Scroll scroll = new Scroll(0).noBackground();
|
||||
PropertyList.draw(scroll, mod.getConfig().getProperties(), Set.of(ENABLED), scrollContentWidth(listWidth),
|
||||
0);
|
||||
draw(scroll.dimensions(scrollWidth(listWidth), bodyHeight).position(contentX, bodyY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts every setting except the on/off switch back to its default, then
|
||||
* rebuilds the screen so the inputs show the new values.
|
||||
*/
|
||||
private void reset() {
|
||||
for (Map.Entry<String, Property> entry : mod.getConfig().getProperties().entrySet()) {
|
||||
if (!entry.getKey().equals(ENABLED)) {
|
||||
entry.getValue().reset();
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
}
|
||||
}
|
||||
@@ -3,24 +3,35 @@ package org.saturnclient.ui.screens;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.config.AnimationConfig;
|
||||
import org.saturnclient.config.Config;
|
||||
import org.saturnclient.config.property.FloatProperty;
|
||||
import org.saturnclient.ui.SaturnScreen;
|
||||
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;
|
||||
import org.saturnclient.ui.screens.cosmetics.CloakMenu;
|
||||
|
||||
/**
|
||||
* 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
|
||||
protected FloatProperty getScaleSetting() {
|
||||
return Config.hudMenuScale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ui() {
|
||||
// Logo animations (unchanged)
|
||||
draw(new ImageTexture(Textures.LOGO_TEXT)
|
||||
.dimensions(98, 10)
|
||||
.center(width, height, 0, -36)
|
||||
@@ -31,32 +42,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", () -> 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 CloakMenu());
|
||||
}).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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,77 +1,153 @@
|
||||
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.property.FloatProperty;
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.ui.Element;
|
||||
import org.saturnclient.ui.SaturnScreen;
|
||||
import org.saturnclient.ui.anim.Animation;
|
||||
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.IconButton;
|
||||
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.screens.cosmetics.CloakMenu;
|
||||
|
||||
/**
|
||||
* Saturn's title screen: the logo over the game's menus in the middle, the
|
||||
* player and a quit button in the top corners, and along the bottom a dock
|
||||
* (Saturn's settings, its mods and cosmetics, then Minecraft's options) and the version.
|
||||
*/
|
||||
public class TitleMenu extends SaturnScreen {
|
||||
// The logo and wordmark as the shift menu has them: the wordmark over the logo's foot
|
||||
private static final int LOGO = 98;
|
||||
private static final int WORDMARK_WIDTH = 98;
|
||||
private static final int WORDMARK_HEIGHT = 10;
|
||||
private static final int WORDMARK_Y = 88;
|
||||
/** From the logo's foot to the menus, close so they read as one block */
|
||||
private static final int MENUS_GAP = 14;
|
||||
private static final int COLUMN_WIDTH = 280;
|
||||
private static final int BUTTON_HEIGHT = 36;
|
||||
private static final int GAP = 8;
|
||||
/** Space around the account chip, quit button, dock and version, from the screen's edges */
|
||||
private static final int EDGE = 14;
|
||||
/** The top corners sit a little tighter */
|
||||
private static final int TOP_EDGE = 10;
|
||||
/** As tall as the account chip beside it */
|
||||
private static final int QUIT_SIZE = AccountChip.HEIGHT;
|
||||
|
||||
// The intro comes in in reading order, left to right and top to bottom, one step apart
|
||||
private static final int STEP_ACCOUNT = 0;
|
||||
private static final int STEP_QUIT = 1;
|
||||
private static final int STEP_LOGO = 2;
|
||||
private static final int STEP_MENUS = 3;
|
||||
/** After the two menus */
|
||||
private static final int STEP_VERSION = 5;
|
||||
private static final int STEP_DOCK = 6;
|
||||
private static final float VERSION_SCALE = 0.5f;
|
||||
private static final float VERSION_ALPHA = 0.6f;
|
||||
|
||||
public TitleMenu() {
|
||||
super("Title Menu");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FloatProperty getScaleSetting() {
|
||||
return Config.titleScale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ui() {
|
||||
backgroundBlur = 0;
|
||||
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)
|
||||
.animation(new SlideY(AnimationConfig.logo, -20)));
|
||||
// Logo and menus, centered together a little above the middle
|
||||
int menusHeight = BUTTON_HEIGHT * 2 + GAP;
|
||||
int blockHeight = LOGO + MENUS_GAP + menusHeight;
|
||||
int top = (height - blockHeight) / 2 - 20;
|
||||
|
||||
AnimationStagger mainButtonStagger = new AnimationStagger(AnimationConfig.mainMenu);
|
||||
draw(new ImageTexture(Config.getLogo())
|
||||
.dimensions(LOGO, LOGO)
|
||||
.position((width - LOGO) / 2, top)
|
||||
.animation(step(new SlideY(AnimationConfig.logo, -20), STEP_LOGO)));
|
||||
draw(new ImageTexture(Textures.LOGO_TEXT)
|
||||
.dimensions(WORDMARK_WIDTH, WORDMARK_HEIGHT)
|
||||
.position((width - WORDMARK_WIDTH) / 2, top + WORDMARK_Y)
|
||||
.animation(step(new Fade(AnimationConfig.logo.duration.value), STEP_LOGO)));
|
||||
|
||||
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)));
|
||||
AnimationStagger menus = new AnimationStagger(AnimationConfig.mainMenu)
|
||||
.after(stepDelay(STEP_MENUS));
|
||||
|
||||
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)));
|
||||
addMenuButton(menus, new Button("Singleplayer",
|
||||
() -> Providers.saturn.getClient().setScreen(MinecraftScreen.SelectWorld))
|
||||
.dimensions(COLUMN_WIDTH, BUTTON_HEIGHT).position(0, 0));
|
||||
addMenuButton(menus, new Button("Multiplayer",
|
||||
() -> Providers.saturn.getClient().setScreen(MinecraftScreen.Multiplayer))
|
||||
.dimensions(COLUMN_WIDTH, BUTTON_HEIGHT).position(0, BUTTON_HEIGHT + GAP));
|
||||
|
||||
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)));
|
||||
draw(menus.position((width - COLUMN_WIDTH) / 2, top + LOGO + MENUS_GAP));
|
||||
|
||||
mainButtonStagger.draw(new Button("QUIT", () -> {
|
||||
Providers.saturn.getClient().scheduleStop();
|
||||
}).scale(0.69f).dimensions(206, 52).position(146, 80)
|
||||
.animation(new SlideFade(AnimationConfig.mainMenu, -10)));
|
||||
// Top corners: the player, and quitting the game
|
||||
draw(new AccountChip()
|
||||
.position(TOP_EDGE, TOP_EDGE)
|
||||
.animation(step(new SlideFade(AnimationConfig.mainMenu, 8), STEP_ACCOUNT)));
|
||||
draw(new IconButton(Textures.ICON_CLOSE, () -> Providers.saturn.getClient().scheduleStop())
|
||||
.dimensions(QUIT_SIZE, QUIT_SIZE)
|
||||
.position(width - TOP_EDGE - QUIT_SIZE, TOP_EDGE)
|
||||
.animation(step(new SlideFade(AnimationConfig.mainMenu, 8), STEP_QUIT)));
|
||||
|
||||
draw(mainButtonStagger.dimensions(289, 143).center(width, height, 0, 50));
|
||||
// Along the bottom: the dock, Saturn first (its settings) and Minecraft's options last, and the version
|
||||
Dock dock = new Dock(List.of(
|
||||
new Dock.Item(Textures.LOGO, () -> Providers.saturn.getClient().setScreen(new ClientSettings())),
|
||||
new Dock.Item(Textures.ICON_MODS, () -> Providers.saturn.getClient().setScreen(new ModMenu())),
|
||||
new Dock.Item(Textures.ICON_COSMETICS,
|
||||
() -> Providers.saturn.getClient().setScreen(new CosmeticsMenu())),
|
||||
new Dock.Item(Textures.ICON_SETTINGS,
|
||||
() -> Providers.saturn.getClient().setScreen(MinecraftScreen.Options))));
|
||||
draw(dock.position((width - dock.width) / 2, height - EDGE - Dock.HEIGHT)
|
||||
.animation(step(new SlideFade(AnimationConfig.mainMenu, -12), STEP_DOCK)));
|
||||
|
||||
int s = Fonts.getHeight() + 20;
|
||||
String version = getVersionText();
|
||||
int versionHeight = (int) (Fonts.getHeight() * VERSION_SCALE);
|
||||
draw(new Label(() -> version, VERSION_SCALE, Theme::getBodyFont,
|
||||
() -> Theme.withAlpha(VERSION_ALPHA, Theme.getMutedFg()))
|
||||
.position(EDGE, height - EDGE - versionHeight)
|
||||
.animation(step(new Fade(AnimationConfig.mainMenu), STEP_VERSION)));
|
||||
}
|
||||
|
||||
AnimationStagger saturnStagger = new AnimationStagger(AnimationConfig.mainMenu.stagger.value);
|
||||
/**
|
||||
* When the intro's {@code step} starts, in ms: the Main Menu stagger apart,
|
||||
* or all at once when staggering is off.
|
||||
*/
|
||||
private static int stepDelay(int step) {
|
||||
return Config.stagger.value ? step * AnimationConfig.mainMenu.stagger.value : 0;
|
||||
}
|
||||
|
||||
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)));
|
||||
private static Animation step(Animation animation, int step) {
|
||||
animation.delay = stepDelay(step);
|
||||
return animation;
|
||||
}
|
||||
|
||||
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)));
|
||||
/**
|
||||
* "Saturn 0.1.0 · 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 " + version[0];
|
||||
return version.length > 1 ? text + " · " + version[1] : text;
|
||||
}
|
||||
|
||||
saturnStagger.draw(new TextureButton(Textures.CLOAK, () -> {
|
||||
Providers.saturn.getClient().setScreen(new CloakMenu());
|
||||
}).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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
package org.saturnclient.ui.screens.cosmetics;
|
||||
|
||||
import org.saturnclient.client.player.SaturnPlayer;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.config.AnimationConfig;
|
||||
import org.saturnclient.cosmetics.Cloaks;
|
||||
import org.saturnclient.ui.SaturnScreen;
|
||||
import org.saturnclient.ui.anim.Fade;
|
||||
import org.saturnclient.ui.anim.SlideY;
|
||||
import org.saturnclient.ui.components.CosmeticPreview;
|
||||
import org.saturnclient.ui.components.Sidebar;
|
||||
import org.saturnclient.ui.components.SkinPreview;
|
||||
import org.saturnclient.ui.elements.AnimationStagger;
|
||||
import org.saturnclient.ui.elements.Scroll;
|
||||
import org.saturnclient.ui.elements.TabMenu;
|
||||
import org.saturnclient.ui.elements.TabMenu.TabMenuComponent;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
|
||||
public class CloakMenu extends SaturnScreen {
|
||||
public CloakMenu() {
|
||||
super("Cloaks Menu");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ui() {
|
||||
int p = 10;
|
||||
int g = 10;
|
||||
|
||||
Scroll scroll = new Scroll(p);
|
||||
|
||||
int row = 0;
|
||||
int col = 0;
|
||||
|
||||
SaturnPlayer player = SaturnPlayer.get();
|
||||
|
||||
AnimationStagger stagger = new AnimationStagger(AnimationConfig.cosmeticsMenu);
|
||||
|
||||
if (player != null) {
|
||||
for (String cloak : Cloaks.availableCloaks) {
|
||||
|
||||
stagger.draw(
|
||||
new CosmeticPreview(
|
||||
cloak == player.cloak,
|
||||
Textures.getCloakPreview(cloak),
|
||||
() -> Cloaks.setCloak(cloak))
|
||||
.position((50 + g) * col, (111 + g) * row)
|
||||
.animation(new SlideY(AnimationConfig.cosmeticsMenu, 14)));
|
||||
|
||||
if (col == 5) {
|
||||
col = 0;
|
||||
row++;
|
||||
} else {
|
||||
col++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scroll.draw(stagger);
|
||||
|
||||
int scrollWidth = 480 + 10 + (g * 2) + (p * 2);
|
||||
|
||||
draw(scroll
|
||||
.dimensions(scrollWidth, 350)
|
||||
.center(width, height));
|
||||
|
||||
draw(new SkinPreview(-30.0f, false)
|
||||
.scale(3.5f)
|
||||
.position(scroll.x + (scrollWidth - 220), scroll.y + 40)
|
||||
.animation(new Fade(500)));
|
||||
|
||||
draw(new Sidebar(2, this.provider::close)
|
||||
.center(width, height, -(scrollWidth / 2 + 20), 0));
|
||||
|
||||
draw(new TabMenu(0,
|
||||
new TabMenuComponent(Textures.CLOAK, () -> {
|
||||
}),
|
||||
new TabMenuComponent(Textures.HAT, () -> {
|
||||
Providers.saturn.getClient().setScreen(new HatMenu());
|
||||
}))
|
||||
.center(width, height, 0, -195));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user