Compare commits
10
Commits
df09271e81
...
c3667f8f66
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3667f8f66 | ||
|
|
85c7057ba1 | ||
|
|
55b78989ce | ||
|
|
8b58fe919d | ||
|
|
6bfdfc40c9 | ||
|
|
351507d60d | ||
|
|
33c09b6922 | ||
|
|
06a90d8e51 | ||
|
|
7264c7b38b | ||
|
|
a413648be9 |
@@ -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,7 @@ 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.IntProperty;
|
||||
import org.saturnclient.config.property.KeybindingProperty;
|
||||
import org.saturnclient.config.property.Property;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
@@ -17,6 +18,7 @@ public class Config {
|
||||
public static BoolProperty saturnTitleScreen = Property.bool(true);
|
||||
public static BoolProperty cloakPhysics = Property.bool(true);
|
||||
public static BoolProperty stagger = Property.bool(true);
|
||||
public static IntProperty backgroundBlur = Property.integer(5).range(0, 10);
|
||||
public static KeybindingProperty openEmoteWheel = Property.keybinding(GLFWProvider.GLFW_KEY_B);
|
||||
|
||||
public static IdentifierRef getLogo() {
|
||||
@@ -27,11 +29,12 @@ 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("Background blur", backgroundBlur, "How much the game behind Saturn's screens is blurred");
|
||||
|
||||
// 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
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
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 {
|
||||
public static ConfigManager config;
|
||||
|
||||
// Main UI background (slightly warm dark to match Saturn tone)
|
||||
public static final ColorProperty BACKGROUND = Property.color(0xFF12100D);
|
||||
|
||||
@@ -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", 0xFF12100D, 0xFFF2E6D2, 0xFF1C1915, 0xFFEAD9B8, 0xFFE2BF7D, 0xFF1A140A, 0xFFE2BF7D),
|
||||
new Preset("Midnight", 0xFF0E1117, 0xFFE4EAF5, 0xFF171C26, 0xFFC9D4E8, 0xFF8FB3FF, 0xFF0A1224, 0xFF8FB3FF),
|
||||
new Preset("Amethyst", 0xFF120F17, 0xFFEDE4F7, 0xFF1C1724, 0xFFD8C9EB, 0xFFC79BFF, 0xFF1A0F2A, 0xFFC79BFF),
|
||||
new Preset("Emerald", 0xFF0D1210, 0xFFE2F2EA, 0xFF16201B, 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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,36 @@ 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;
|
||||
|
||||
@@ -7,11 +7,40 @@ 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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -13,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;
|
||||
|
||||
@@ -26,17 +26,22 @@ public class ModLayout {
|
||||
public NamedProperty prop() {
|
||||
// A LinkedHashMap, unlike Map.of, keeps the order the settings are shown in.
|
||||
Map<String, Property> props = new LinkedHashMap<>();
|
||||
props.put("X", x);
|
||||
props.put("Y", y);
|
||||
props.put("Scale", scale);
|
||||
props.put("Background Color", bgColor);
|
||||
props.put("Foreground Color", fgColor);
|
||||
props.put("Corner Radius", radius);
|
||||
props.put("Font", font);
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
@@ -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,6 +15,14 @@ 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);
|
||||
|
||||
@@ -22,7 +30,7 @@ public class ElementRenderer {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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;
|
||||
|
||||
/**
|
||||
@@ -18,6 +19,9 @@ public abstract class PanelScreen extends SaturnScreen {
|
||||
|
||||
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;
|
||||
@@ -61,4 +65,18 @@ public abstract class PanelScreen extends SaturnScreen {
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
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;
|
||||
|
||||
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();
|
||||
|
||||
@@ -22,7 +28,8 @@ public abstract class SaturnScreen {
|
||||
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 +38,24 @@ 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));
|
||||
}
|
||||
|
||||
public void init() {
|
||||
elements.clear();
|
||||
width = provider.getWidth();
|
||||
height = provider.getHeight();
|
||||
Popup.reset(width, height);
|
||||
ui(); // abstraction to render the saturn ui and also render extra stuff here
|
||||
}
|
||||
|
||||
@@ -54,6 +75,7 @@ public abstract class SaturnScreen {
|
||||
renderScope.getMatrixStack().scale(0.5f, 0.5f);
|
||||
|
||||
ElementRenderer.render(new ArrayList<>(elements), elapsed, renderScope, mouseX, mouseY);
|
||||
Popup.render(renderScope, elapsed, mouseX, mouseY);
|
||||
|
||||
renderScope.getMatrixStack().pop();
|
||||
}
|
||||
@@ -66,7 +88,9 @@ public abstract class SaturnScreen {
|
||||
mouseX *= 2;
|
||||
mouseY *= 2;
|
||||
|
||||
ElementRenderer.mouseClicked(elements, mouseX, mouseY, button);
|
||||
if (!Popup.mouseClicked(mouseX, mouseY, button)) {
|
||||
ElementRenderer.mouseClicked(elements, mouseX, mouseY, button);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -75,7 +99,9 @@ public abstract class SaturnScreen {
|
||||
mouseX *= 2;
|
||||
mouseY *= 2;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -84,6 +110,8 @@ public abstract class SaturnScreen {
|
||||
mouseX *= 2;
|
||||
mouseY *= 2;
|
||||
|
||||
// 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;
|
||||
@@ -97,7 +125,9 @@ public abstract class SaturnScreen {
|
||||
mouseX *= 2;
|
||||
mouseY *= 2;
|
||||
|
||||
ElementRenderer.mouseScrolled(elements, mouseX, mouseY, horizontalAmount, verticalAmount);
|
||||
if (!Popup.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)) {
|
||||
ElementRenderer.mouseScrolled(elements, mouseX, mouseY, horizontalAmount, verticalAmount);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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.withAlpha(0.72f, 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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.saturnclient.ui.components;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -23,6 +21,7 @@ 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;
|
||||
@@ -48,15 +47,6 @@ public final class PropertyList {
|
||||
* @return the y below the last row
|
||||
*/
|
||||
public static int draw(Scroll scroll, Map<String, Property> properties, Set<String> skip, int width, int y) {
|
||||
// Controls go on after every row, so an open color picker covers the rows below it.
|
||||
List<Element> controls = new ArrayList<>();
|
||||
y = drawRows(scroll, properties, skip, width, y, controls);
|
||||
controls.forEach(scroll::draw);
|
||||
return y;
|
||||
}
|
||||
|
||||
private static int drawRows(Scroll scroll, Map<String, Property> properties, Set<String> skip, int width, int y,
|
||||
List<Element> controls) {
|
||||
for (Map.Entry<String, Property> entry : properties.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
Property prop = entry.getValue();
|
||||
@@ -66,12 +56,8 @@ public final class PropertyList {
|
||||
}
|
||||
|
||||
if (prop instanceof NamespaceProperty namespace) {
|
||||
y += SECTION_GAP;
|
||||
String title = name.toUpperCase(Locale.ROOT);
|
||||
scroll.draw(new Label(() -> title, SECTION_SCALE, () -> Theme.FONT.value, Theme::getMutedFg)
|
||||
.position(0, y));
|
||||
y += (int) (Fonts.getHeight() * SECTION_SCALE) + 4;
|
||||
y = drawRows(scroll, namespace.value, Set.of(), width, y, controls);
|
||||
y = drawSection(scroll, name, y + SECTION_GAP) + 4;
|
||||
y = draw(scroll, namespace.value, Set.of(), width, y);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -80,14 +66,25 @@ public final class PropertyList {
|
||||
continue;
|
||||
}
|
||||
|
||||
scroll.draw(new Row(name, width, control.width).position(0, y));
|
||||
controls.add(control.position(width - control.width, y + (ROW_HEIGHT - control.height) / 2));
|
||||
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).
|
||||
*/
|
||||
@@ -97,9 +94,9 @@ public final class PropertyList {
|
||||
} else if (prop instanceof ColorProperty p) {
|
||||
return new ColorInput(p);
|
||||
} else if (prop instanceof IntProperty p) {
|
||||
return new IntInput(p);
|
||||
return p.hasRange() ? Slider.of(p) : new IntInput(p);
|
||||
} else if (prop instanceof FloatProperty p) {
|
||||
return new FloatInput(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) {
|
||||
@@ -110,17 +107,22 @@ public final class PropertyList {
|
||||
}
|
||||
|
||||
/**
|
||||
* A row's name and the line under it. The control is a separate element on
|
||||
* top, so it gets the clicks and key presses.
|
||||
* 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, int width, 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;
|
||||
@@ -128,10 +130,24 @@ public final class PropertyList {
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
int nameY = (height - (int) (Fonts.getHeight() * NAME_SCALE)) / 2;
|
||||
String text = Fonts.ellipsize(name, Theme.FONT.value, NAME_SCALE, width - controlWidth - 16);
|
||||
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.drawText(NAME_SCALE, text, 0, nameY, Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
renderScope.drawRectangle(0, height - 1, width, 1, Theme.withAlpha(0.06f, Theme.FOREGROUND.value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,17 +2,16 @@ 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.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.CosmeticsMenu;
|
||||
|
||||
/**
|
||||
* The navigation rail beside Saturn's screens: the logo, one button per screen,
|
||||
@@ -38,10 +37,9 @@ public class Sidebar extends Element {
|
||||
|
||||
private final Item[] items = {
|
||||
new Item(Textures.ICON_MODS, () -> Providers.saturn.getClient().setScreen(new ModMenu())),
|
||||
new Item(Textures.ICON_COSMETICS, () -> Providers.saturn.getClient().setScreen(new CloakMenu())),
|
||||
new Item(Textures.ICON_COSMETICS, () -> Providers.saturn.getClient().setScreen(new CosmeticsMenu())),
|
||||
new Item(Textures.ICON_HUD_EDITOR, () -> Providers.saturn.getClient().setScreen(new HudEditor())),
|
||||
new Item(Textures.ICON_SETTINGS,
|
||||
() -> Providers.saturn.getClient().setScreen(new ConfigEditor(Config.config))),
|
||||
new Item(Textures.ICON_SETTINGS, () -> Providers.saturn.getClient().setScreen(new ClientSettings())),
|
||||
};
|
||||
|
||||
private final int active;
|
||||
|
||||
@@ -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.withAlpha(0.72f, 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,206 +1,53 @@
|
||||
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;
|
||||
|
||||
// The picker opens to the left, since the input sits at the right of its row.
|
||||
private static final int PICKER_X = -PICKER_WIDTH - 12;
|
||||
private static final int SWATCH = 20;
|
||||
|
||||
// Bounds of the picker's backdrop, relative to the color square
|
||||
private static final int BACKDROP_PADDING = 8;
|
||||
private static final int BACKDROP_TOP = -SLIDER_HEIGHT - 10 - BACKDROP_PADDING;
|
||||
private static final int BACKDROP_BOTTOM = PICKER_HEIGHT + 10 + HUE_SLIDER_HEIGHT + BACKDROP_PADDING;
|
||||
/** 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 = 116;
|
||||
this.height = Field.HEIGHT;
|
||||
syncFromColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
open = !open;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseClicked(double mouseX, double mouseY, int button) {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean onInput = mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height;
|
||||
if (!onInput && !isOverPicker(mouseX - PICKER_X, mouseY)) {
|
||||
open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
handlePickerClick(mouseX - PICKER_X, mouseY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
|
||||
if (open) {
|
||||
handlePickerDrag(mouseX - PICKER_X, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(double mouseX, double mouseY, int button) {
|
||||
draggingOpacity = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
Field.drawBox(renderScope, width, height, ctx.isHovering(), open);
|
||||
originX = ctx.originX;
|
||||
originY = ctx.originY;
|
||||
|
||||
int swatchY = (height - SWATCH) / 2;
|
||||
renderScope.drawRoundedRectangle(swatchY, swatchY, SWATCH, SWATCH, 5, 1, prop.value,
|
||||
Field.drawBox(renderScope, width, height, ctx.isHovering(), Popup.isOpenFor(this));
|
||||
|
||||
int swatchInset = (height - SWATCH) / 2;
|
||||
renderScope.drawRoundedRectangle(swatchInset, swatchInset, SWATCH, SWATCH, 5, 1, prop.value,
|
||||
Theme.withAlpha(0.3f, Theme.FOREGROUND.value));
|
||||
|
||||
renderScope.drawText(Field.TEXT_SCALE, toHex(prop.value), swatchY + SWATCH + 8,
|
||||
renderScope.drawText(Field.TEXT_SCALE, toHex(prop.value), swatchInset + SWATCH + 8,
|
||||
Field.textY(height, Field.TEXT_SCALE), Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
|
||||
if (!open)
|
||||
return;
|
||||
|
||||
int pickerX = PICKER_X;
|
||||
int pickerY = 0;
|
||||
|
||||
// Backdrop behind the picker and its two sliders
|
||||
renderScope.drawRoundedRectangle(pickerX - BACKDROP_PADDING, BACKDROP_TOP,
|
||||
PICKER_WIDTH + BACKDROP_PADDING * 2, BACKDROP_BOTTOM - BACKDROP_TOP,
|
||||
Theme.WIDGET_RADIUS.value, 1, Theme.withAlpha(0.95f, Theme.BACKGROUND.value), Theme.getBorder());
|
||||
|
||||
renderColorSquare(renderScope, pickerX, pickerY);
|
||||
renderOpacitySlider(renderScope, pickerX, pickerY - SLIDER_HEIGHT - 10);
|
||||
renderHueSlider(renderScope, pickerX, pickerY + PICKER_HEIGHT + 10);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 static boolean isOverPicker(double x, double y) {
|
||||
return x >= -BACKDROP_PADDING && x <= PICKER_WIDTH + BACKDROP_PADDING
|
||||
&& y >= BACKDROP_TOP && y <= BACKDROP_BOTTOM;
|
||||
}
|
||||
|
||||
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();
|
||||
@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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,16 +59,4 @@ public class ColorInput extends Element {
|
||||
? String.format("#%06X", color & 0xFFFFFF)
|
||||
: String.format("#%08X", color);
|
||||
}
|
||||
|
||||
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,164 @@
|
||||
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.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;
|
||||
|
||||
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) {
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
|
||||
Theme.withAlpha(0.97f, Theme.BACKGROUND.value), Theme.withAlpha(0.14f, Theme.FOREGROUND.value));
|
||||
|
||||
renderOpacity(renderScope);
|
||||
renderSquare(renderScope);
|
||||
renderHue(renderScope);
|
||||
}
|
||||
|
||||
private void renderOpacity(RenderScope renderScope) {
|
||||
int rgb = prop.value & 0xFFFFFF;
|
||||
for (int x = 0; x < SIZE; x++) {
|
||||
int alpha = (int) (x / (float) SIZE * 255) & 0xFF;
|
||||
renderScope.fill(PADDING + x, OPACITY_Y, PADDING + x + 1, OPACITY_Y + SLIDER_HEIGHT, (alpha << 24) | rgb);
|
||||
}
|
||||
|
||||
drawMarker(renderScope, PADDING + (int) (opacity * SIZE), OPACITY_Y);
|
||||
}
|
||||
|
||||
private void renderSquare(RenderScope renderScope) {
|
||||
for (int x = 0; x < SIZE; x++) {
|
||||
for (int y = 0; y < SIZE; y++) {
|
||||
int color = Color.HSBtoRGB(hue, x / (float) SIZE, 1f - y / (float) SIZE);
|
||||
renderScope.fill(PADDING + x, SQUARE_Y + y, PADDING + x + 1, SQUARE_Y + y + 1, 0xFF000000 | color);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
for (int x = 0; x < SIZE; x++) {
|
||||
int color = Color.HSBtoRGB(x / (float) SIZE, 1f, 1f);
|
||||
renderScope.fill(PADDING + x, HUE_Y, PADDING + x + 1, HUE_Y + SLIDER_HEIGHT, 0xFF000000 | color);
|
||||
}
|
||||
|
||||
drawMarker(renderScope, PADDING + (int) (hue * SIZE), HUE_Y);
|
||||
}
|
||||
|
||||
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.withAlpha(0.97f, Theme.BACKGROUND.value), 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,5 +1,8 @@
|
||||
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.elements.Field;
|
||||
@@ -11,7 +14,16 @@ public class FloatInput extends Input {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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.withAlpha(0.97f, Theme.BACKGROUND.value), 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)));
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,30 @@
|
||||
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.elements.Field;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
|
||||
/**
|
||||
* Steps through a {@link SelectProperty}'s values with arrows on either side.
|
||||
* Clicking the left third goes back; anywhere else goes forward.
|
||||
* A dropdown for a {@link SelectProperty}: the current value, opening a list
|
||||
* of every value when clicked.
|
||||
*/
|
||||
public class Select extends Element {
|
||||
private static final int TEXT_X = 12;
|
||||
private static final int ARROW = 16;
|
||||
private static final int ARROW_INSET = 8;
|
||||
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 = 200;
|
||||
@@ -27,28 +33,34 @@ public class Select extends Element {
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
Field.drawBox(renderScope, width, height, ctx.isHovering(), false);
|
||||
lastContext = ctx;
|
||||
boolean open = Popup.isOpenFor(this);
|
||||
|
||||
boolean prevHovered = ctx.isHovering(0, 0, width / 3, height);
|
||||
boolean nextHovered = ctx.isHovering() && !prevHovered;
|
||||
int arrowY = (height - ARROW) / 2;
|
||||
Field.drawBox(renderScope, width, height, ctx.isHovering(), open);
|
||||
|
||||
renderScope.drawTexture(Textures.ICON_CHEVRON_LEFT, ARROW_INSET, arrowY, 0, 0, ARROW, ARROW,
|
||||
prevHovered ? Theme.ACCENT.value : Theme.getMutedFg());
|
||||
renderScope.drawTexture(Textures.ICON_CHEVRON_RIGHT, width - ARROW_INSET - ARROW, arrowY, 0, 0, ARROW, ARROW,
|
||||
nextHovered ? Theme.ACCENT.value : Theme.getMutedFg());
|
||||
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);
|
||||
|
||||
int textSpace = width - (ARROW_INSET + ARROW + 4) * 2;
|
||||
String text = Fonts.ellipsize(prop.getSelection(), Theme.FONT.value, Field.TEXT_SCALE, textSpace);
|
||||
Field.drawCenteredText(renderScope, text, width, height, 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 (mouseX < width / 3) {
|
||||
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 {
|
||||
prop.next();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,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
|
||||
|
||||
@@ -6,7 +6,8 @@ import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
|
||||
/**
|
||||
* A faint horizontal line across the element's width.
|
||||
* 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 {
|
||||
|
||||
@@ -17,6 +18,6 @@ public class Divider extends Element {
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawRectangle(0, 0, width, 1, Theme.getBorder());
|
||||
renderScope.drawRectangle(0, 0, width, height, Theme.getBorder());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,24 +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;
|
||||
@@ -50,7 +66,8 @@ public class Scroll extends Element {
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
calculateMaxScroll();
|
||||
updateMaxScroll();
|
||||
int offset = getOffset();
|
||||
|
||||
if (background) {
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height,
|
||||
@@ -59,75 +76,28 @@ public class Scroll extends Element {
|
||||
|
||||
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
|
||||
@@ -146,17 +116,80 @@ public class Scroll extends Element {
|
||||
return;
|
||||
}
|
||||
|
||||
ElementRenderer.mouseClicked(children, mouseX - padding, mouseY - padding + scroll, button);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,11 +33,17 @@ public class Textures {
|
||||
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_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");
|
||||
|
||||
@@ -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,37 +0,0 @@
|
||||
package org.saturnclient.ui.screens;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.saturnclient.config.ConfigManager;
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.ui.PanelScreen;
|
||||
import org.saturnclient.ui.components.PropertyList;
|
||||
import org.saturnclient.ui.components.Sidebar;
|
||||
import org.saturnclient.ui.elements.Label;
|
||||
import org.saturnclient.ui.elements.Scroll;
|
||||
|
||||
/**
|
||||
* Edits every property of a config, such as the client's own settings.
|
||||
*/
|
||||
public class ConfigEditor extends PanelScreen {
|
||||
private static final int SCROLLBAR_SPACE = 14;
|
||||
private static final int LIST_Y = 44;
|
||||
|
||||
private final ConfigManager config;
|
||||
|
||||
public ConfigEditor(ConfigManager config) {
|
||||
super("Config Editor", Sidebar.SETTINGS);
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void panel() {
|
||||
draw(new Label(() -> "Settings", 1.15f, () -> Theme.FONT.value, () -> Theme.FOREGROUND.value)
|
||||
.position(contentX, contentY + 2));
|
||||
|
||||
Scroll scroll = new Scroll(0).noBackground();
|
||||
PropertyList.draw(scroll, config.getProperties(), Set.of(), contentWidth, 0);
|
||||
draw(scroll.dimensions(contentWidth + SCROLLBAR_SPACE, contentHeight - LIST_Y)
|
||||
.position(contentX, contentY + LIST_Y));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
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.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);
|
||||
if (name != null) {
|
||||
return name;
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (String word : id.split("_")) {
|
||||
if (!builder.isEmpty()) {
|
||||
builder.append(' ');
|
||||
}
|
||||
builder.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ public class ModMenu extends PanelScreen {
|
||||
|
||||
private static final int COLUMNS = 3;
|
||||
private static final int CARD_GAP = 10;
|
||||
private static final int SCROLLBAR_SPACE = 14;
|
||||
|
||||
private String category = ALL;
|
||||
private String query = "";
|
||||
@@ -40,7 +39,7 @@ public class ModMenu extends PanelScreen {
|
||||
protected void panel() {
|
||||
// The search box starts empty after a resize, so the query does too.
|
||||
query = "";
|
||||
gridWidth = contentWidth;
|
||||
gridWidth = scrollContentWidth(contentWidth);
|
||||
|
||||
// Header
|
||||
draw(new Label(() -> "Mods", 1.15f, () -> Theme.FONT.value, () -> Theme.FOREGROUND.value)
|
||||
@@ -66,7 +65,7 @@ public class ModMenu extends PanelScreen {
|
||||
// Mods
|
||||
int scrollY = chipsY + categories.height + 14;
|
||||
scroll = new Scroll(0).noBackground();
|
||||
scroll.dimensions(gridWidth + SCROLLBAR_SPACE, panelY + panelHeight - 12 - scrollY);
|
||||
scroll.dimensions(scrollWidth(contentWidth), panelY + panelHeight - 12 - scrollY);
|
||||
refresh(true);
|
||||
draw(scroll.position(contentX, scrollY));
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ public class ModSettings extends PanelScreen {
|
||||
private static final int PREVIEW_WIDTH = 280;
|
||||
private static final int PREVIEW_MAX_HEIGHT = 220;
|
||||
private static final int COLUMN_GAP = 22;
|
||||
private static final int SCROLLBAR_SPACE = 14;
|
||||
|
||||
private final Mod mod;
|
||||
|
||||
@@ -77,8 +76,9 @@ public class ModSettings extends PanelScreen {
|
||||
}
|
||||
|
||||
Scroll scroll = new Scroll(0).noBackground();
|
||||
PropertyList.draw(scroll, mod.getConfig().getProperties(), Set.of(ENABLED), listWidth, 0);
|
||||
draw(scroll.dimensions(listWidth + SCROLLBAR_SPACE, bodyHeight).position(contentX, bodyY));
|
||||
PropertyList.draw(scroll, mod.getConfig().getProperties(), Set.of(ENABLED), scrollContentWidth(listWidth),
|
||||
0);
|
||||
draw(scroll.dimensions(scrollWidth(listWidth), bodyHeight).position(contentX, bodyY));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,6 @@ import org.saturnclient.ui.elements.Button;
|
||||
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;
|
||||
|
||||
public class ShiftMenu extends SaturnScreen {
|
||||
public ShiftMenu() {
|
||||
@@ -51,7 +50,7 @@ public class ShiftMenu extends SaturnScreen {
|
||||
.animation(new Fade(AnimationConfig.shiftMenu)));
|
||||
|
||||
stagger.draw(new TextureButton(Textures.COSMETICS, () -> {
|
||||
Providers.saturn.getClient().setScreen(new CloakMenu());
|
||||
Providers.saturn.getClient().setScreen(new CosmeticsMenu());
|
||||
}).dimensions(btnHeight, btnHeight).position(btnWidth + btnHeight + (spacing * 2), 0)
|
||||
.animation(new Fade(AnimationConfig.shiftMenu)));
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import org.saturnclient.ui.elements.ImageTexture;
|
||||
import org.saturnclient.ui.elements.TextureButton;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
import org.saturnclient.ui.screens.cosmetics.CloakMenu;
|
||||
|
||||
public class TitleMenu extends SaturnScreen {
|
||||
public TitleMenu() {
|
||||
@@ -23,7 +22,7 @@ public class TitleMenu extends SaturnScreen {
|
||||
|
||||
@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)));
|
||||
@@ -68,7 +67,7 @@ public class TitleMenu extends SaturnScreen {
|
||||
.animation(new SlideFade(AnimationConfig.mainMenu, s + 8)));
|
||||
|
||||
saturnStagger.draw(new TextureButton(Textures.CLOAK, () -> {
|
||||
Providers.saturn.getClient().setScreen(new CloakMenu());
|
||||
Providers.saturn.getClient().setScreen(new CosmeticsMenu());
|
||||
}).dimensions(s, s).position((s + 2) * 2, 0)
|
||||
.animation(new SlideFade(AnimationConfig.mainMenu, s + 8)));
|
||||
|
||||
|
||||
@@ -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(Sidebar.COSMETICS, this.provider::close)
|
||||
.center(width, height, -(scrollWidth / 2 + 35), 0));
|
||||
|
||||
draw(new TabMenu(0,
|
||||
new TabMenuComponent(Textures.CLOAK, () -> {
|
||||
}),
|
||||
new TabMenuComponent(Textures.HAT, () -> {
|
||||
Providers.saturn.getClient().setScreen(new HatMenu());
|
||||
}))
|
||||
.center(width, height, 0, -195));
|
||||
}
|
||||
}
|
||||
@@ -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.Hats;
|
||||
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 HatMenu extends SaturnScreen {
|
||||
public HatMenu() {
|
||||
super("Hats 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 hat : Hats.availableHats) {
|
||||
stagger.draw(
|
||||
new CosmeticPreview(
|
||||
hat == player.hat,
|
||||
Textures.getHatPreview(hat),
|
||||
() -> Hats.setHat(hat))
|
||||
.dimensions(50, 50)
|
||||
.position((50 + g) * col, (50 + 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(170f, true)
|
||||
.scale(3.5f)
|
||||
.position(scroll.x + (scrollWidth - 220), scroll.y + 40)
|
||||
.animation(new Fade(500)));
|
||||
|
||||
draw(new Sidebar(Sidebar.COSMETICS, this.provider::close)
|
||||
.center(width, height, -(scrollWidth / 2 + 35), 0));
|
||||
|
||||
draw(new TabMenu(1,
|
||||
new TabMenuComponent(Textures.CLOAK, () -> {
|
||||
Providers.saturn.getClient().setScreen(new CloakMenu());
|
||||
}),
|
||||
new TabMenuComponent(Textures.HAT, () -> {
|
||||
}))
|
||||
.center(width, height, 0, -195));
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
package org.saturnclient.ui.screens.store;
|
||||
|
||||
// import org.saturnclient.cosmetics.cloak.Cloaks;
|
||||
import org.saturnclient.client.ServiceClient;
|
||||
import org.saturnclient.client.player.SaturnPlayer;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.config.AnimationConfig;
|
||||
import org.saturnclient.config.Theme;
|
||||
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.elements.AnimationStagger;
|
||||
import org.saturnclient.ui.elements.ImageTexture;
|
||||
import org.saturnclient.ui.elements.Scroll;
|
||||
import org.saturnclient.ui.elements.TabMenu;
|
||||
import org.saturnclient.ui.elements.Text;
|
||||
import org.saturnclient.ui.elements.TabMenu.TabMenuComponent;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
|
||||
// import com.mojang.blaze3d.systems.RenderSystem;
|
||||
|
||||
public class CloakStore extends SaturnScreen {
|
||||
private long lastPurchaseTime = 0;
|
||||
|
||||
public CloakStore() {
|
||||
super("Cloak Store");
|
||||
}
|
||||
|
||||
public CloakStore(long lastPurchaseTime) {
|
||||
super("Cloak Store");
|
||||
this.lastPurchaseTime = lastPurchaseTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ui() {
|
||||
int p = 10;
|
||||
int gx = 10;
|
||||
int gy = 20;
|
||||
|
||||
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.ALL_CLOAKS) {
|
||||
if (!Cloaks.availableCloaks.contains(cloak)) {
|
||||
|
||||
int x = (50 + gx) * col;
|
||||
int y = (111 + gy) * row;
|
||||
|
||||
stagger.draw(
|
||||
new CosmeticPreview(
|
||||
cloak == player.cloak,
|
||||
Textures.getCloakPreview(cloak),
|
||||
() -> handlePurchase(cloak))
|
||||
.position(x, y)
|
||||
.animation(new SlideY(AnimationConfig.cosmeticsMenu, 16)));
|
||||
|
||||
String t = "100";
|
||||
|
||||
stagger.draw(
|
||||
new Text(t)
|
||||
.position(x + Fonts.centerX(50, t, Theme.FONT.value), y + 113)
|
||||
.scale(0.5f)
|
||||
.animation(new Fade(400)));
|
||||
|
||||
stagger.draw(
|
||||
new ImageTexture(Textures.COINS)
|
||||
.dimensions(16, 16)
|
||||
.position(x + Fonts.getWidth(t, Theme.FONT.value) - 1, y + 112)
|
||||
.scale(0.5f)
|
||||
.animation(new Fade(400)));
|
||||
|
||||
if (col == 8) {
|
||||
col = 0;
|
||||
row++;
|
||||
} else {
|
||||
col++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scroll.draw(stagger);
|
||||
|
||||
int scrollWidth = 480 + 40 + (gx * 2) + (p * 2);
|
||||
|
||||
draw(scroll
|
||||
.dimensions(scrollWidth, 350)
|
||||
.center(width, height, 15, 0));
|
||||
|
||||
draw(new Sidebar(Sidebar.COSMETICS, this.provider::close)
|
||||
.center(width, height, -((scrollWidth - 30) / 2 + 35), 0));
|
||||
|
||||
draw(new TabMenu(0,
|
||||
new TabMenuComponent(Textures.CLOAK, () -> {
|
||||
}),
|
||||
new TabMenuComponent(Textures.HAT, () -> {
|
||||
Providers.saturn.getClient().setScreen(new HatStore());
|
||||
}))
|
||||
.center(width, height, 0, -195));
|
||||
}
|
||||
|
||||
private void handlePurchase(String cloak) {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (now - lastPurchaseTime >= 3000) {
|
||||
// Utils.notify(NotificationKind.Info, "Purchase processing", "Please wait");
|
||||
|
||||
ServiceClient.buyCloak(cloak);
|
||||
lastPurchaseTime = now;
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
|
||||
// RenderSystem.recordRenderCall(() -> {
|
||||
// MinecraftProvider.PROVIDER.setScreen(new CloakStore(now));
|
||||
// // Utils.notify(NotificationKind.Success,
|
||||
// // "Purchase complete",
|
||||
// // "Congrats, enjoy your new cloak!");
|
||||
// });
|
||||
}).start();
|
||||
|
||||
} else {
|
||||
// Utils.notify(NotificationKind.Error,
|
||||
// "Timeout error",
|
||||
// "Please wait 3 seconds");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
package org.saturnclient.ui.screens.store;
|
||||
|
||||
import org.saturnclient.client.ServiceClient;
|
||||
import org.saturnclient.client.player.SaturnPlayer;
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.config.AnimationConfig;
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.cosmetics.Hats;
|
||||
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.elements.AnimationStagger;
|
||||
import org.saturnclient.ui.elements.ImageTexture;
|
||||
import org.saturnclient.ui.elements.Scroll;
|
||||
import org.saturnclient.ui.elements.TabMenu;
|
||||
import org.saturnclient.ui.elements.Text;
|
||||
import org.saturnclient.ui.elements.TabMenu.TabMenuComponent;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
|
||||
// import com.mojang.blaze3d.systems.RenderSystem;
|
||||
|
||||
public class HatStore extends SaturnScreen {
|
||||
private long lastPurchaseTime = 0;
|
||||
|
||||
public HatStore() {
|
||||
super("Hat Store");
|
||||
}
|
||||
|
||||
public HatStore(long lastPurchaseTime) {
|
||||
super("Hat Store");
|
||||
this.lastPurchaseTime = lastPurchaseTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ui() {
|
||||
int p = 10;
|
||||
int gx = 10;
|
||||
int gy = 20;
|
||||
|
||||
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 hat : Hats.ALL_HATS) {
|
||||
if (!Hats.availableHats.contains(hat)) {
|
||||
|
||||
int x = (50 + gx) * col;
|
||||
int y = (50 + gy) * row;
|
||||
|
||||
stagger.draw(
|
||||
new CosmeticPreview(
|
||||
hat == player.hat,
|
||||
Textures.getHatPreview(hat),
|
||||
() -> handlePurchase(hat))
|
||||
.dimensions(50, 50)
|
||||
.position(x, y)
|
||||
.animation(new SlideY(AnimationConfig.cosmeticsMenu, 16)));
|
||||
|
||||
String t = "50";
|
||||
|
||||
stagger.draw(
|
||||
new Text(t)
|
||||
.position(x + Fonts.centerX(50, t, Theme.FONT.value), y + 53)
|
||||
.scale(0.5f)
|
||||
.animation(new Fade(400)));
|
||||
|
||||
stagger.draw(
|
||||
new ImageTexture(Textures.COINS)
|
||||
.dimensions(16, 16)
|
||||
.position(x + Fonts.getWidth(t, Theme.FONT.value) + 4, y + 52)
|
||||
.scale(0.5f)
|
||||
.animation(new Fade(400)));
|
||||
|
||||
if (col == 8) {
|
||||
col = 0;
|
||||
row++;
|
||||
} else {
|
||||
col++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scroll.draw(stagger);
|
||||
|
||||
int scrollWidth = 480 + 40 + (gx * 2) + (p * 2);
|
||||
|
||||
draw(scroll
|
||||
.dimensions(scrollWidth, 350)
|
||||
.center(width, height, 15, 0));
|
||||
|
||||
draw(new Sidebar(Sidebar.COSMETICS, this.provider::close)
|
||||
.center(width, height, -((scrollWidth - 30) / 2 + 35), 0));
|
||||
|
||||
draw(new TabMenu(1,
|
||||
new TabMenuComponent(Textures.CLOAK, () -> {
|
||||
Providers.saturn.getClient().setScreen(new CloakStore());
|
||||
}),
|
||||
new TabMenuComponent(Textures.HAT, () -> {
|
||||
}))
|
||||
.center(width, height, 0, -195));
|
||||
}
|
||||
|
||||
private void handlePurchase(String hat) {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (now - lastPurchaseTime >= 3000) {
|
||||
// Utils.notify(NotificationKind.Info, "Purchase processing", "Please wait");
|
||||
|
||||
ServiceClient.buyHat(hat);
|
||||
lastPurchaseTime = now;
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
|
||||
// RenderSystem.recordRenderCall(() -> {
|
||||
// MinecraftProvider.PROVIDER.setScreen(new HatStore(now));
|
||||
// // Utils.notify(NotificationKind.Success,
|
||||
// // "Purchase complete",
|
||||
// // "Congrats, enjoy your new hat!");
|
||||
// });
|
||||
}).start();
|
||||
|
||||
} else {
|
||||
// Utils.notify(NotificationKind.Error,
|
||||
// "Timeout error",
|
||||
// "Please wait 3 seconds");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"/></svg>
|
||||
|
After Width: | Height: | Size: 317 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
|
||||
|
After Width: | Height: | Size: 210 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||
|
After Width: | Height: | Size: 207 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="13.5" cy="6.5" r="1.5"/><circle cx="17.5" cy="10.5" r="1.5"/><circle cx="8.5" cy="7.5" r="1.5"/><circle cx="6.5" cy="12.5" r="1.5"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>
|
||||
|
After Width: | Height: | Size: 555 B |
Reference in New Issue
Block a user