diff --git a/src/main/java/org/saturnclient/config/AnimationConfig.java b/src/main/java/org/saturnclient/config/AnimationConfig.java index acaadda..3f429fb 100644 --- a/src/main/java/org/saturnclient/config/AnimationConfig.java +++ b/src/main/java/org/saturnclient/config/AnimationConfig.java @@ -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 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 getProperties() { + Map 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"); } } diff --git a/src/main/java/org/saturnclient/config/Config.java b/src/main/java/org/saturnclient/config/Config.java index d8424b5..01a0ed2 100644 --- a/src/main/java/org/saturnclient/config/Config.java +++ b/src/main/java/org/saturnclient/config/Config.java @@ -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); diff --git a/src/main/java/org/saturnclient/config/ConfigManager.java b/src/main/java/org/saturnclient/config/ConfigManager.java index b133cb6..f844fe0 100644 --- a/src/main/java/org/saturnclient/config/ConfigManager.java +++ b/src/main/java/org/saturnclient/config/ConfigManager.java @@ -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 */ diff --git a/src/main/java/org/saturnclient/config/Theme.java b/src/main/java/org/saturnclient/config/Theme.java index 70ad3af..b3f0d41 100644 --- a/src/main/java/org/saturnclient/config/Theme.java +++ b/src/main/java/org/saturnclient/config/Theme.java @@ -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 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"); } /** @@ -64,6 +97,35 @@ public class Theme { return hover ? ACCENT_FG.value : PRIMARY_FG.value; } + /** + * Translucent background for panels drawn over the world + */ + public static int getPanelBg() { + return withAlpha(0.82f, BACKGROUND.value); + } + + /** + * Faint outline for panels, cards and inputs + */ + public static int getBorder() { + return withAlpha(0.08f, FOREGROUND.value); + } + + /** + * Secondary text: descriptions, counts and placeholders + */ + public static int getMutedFg() { + return withAlpha(0.65f, FOREGROUND.value); + } + + /** + * Font for secondary text. Inter bold is too heavy there, so it becomes + * regular Inter; other fonts are kept. + */ + public static int getBodyFont() { + return FONT.value == 2 ? 1 : FONT.value; + } + /** * Multiplies existing alpha with 0–255 alpha. */ diff --git a/src/main/java/org/saturnclient/config/property/BoolProperty.java b/src/main/java/org/saturnclient/config/property/BoolProperty.java index 231f314..45025d3 100644 --- a/src/main/java/org/saturnclient/config/property/BoolProperty.java +++ b/src/main/java/org/saturnclient/config/property/BoolProperty.java @@ -12,6 +12,12 @@ public class BoolProperty extends Property { this.defaultValue = defaultValue; } + @Override + public void reset() { + this.value = defaultValue; + this.isReset = true; + } + @Override public JsonNode toJson() { return JsonNodeFactory.instance.booleanNode(this.value); diff --git a/src/main/java/org/saturnclient/config/property/ColorProperty.java b/src/main/java/org/saturnclient/config/property/ColorProperty.java index e25de45..5ee2cab 100644 --- a/src/main/java/org/saturnclient/config/property/ColorProperty.java +++ b/src/main/java/org/saturnclient/config/property/ColorProperty.java @@ -12,6 +12,12 @@ public class ColorProperty extends Property { this.defaultValue = defaultValue; } + @Override + public void reset() { + this.value = defaultValue; + this.isReset = true; + } + @Override public JsonNode toJson() { return JsonNodeFactory.instance.textNode( diff --git a/src/main/java/org/saturnclient/config/property/CurveProperty.java b/src/main/java/org/saturnclient/config/property/CurveProperty.java new file mode 100644 index 0000000..12818d7 --- /dev/null +++ b/src/main/java/org/saturnclient/config/property/CurveProperty.java @@ -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"); + } +} diff --git a/src/main/java/org/saturnclient/config/property/FloatProperty.java b/src/main/java/org/saturnclient/config/property/FloatProperty.java index 6b1dd6e..413999d 100644 --- a/src/main/java/org/saturnclient/config/property/FloatProperty.java +++ b/src/main/java/org/saturnclient/config/property/FloatProperty.java @@ -7,11 +7,42 @@ public class FloatProperty extends Property { public float value; public final float defaultValue; + /** Bounds for a slider, when the setting has a sensible range */ + public Float min, max; + public float step = 0.1f; + /** Shown after the value, like "×" */ + public String unit = ""; + public FloatProperty(float defaultValue) { this.value = defaultValue; this.defaultValue = defaultValue; } + /** + * Gives the setting a range, so it's edited with a slider. + */ + public FloatProperty range(float min, float max, float step) { + this.min = min; + this.max = max; + this.step = step; + return this; + } + + public FloatProperty unit(String unit) { + this.unit = unit; + return this; + } + + public boolean hasRange() { + return min != null && max != null; + } + + @Override + public void reset() { + this.value = defaultValue; + this.isReset = true; + } + @Override public JsonNode toJson() { return JsonNodeFactory.instance.numberNode(this.value); diff --git a/src/main/java/org/saturnclient/config/property/IntProperty.java b/src/main/java/org/saturnclient/config/property/IntProperty.java index 8160455..3a07ed8 100644 --- a/src/main/java/org/saturnclient/config/property/IntProperty.java +++ b/src/main/java/org/saturnclient/config/property/IntProperty.java @@ -7,11 +7,46 @@ public class IntProperty extends Property { public int value; public final int defaultValue; + /** Bounds for a slider, when the setting has a sensible range */ + public Integer min, max; + public int step = 1; + /** Shown after the value, like "ms" */ + public String unit = ""; + public IntProperty(int defaultValue) { this.value = defaultValue; this.defaultValue = defaultValue; } + /** + * Gives the setting a range, so it's edited with a slider. + */ + public IntProperty range(int min, int max) { + return range(min, max, 1); + } + + public IntProperty range(int min, int max, int step) { + this.min = min; + this.max = max; + this.step = step; + return this; + } + + public IntProperty unit(String unit) { + this.unit = unit; + return this; + } + + public boolean hasRange() { + return min != null && max != null; + } + + @Override + public void reset() { + this.value = defaultValue; + this.isReset = true; + } + @Override public JsonNode toJson() { return JsonNodeFactory.instance.numberNode(this.value); diff --git a/src/main/java/org/saturnclient/config/property/KeybindingProperty.java b/src/main/java/org/saturnclient/config/property/KeybindingProperty.java index 10f4422..ec98a5c 100644 --- a/src/main/java/org/saturnclient/config/property/KeybindingProperty.java +++ b/src/main/java/org/saturnclient/config/property/KeybindingProperty.java @@ -15,6 +15,12 @@ public class KeybindingProperty extends Property { this.defaultValue = defaultValue; } + @Override + public void reset() { + this.value = defaultValue; + this.isReset = true; + } + @Override public JsonNode toJson() { return JsonNodeFactory.instance.numberNode(this.value); diff --git a/src/main/java/org/saturnclient/config/property/NamedProperty.java b/src/main/java/org/saturnclient/config/property/NamedProperty.java index ddf2dc2..be6cf39 100644 --- a/src/main/java/org/saturnclient/config/property/NamedProperty.java +++ b/src/main/java/org/saturnclient/config/property/NamedProperty.java @@ -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; + } } \ No newline at end of file diff --git a/src/main/java/org/saturnclient/config/property/NamespaceProperty.java b/src/main/java/org/saturnclient/config/property/NamespaceProperty.java index bf71ef7..8b52176 100644 --- a/src/main/java/org/saturnclient/config/property/NamespaceProperty.java +++ b/src/main/java/org/saturnclient/config/property/NamespaceProperty.java @@ -13,6 +13,11 @@ public class NamespaceProperty extends Property { this.value = value; } + @Override + public void reset() { + this.value.values().forEach(Property::reset); + } + @Override public JsonNode toJson() { ObjectNode nested = JsonNodeFactory.instance.objectNode(); diff --git a/src/main/java/org/saturnclient/config/property/Property.java b/src/main/java/org/saturnclient/config/property/Property.java index becdb3b..c380a7b 100644 --- a/src/main/java/org/saturnclient/config/property/Property.java +++ b/src/main/java/org/saturnclient/config/property/Property.java @@ -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 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); } diff --git a/src/main/java/org/saturnclient/config/property/SelectProperty.java b/src/main/java/org/saturnclient/config/property/SelectProperty.java index 4edb555..7676732 100644 --- a/src/main/java/org/saturnclient/config/property/SelectProperty.java +++ b/src/main/java/org/saturnclient/config/property/SelectProperty.java @@ -14,6 +14,12 @@ public class SelectProperty extends Property { this.availableValues = availableValues; } + @Override + public void reset() { + this.value = defaultValue; + this.isReset = true; + } + @Override public JsonNode toJson() { return JsonNodeFactory.instance.numberNode(this.value); diff --git a/src/main/java/org/saturnclient/config/property/StringProperty.java b/src/main/java/org/saturnclient/config/property/StringProperty.java index e09d387..70e2462 100644 --- a/src/main/java/org/saturnclient/config/property/StringProperty.java +++ b/src/main/java/org/saturnclient/config/property/StringProperty.java @@ -12,6 +12,12 @@ public class StringProperty extends Property { this.defaultValue = defaultValue; } + @Override + public void reset() { + this.value = defaultValue; + this.isReset = true; + } + @Override public JsonNode toJson() { return JsonNodeFactory.instance.textNode(this.value); diff --git a/src/main/java/org/saturnclient/mod/ModLayout.java b/src/main/java/org/saturnclient/mod/ModLayout.java index ab0c8b6..eb380cf 100644 --- a/src/main/java/org/saturnclient/mod/ModLayout.java +++ b/src/main/java/org/saturnclient/mod/ModLayout.java @@ -1,5 +1,6 @@ package org.saturnclient.mod; +import java.util.LinkedHashMap; import java.util.Map; import org.saturnclient.config.property.Property; @@ -12,10 +13,10 @@ import org.saturnclient.config.property.NamedProperty; public class ModLayout { public IntProperty x = Property.integer(0); public IntProperty y = Property.integer(0); - public FloatProperty scale = Property.floatProp(1.0f); + public FloatProperty scale = Property.floatProp(1.0f).range(0.5f, 4f, 0.1f).unit("×"); public ColorProperty bgColor = Property.color(0x00000000); public ColorProperty fgColor = Property.color(0xFFffffff); - public IntProperty radius = Property.integer(0); + public IntProperty radius = Property.integer(0).range(0, 20); public SelectProperty font = Property.font(0); public boolean renderBackground = true; @@ -23,14 +24,22 @@ public class ModLayout { public int height = 0; public NamedProperty prop() { - return Property.namespace(Map.of( - "X", x, - "Y", y, - "Scale", scale, - "Background Color", bgColor, - "Foreground Color", fgColor, - "Corner Radius", radius, - "Font", font)).named("In-Game Display"); + // A LinkedHashMap, unlike Map.of, keeps the order the settings are shown in. + Map props = new LinkedHashMap<>(); + put(props, "X", x, "Distance from the left of the screen"); + put(props, "Y", y, "Distance from the top of the screen"); + put(props, "Scale", scale, "Size of the display"); + put(props, "Background Color", bgColor, "Fill behind the display"); + put(props, "Foreground Color", fgColor, "Color of the text"); + put(props, "Corner Radius", radius, "Rounding of the background's corners"); + put(props, "Font", font, "Font of the text"); + + return Property.namespace(props).named("In-Game Display"); + } + + private static void put(Map props, String name, Property prop, String description) { + prop.description = description; + props.put(name, prop); } public ModLayout() { diff --git a/src/main/java/org/saturnclient/mod/mods/ArmorDisplayMod.java b/src/main/java/org/saturnclient/mod/mods/ArmorDisplayMod.java index 40210a0..84c72b8 100644 --- a/src/main/java/org/saturnclient/mod/mods/ArmorDisplayMod.java +++ b/src/main/java/org/saturnclient/mod/mods/ArmorDisplayMod.java @@ -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()); } diff --git a/src/main/java/org/saturnclient/mod/mods/ClockMod.java b/src/main/java/org/saturnclient/mod/mods/ClockMod.java index b04645f..2f69237 100644 --- a/src/main/java/org/saturnclient/mod/mods/ClockMod.java +++ b/src/main/java/org/saturnclient/mod/mods/ClockMod.java @@ -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()); } diff --git a/src/main/java/org/saturnclient/mod/mods/CoordinatesMod.java b/src/main/java/org/saturnclient/mod/mods/CoordinatesMod.java index 6fb6934..e1a9a2f 100644 --- a/src/main/java/org/saturnclient/mod/mods/CoordinatesMod.java +++ b/src/main/java/org/saturnclient/mod/mods/CoordinatesMod.java @@ -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()); } diff --git a/src/main/java/org/saturnclient/mod/mods/FreelookMod.java b/src/main/java/org/saturnclient/mod/mods/FreelookMod.java index 53b6067..2e48d8e 100644 --- a/src/main/java/org/saturnclient/mod/mods/FreelookMod.java +++ b/src/main/java/org/saturnclient/mod/mods/FreelookMod.java @@ -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 --- diff --git a/src/main/java/org/saturnclient/mod/mods/FullbrightMod.java b/src/main/java/org/saturnclient/mod/mods/FullbrightMod.java index 9b34c5b..fa66c8c 100644 --- a/src/main/java/org/saturnclient/mod/mods/FullbrightMod.java +++ b/src/main/java/org/saturnclient/mod/mods/FullbrightMod.java @@ -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")); } // --------------------------------------------------------------- diff --git a/src/main/java/org/saturnclient/mod/mods/HealthDisplayMod.java b/src/main/java/org/saturnclient/mod/mods/HealthDisplayMod.java index da15115..822e120 100644 --- a/src/main/java/org/saturnclient/mod/mods/HealthDisplayMod.java +++ b/src/main/java/org/saturnclient/mod/mods/HealthDisplayMod.java @@ -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()); } diff --git a/src/main/java/org/saturnclient/mod/mods/KeystrokesMod.java b/src/main/java/org/saturnclient/mod/mods/KeystrokesMod.java index 50b9eca..eb0507f 100644 --- a/src/main/java/org/saturnclient/mod/mods/KeystrokesMod.java +++ b/src/main/java/org/saturnclient/mod/mods/KeystrokesMod.java @@ -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; } diff --git a/src/main/java/org/saturnclient/mod/mods/NametagsMod.java b/src/main/java/org/saturnclient/mod/mods/NametagsMod.java index 409e6a0..0a066da 100644 --- a/src/main/java/org/saturnclient/mod/mods/NametagsMod.java +++ b/src/main/java/org/saturnclient/mod/mods/NametagsMod.java @@ -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")); } // --------------------------------------------------------------- diff --git a/src/main/java/org/saturnclient/mod/mods/SpeedometerMod.java b/src/main/java/org/saturnclient/mod/mods/SpeedometerMod.java index b0579e5..013b894 100644 --- a/src/main/java/org/saturnclient/mod/mods/SpeedometerMod.java +++ b/src/main/java/org/saturnclient/mod/mods/SpeedometerMod.java @@ -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()); } diff --git a/src/main/java/org/saturnclient/mod/mods/ZoomMod.java b/src/main/java/org/saturnclient/mod/mods/ZoomMod.java index 92a66c8..c403a34 100644 --- a/src/main/java/org/saturnclient/mod/mods/ZoomMod.java +++ b/src/main/java/org/saturnclient/mod/mods/ZoomMod.java @@ -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")); } // --------------------------------------------------------------- diff --git a/src/main/java/org/saturnclient/ui/ElementContext.java b/src/main/java/org/saturnclient/ui/ElementContext.java index 2077161..d78f6cf 100644 --- a/src/main/java/org/saturnclient/ui/ElementContext.java +++ b/src/main/java/org/saturnclient/ui/ElementContext.java @@ -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); } diff --git a/src/main/java/org/saturnclient/ui/ElementRenderer.java b/src/main/java/org/saturnclient/ui/ElementRenderer.java index 7744ef6..ec2cd96 100644 --- a/src/main/java/org/saturnclient/ui/ElementRenderer.java +++ b/src/main/java/org/saturnclient/ui/ElementRenderer.java @@ -15,6 +15,14 @@ public class ElementRenderer { } public static void render(List 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 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(); } } diff --git a/src/main/java/org/saturnclient/ui/PanelScreen.java b/src/main/java/org/saturnclient/ui/PanelScreen.java new file mode 100644 index 0000000..2bf84a2 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/PanelScreen.java @@ -0,0 +1,82 @@ +package org.saturnclient.ui; + +import org.saturnclient.ui.components.Sidebar; +import org.saturnclient.ui.elements.ImageTexture; +import org.saturnclient.ui.elements.Panel; +import org.saturnclient.ui.elements.Scroll; +import org.saturnclient.ui.resources.Textures; + +/** + * A screen laid out as the sidebar beside one panel, centered and shrunk to + * fit smaller windows. Subclasses fill the panel in {@link #panel()}, using the + * content bounds this class computes. + */ +public abstract class PanelScreen extends SaturnScreen { + private static final int MAX_WIDTH = 900; + private static final int MAX_HEIGHT = 580; + private static final int MARGIN = 40; + private static final int GAP = 12; + + protected static final int PADDING = 20; + + /** How far a scroll reaches past its content area, into the padding, for its bar */ + private static final int SCROLL_OVERHANG = 14; + + private final int sidebarItem; + + protected int panelX, panelY, panelWidth, panelHeight; + /** The panel's area inside its padding */ + protected int contentX, contentY, contentWidth, contentHeight; + + /** + * @param sidebarItem the {@link Sidebar} item to highlight + */ + public PanelScreen(String title, int sidebarItem) { + super(title); + this.sidebarItem = sidebarItem; + } + + @Override + public final void ui() { + panelWidth = Math.min(MAX_WIDTH, width - MARGIN * 2 - Sidebar.WIDTH - GAP); + panelHeight = Math.min(MAX_HEIGHT, height - MARGIN * 2); + + int left = (width - (Sidebar.WIDTH + GAP + panelWidth)) / 2; + panelX = left + Sidebar.WIDTH + GAP; + panelY = (height - panelHeight) / 2; + + contentX = panelX + PADDING; + contentY = panelY + PADDING; + contentWidth = panelWidth - PADDING * 2; + contentHeight = panelHeight - PADDING * 2; + + draw(new Sidebar(sidebarItem, this.provider::close) + .dimensions(Sidebar.WIDTH, panelHeight) + .position(left, panelY)); + + draw(new Panel().dimensions(panelWidth, panelHeight).position(panelX, panelY)); + + panel(); + + draw(new ImageTexture(Textures.LOGO_TEXT_BIG).dimensions(180, 18).position(width - 180 - 20, height - 18 - 20)); + } + + /** + * Draws the panel's content. + */ + protected abstract void panel(); + + /** + * The width of a scroll filling {@code width} of the content area. + */ + protected static int scrollWidth(int width) { + return width + SCROLL_OVERHANG; + } + + /** + * The width left for a scroll's children, clear of its scrollbar. + */ + protected static int scrollContentWidth(int width) { + return scrollWidth(width) - Scroll.SCROLLBAR_SPACE; + } +} diff --git a/src/main/java/org/saturnclient/ui/Popup.java b/src/main/java/org/saturnclient/ui/Popup.java new file mode 100644 index 0000000..ab92e19 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/Popup.java @@ -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)); + } +} diff --git a/src/main/java/org/saturnclient/ui/RenderScope.java b/src/main/java/org/saturnclient/ui/RenderScope.java index ed4513f..0bbc677 100644 --- a/src/main/java/org/saturnclient/ui/RenderScope.java +++ b/src/main/java/org/saturnclient/ui/RenderScope.java @@ -3,6 +3,7 @@ package org.saturnclient.ui; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.Locale; import org.saturnclient.common.ref.asset.IdentifierRef; import org.saturnclient.common.ref.asset.SpriteRef; @@ -36,19 +37,66 @@ public interface RenderScope { radius = Math.min(radius, width / 2); radius = Math.min(radius, height / 2); - String svg = String.format( - "" + - "" + - "", - width, height, + String rect = String.format( + "", width, height, radius, radius); + drawSvgShape(rect, "rounded_" + width + "_" + height + "_" + radius, x, y, width, height, color); + } + + /** + * Draws a rounded outline that stays inside the given bounds, so it lines up + * with a {@link #drawRoundedRectangle} of the same size and radius. + */ + default void drawRoundedBorder(int x, int y, int width, int height, int radius, int thickness, int color) { + if (color == 0 || thickness <= 0) + return; + + radius = Math.min(radius, width / 2); + radius = Math.min(radius, height / 2); + thickness = Math.min(thickness, Math.min(width, height) / 2); + + // The stroke is centered on the path, so inset it by half its width. + float inset = thickness / 2f; + float innerRadius = Math.max(0, radius - inset); + + String rect = String.format(Locale.ROOT, + "", + inset, inset, + width - (float) thickness, height - (float) thickness, + innerRadius, innerRadius, + thickness); + + drawSvgShape(rect, "rounded_border_" + width + "_" + height + "_" + radius + "_" + thickness, + x, y, width, height, color); + } + + /** + * Draws a rounded rectangle filled with {@code background} and outlined with + * {@code border}. Either color can be 0 to skip that part. + */ + default void drawRoundedRectangle(int x, int y, int width, int height, int radius, int thickness, + int background, int border) { + drawRoundedRectangle(x, y, width, height, radius, background); + drawRoundedBorder(x, y, width, height, radius, thickness, border); + } + + /** + * Renders {@code shape} (SVG elements in a {@code width} x {@code height} + * canvas, drawn in white) to a cached texture and draws it tinted with + * {@code color}. + */ + private void drawSvgShape(String shape, String cacheKey, int x, int y, int width, int height, int color) { + String svg = String.format( + "%s", + width, height, shape); + InputStream svgStream = new ByteArrayInputStream(svg.getBytes(StandardCharsets.UTF_8)); drawTexture(SvgTexture.getSvg(svgStream, - IdentifierRef.ofSaturn( - "textures/gui/tmp_rect/rounded_" + width + "_" + height + "_" + radius), + IdentifierRef.ofSaturn("textures/gui/tmp_rect/" + cacheKey), width * 4, height * 4), x, y, 0, 0, width, height, color); } diff --git a/src/main/java/org/saturnclient/ui/SaturnScreen.java b/src/main/java/org/saturnclient/ui/SaturnScreen.java index 4f99701..aea1cfe 100644 --- a/src/main/java/org/saturnclient/ui/SaturnScreen.java +++ b/src/main/java/org/saturnclient/ui/SaturnScreen.java @@ -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 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; } diff --git a/src/main/java/org/saturnclient/ui/components/ColorGrid.java b/src/main/java/org/saturnclient/ui/components/ColorGrid.java new file mode 100644 index 0000000..260eb8d --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/ColorGrid.java @@ -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 properties, int width, int y) { + int tileWidth = (width - GAP * (COLUMNS - 1)) / COLUMNS; + int i = 0; + + for (Map.Entry 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); + } + } +} diff --git a/src/main/java/org/saturnclient/ui/components/CosmeticPreview.java b/src/main/java/org/saturnclient/ui/components/CosmeticPreview.java deleted file mode 100644 index 81935ad..0000000 --- a/src/main/java/org/saturnclient/ui/components/CosmeticPreview.java +++ /dev/null @@ -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(); - } - } -} diff --git a/src/main/java/org/saturnclient/ui/components/CosmeticTile.java b/src/main/java/org/saturnclient/ui/components/CosmeticTile.java new file mode 100644 index 0000000..f0cbfe5 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/CosmeticTile.java @@ -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; + } +} diff --git a/src/main/java/org/saturnclient/ui/components/HudPreviewCard.java b/src/main/java/org/saturnclient/ui/components/HudPreviewCard.java new file mode 100644 index 0000000..d1e062c --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/HudPreviewCard.java @@ -0,0 +1,54 @@ +package org.saturnclient.ui.components; + +import org.saturnclient.config.Theme; +import org.saturnclient.mod.HudMod; +import org.saturnclient.mod.ModLayout; +import org.saturnclient.ui.Element; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; + +/** + * A card showing a HUD mod as it looks in game, following its settings live. + * The mod is drawn at its in-game scale, shrunk if needed to fit the card. + */ +public class HudPreviewCard extends Element { + private static final int PADDING = 16; + private static final int TITLE_HEIGHT = 24; + private static final float TITLE_SCALE = 0.6f; + + private final HudMod mod; + private final HudModPreview preview; + + public HudPreviewCard(HudMod mod) { + this.mod = mod; + this.preview = new HudModPreview(mod); + } + + @Override + public void render(RenderScope renderScope, ElementContext ctx) { + renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value + 4, 1, + Theme.withAlpha(0.72f, Theme.PRIMARY.value), Theme.withAlpha(0.06f, Theme.FOREGROUND.value)); + + renderScope.drawText(TITLE_SCALE, "PREVIEW", PADDING, PADDING, Theme.FONT.value, Theme.getMutedFg()); + + ModLayout layout = mod.getDimensions(); + if (layout.width <= 0 || layout.height <= 0) { + return; + } + + // HUD mods are laid out in GUI pixels, which are two Saturn units. + int areaWidth = width - PADDING * 2; + int areaHeight = height - PADDING * 2 - TITLE_HEIGHT; + float scale = Math.min(layout.scale.value * 2, + Math.min((float) areaWidth / layout.width, (float) areaHeight / layout.height)); + + float x = PADDING + (areaWidth - layout.width * scale) / 2; + float y = PADDING + TITLE_HEIGHT + (areaHeight - layout.height * scale) / 2; + + renderScope.getMatrixStack().push(); + renderScope.getMatrixStack().translate(x, y); + renderScope.getMatrixStack().scale(scale, scale); + preview.render(renderScope, ctx); + renderScope.getMatrixStack().pop(); + } +} diff --git a/src/main/java/org/saturnclient/ui/components/ModHeader.java b/src/main/java/org/saturnclient/ui/components/ModHeader.java new file mode 100644 index 0000000..c0b70bf --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/ModHeader.java @@ -0,0 +1,91 @@ +package org.saturnclient.ui.components; + +import org.saturnclient.config.Theme; +import org.saturnclient.mod.Mod; +import org.saturnclient.ui.Element; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.resources.Fonts; + +/** + * The top of a mod's settings: its icon, name, version and tags, and description. + */ +public class ModHeader extends Element { + public static final int HEIGHT = 60; + + private static final int ICON = 30; + private static final int TEXT_X = HEIGHT + 16; + private static final float NAME_SCALE = 1.2f; + private static final float DESCRIPTION_SCALE = 0.7f; + + private static final int CHIP_HEIGHT = 22; + private static final int CHIP_PADDING = 8; + private static final int CHIP_GAP = 6; + private static final float CHIP_SCALE = 0.58f; + + private final Mod mod; + + public ModHeader(Mod mod, int width) { + this.mod = mod; + this.width = width; + this.height = HEIGHT; + } + + @Override + public void render(RenderScope renderScope, ElementContext ctx) { + boolean enabled = mod.isEnabled(); + + // Icon + renderScope.drawRoundedRectangle(0, 0, HEIGHT, HEIGHT, Theme.WIDGET_RADIUS.value + 4, + enabled ? Theme.withAlpha(0.18f, Theme.ACCENT.value) : Theme.withAlpha(0.06f, Theme.FOREGROUND.value)); + renderScope.drawTexture(mod.getIconTexture(), (HEIGHT - ICON) / 2, (HEIGHT - ICON) / 2, 0, 0, ICON, ICON, + Theme.PRIMARY_FG.value); + + // Name, then the version and tags as chips + int nameHeight = (int) (Fonts.getHeight() * NAME_SCALE); + int nameY = 4; + renderScope.drawText(NAME_SCALE, mod.getName(), TEXT_X, nameY, Theme.FONT.value, Theme.FOREGROUND.value); + + int chipX = TEXT_X + (int) (Fonts.getWidth(mod.getName(), Theme.FONT.value) * NAME_SCALE) + 12; + int chipY = nameY + (nameHeight - CHIP_HEIGHT) / 2; + + if (mod.getVersion() != null) { + chipX = drawChip(renderScope, mod.getVersion(), chipX, chipY, false) + CHIP_GAP; + } + + for (String tag : mod.getTags()) { + chipX = drawChip(renderScope, tag, chipX, chipY, true) + CHIP_GAP; + } + + // Description + if (mod.getDescription() != null) { + String description = Fonts.ellipsize(mod.getDescription(), Theme.getBodyFont(), DESCRIPTION_SCALE, + width - TEXT_X); + renderScope.drawText(DESCRIPTION_SCALE, description, TEXT_X, nameY + nameHeight + 8, + Theme.getBodyFont(), Theme.getMutedFg()); + } + } + + /** + * @return the x after the chip + */ + private int drawChip(RenderScope renderScope, String text, int x, int y, boolean outlined) { + int width = CHIP_PADDING * 2 + (int) (Fonts.getWidth(text, Theme.FONT.value) * CHIP_SCALE); + + if (x + width > this.width) { + return x; + } + + if (outlined) { + renderScope.drawRoundedBorder(x, y, width, CHIP_HEIGHT, 6, 1, Theme.withAlpha(0.14f, Theme.FOREGROUND.value)); + } else { + renderScope.drawRoundedRectangle(x, y, width, CHIP_HEIGHT, 6, Theme.withAlpha(0.07f, Theme.FOREGROUND.value)); + } + + int textY = y + (CHIP_HEIGHT - (int) (Fonts.getHeight() * CHIP_SCALE)) / 2; + renderScope.drawText(CHIP_SCALE, text, x + CHIP_PADDING, textY, Theme.FONT.value, + outlined ? Theme.PRIMARY_FG.value : Theme.getMutedFg()); + + return x + width; + } +} diff --git a/src/main/java/org/saturnclient/ui/components/PropertyList.java b/src/main/java/org/saturnclient/ui/components/PropertyList.java new file mode 100644 index 0000000..8012304 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/PropertyList.java @@ -0,0 +1,154 @@ +package org.saturnclient.ui.components; + +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.saturnclient.config.Theme; +import org.saturnclient.config.property.BoolProperty; +import org.saturnclient.config.property.ColorProperty; +import org.saturnclient.config.property.FloatProperty; +import org.saturnclient.config.property.IntProperty; +import org.saturnclient.config.property.KeybindingProperty; +import org.saturnclient.config.property.NamespaceProperty; +import org.saturnclient.config.property.Property; +import org.saturnclient.config.property.SelectProperty; +import org.saturnclient.ui.Element; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.components.inputs.ColorInput; +import org.saturnclient.ui.components.inputs.FloatInput; +import org.saturnclient.ui.components.inputs.IntInput; +import org.saturnclient.ui.components.inputs.KeybindingSelector; +import org.saturnclient.ui.components.inputs.Select; +import org.saturnclient.ui.components.inputs.Slider; +import org.saturnclient.ui.components.inputs.Toggle; +import org.saturnclient.ui.elements.Label; +import org.saturnclient.ui.elements.Scroll; +import org.saturnclient.ui.resources.Fonts; + +/** + * Lays out config properties as rows of name and control, one per property. + * A nested namespace becomes a titled section. + */ +public final class PropertyList { + public static final int ROW_HEIGHT = 56; + + private static final int SECTION_GAP = 18; + private static final float SECTION_SCALE = 0.6f; + + private PropertyList() { + } + + /** + * Adds rows for {@code properties} to {@code scroll}, starting at {@code y}. + * + * @param skip names of top-level properties to leave out + * @return the y below the last row + */ + public static int draw(Scroll scroll, Map properties, Set skip, int width, int y) { + for (Map.Entry entry : properties.entrySet()) { + String name = entry.getKey(); + Property prop = entry.getValue(); + + if (skip.contains(name)) { + continue; + } + + if (prop instanceof NamespaceProperty namespace) { + y = drawSection(scroll, name, y + SECTION_GAP) + 4; + y = draw(scroll, namespace.value, Set.of(), width, y); + continue; + } + + Element control = createControl(prop); + if (control == null) { + continue; + } + + scroll.draw(new Row(name, prop.description, width, control.width).position(0, y)); + scroll.draw(control.position(width - control.width, y + (ROW_HEIGHT - control.height) / 2)); + y += ROW_HEIGHT; + } + + return y; + } + + /** + * Adds a section title, such as a namespace's name, at {@code y}. + * + * @return the y below the title + */ + public static int drawSection(Scroll scroll, String name, int y) { + String title = name.toUpperCase(Locale.ROOT); + scroll.draw(new Label(() -> title, SECTION_SCALE, () -> Theme.FONT.value, Theme::getMutedFg).position(0, y)); + return y + (int) (Fonts.getHeight() * SECTION_SCALE); + } + + /** + * The input for a property, or null for types without one (strings). + */ + private static Element createControl(Property prop) { + if (prop instanceof BoolProperty p) { + return new Toggle(p); + } else if (prop instanceof ColorProperty p) { + return new ColorInput(p); + } else if (prop instanceof IntProperty p) { + return p.hasRange() ? Slider.of(p) : new IntInput(p); + } else if (prop instanceof FloatProperty p) { + return p.hasRange() ? Slider.of(p) : new FloatInput(p); + } else if (prop instanceof SelectProperty p) { + return new Select(p); + } else if (prop instanceof KeybindingProperty p) { + return new KeybindingSelector(p); + } + + return null; + } + + /** + * A row's name, its description under it, and the line below the row. The + * control is a separate element on top, so it gets the clicks and key + * presses. + */ + private static class Row extends Element { + private static final float NAME_SCALE = 0.74f; + private static final float DESCRIPTION_SCALE = 0.62f; + private static final int LINE_GAP = 4; + + private final String name; + private final String description; + private final int controlWidth; + + Row(String name, String description, int width, int controlWidth) { + this.name = name; + this.description = description; + this.controlWidth = controlWidth; + this.width = width; + this.height = ROW_HEIGHT; + } + + @Override + public void render(RenderScope renderScope, ElementContext ctx) { + int textWidth = width - controlWidth - 16; + int nameHeight = (int) (Fonts.getHeight() * NAME_SCALE); + int descriptionHeight = (int) (Fonts.getHeight() * DESCRIPTION_SCALE); + boolean described = !description.isEmpty(); + + int blockHeight = described ? nameHeight + LINE_GAP + descriptionHeight : nameHeight; + int nameY = (height - blockHeight) / 2; + + renderScope.drawText(NAME_SCALE, Fonts.ellipsize(name, Theme.FONT.value, NAME_SCALE, textWidth), + 0, nameY, Theme.FONT.value, Theme.FOREGROUND.value); + + if (described) { + int font = Theme.getBodyFont(); + renderScope.drawText(DESCRIPTION_SCALE, + Fonts.ellipsize(description, font, DESCRIPTION_SCALE, textWidth), + 0, nameY + nameHeight + LINE_GAP, font, Theme.getMutedFg()); + } + + renderScope.drawRectangle(0, height - 1, width, 1, Theme.withAlpha(0.06f, Theme.FOREGROUND.value)); + } + } +} diff --git a/src/main/java/org/saturnclient/ui/components/SaturnModule.java b/src/main/java/org/saturnclient/ui/components/SaturnModule.java index 3a753e3..280df8e 100644 --- a/src/main/java/org/saturnclient/ui/components/SaturnModule.java +++ b/src/main/java/org/saturnclient/ui/components/SaturnModule.java @@ -2,165 +2,119 @@ package org.saturnclient.ui.components; import org.saturnclient.common.provider.Providers; import org.saturnclient.config.Theme; -import org.saturnclient.mod.HudMod; import org.saturnclient.mod.Mod; import org.saturnclient.ui.Element; import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.RenderScope; -import org.saturnclient.ui.Utils; +import org.saturnclient.ui.components.inputs.Toggle; import org.saturnclient.ui.resources.Fonts; import org.saturnclient.ui.resources.Textures; -import org.saturnclient.ui.screens.ConfigEditor; +import org.saturnclient.ui.screens.ModSettings; +/** + * A mod's card in the mod menu: icon, name, description, a settings button and + * an on/off switch. Clicking anywhere but the settings button toggles the mod. + */ public class SaturnModule extends Element { + public static final int HEIGHT = 76; + + private static final int PADDING = 12; + private static final int ICON_TILE = 44; + private static final int ICON = 22; + private static final int SETTINGS = 32; + private static final int SETTINGS_ICON = 17; + private static final int GAP = 12; + private static final float NAME_SCALE = 0.74f; + private static final float DESCRIPTION_SCALE = 0.64f; private final Mod mod; - static int p = 10; // padding - static int s = 14; // settings icon size - static int expand = 5; // settings icon click area - - public SaturnModule(Mod mod) { + public SaturnModule(Mod mod, int width) { this.mod = mod; - this.width = 160; - this.height = 50; + this.width = width; + this.height = HEIGHT; } @Override public void render(RenderScope renderScope, ElementContext ctx) { - boolean hovered = ctx.isHovering(); boolean enabled = mod.isEnabled(); + boolean settingsHovered = ctx.isHovering(getSettingsX(), getSettingsY(), SETTINGS, SETTINGS); + boolean hovered = ctx.isHovering() && !settingsHovered; - int iconX = width - s - p; - int iconY = height - s - p; + int border = enabled ? Theme.withAlpha(hovered ? 0.7f : 0.4f, Theme.ACCENT.value) + : Theme.withAlpha(hovered ? 0.22f : 0.06f, Theme.FOREGROUND.value); - boolean settingsHover = ctx.isHovering( - getSettingsX(), - getSettingsY(), - getSettingsHitBox(), - getSettingsHitBox()); + // Card + renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1, + Theme.withAlpha(0.72f, Theme.PRIMARY.value), border); - // Resolve theme colors - int bgColor = Theme.PRIMARY.value; - int fgColor = hovered && !settingsHover ? Theme.ACCENT.value : Theme.PRIMARY_FG.value; - - int iconBg = enabled ? Theme.ACCENT.value : Theme.PRIMARY.value; - int iconFg = enabled ? Theme.ACCENT_FG.value : fgColor; - - int settingsColor = settingsHover - ? Theme.ACCENT.value - : Theme.withAlpha(160, fgColor); - - int h = height - (p * 2); - - // Main background - renderScope.drawRoundedRectangle( - 0, 0, - width, height, - Theme.WIDGET_RADIUS.value, - bgColor); - - // Icon background - renderScope.drawRoundedRectangle( - p, p, - h, h, - Theme.WIDGET_RADIUS.value, - iconBg); - - // Icon - renderScope.drawTexture( - mod.getIconTexture(), - p + (p / 2), - p + (p / 2), - 0, 0, - h - p, - h - p, - iconFg); - - // Module name - renderScope.drawText( - 0.6f, - mod.getName(), - p + h + 4, - p + 1, - Theme.FONT.value, - fgColor); - - // Version (right aligned) - String version = mod.getVersion(); - int versionWidth = (int) (Fonts.getWidth(version, Theme.FONT.value) * 0.35f); - - renderScope.drawText( - 0.35f, - version, - width - p - versionWidth, - p + 1, - Theme.FONT.value, - fgColor); - - // Tags - int xt = p + h + 4; - for (String tag : mod.getTags()) { - int tagWidth = (int) (Fonts.getWidth(tag, Theme.FONT.value) * 0.4f); - - renderScope.drawRoundedRectangle( - xt, - p + (h / 2), - tagWidth + 6, - 12, - 5, - Theme.ACCENT.value); - - renderScope.drawText( - 0.4f, - tag, - xt + 2, - p + (h / 2) + 3, - Theme.FONT.value, - Theme.ACCENT_FG.value); - - xt += tagWidth + 9; + if (enabled) { + renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, + Theme.withAlpha(0.06f, Theme.ACCENT.value)); } - // Settings icon - renderScope.drawTexture( - Textures.SETTINGS, - iconX, - iconY, - 0, 0, - s, s, - settingsColor); + // Icon + int tileY = (height - ICON_TILE) / 2; + renderScope.drawRoundedRectangle(PADDING, tileY, ICON_TILE, ICON_TILE, Theme.WIDGET_RADIUS.value, + enabled ? Theme.withAlpha(0.18f, Theme.ACCENT.value) : Theme.withAlpha(0.06f, Theme.FOREGROUND.value)); + + renderScope.drawTexture(mod.getIconTexture(), + PADDING + (ICON_TILE - ICON) / 2, tileY + (ICON_TILE - ICON) / 2, + 0, 0, ICON, ICON, Theme.PRIMARY_FG.value); + + // Name and description + int textX = PADDING + ICON_TILE + GAP; + int textWidth = getSettingsX() - GAP - textX; + int nameHeight = (int) (Fonts.getHeight() * NAME_SCALE); + int descriptionHeight = (int) (Fonts.getHeight() * DESCRIPTION_SCALE); + int nameY = (height - nameHeight - 5 - descriptionHeight) / 2; + + renderScope.drawText(NAME_SCALE, + Fonts.ellipsize(mod.getName(), Theme.FONT.value, NAME_SCALE, textWidth), + textX, nameY, Theme.FONT.value, Theme.FOREGROUND.value); + + String description = mod.getDescription() == null ? "" : mod.getDescription(); + renderScope.drawText(DESCRIPTION_SCALE, + Fonts.ellipsize(description, Theme.getBodyFont(), DESCRIPTION_SCALE, textWidth), + textX, nameY + nameHeight + 5, Theme.getBodyFont(), Theme.getMutedFg()); + + // Settings button + if (settingsHovered) { + renderScope.drawRoundedRectangle(getSettingsX(), getSettingsY(), SETTINGS, SETTINGS, + Theme.WIDGET_RADIUS.value, Theme.withAlpha(0.08f, Theme.FOREGROUND.value)); + } + + int settingsIconOffset = (SETTINGS - SETTINGS_ICON) / 2; + renderScope.drawTexture(Textures.ICON_SLIDERS, + getSettingsX() + settingsIconOffset, getSettingsY() + settingsIconOffset, + 0, 0, SETTINGS_ICON, SETTINGS_ICON, + settingsHovered ? Theme.ACCENT.value : Theme.getMutedFg()); + + // Switch + Toggle.drawSwitch(renderScope, getSwitchX(), (height - Toggle.SWITCH_HEIGHT) / 2, enabled); } @Override public void click(int mouseX, int mouseY) { - boolean settingsHover = Utils.isHovering( - mouseX - getSettingsX(), - mouseY - getSettingsY(), - getSettingsHitBox(), - getSettingsHitBox(), - 1.0f); + boolean settingsClicked = mouseX >= getSettingsX() && mouseX <= getSettingsX() + SETTINGS + && mouseY >= getSettingsY() && mouseY <= getSettingsY() + SETTINGS; - if (settingsHover) { - if (mod instanceof HudMod h) { - Providers.saturn.getClient().setScreen(new ConfigEditor(mod.getConfig(), h)); - } else { - Providers.saturn.getClient().setScreen(new ConfigEditor(mod.getConfig())); - } + if (settingsClicked) { + Providers.saturn.getClient().setScreen(new ModSettings(mod)); } else { mod.setEnabled(!mod.isEnabled()); } } + private int getSwitchX() { + return width - PADDING - Toggle.SWITCH_WIDTH; + } + private int getSettingsX() { - return width - s - p - expand; + return getSwitchX() - 8 - SETTINGS; } private int getSettingsY() { - return height - s - p - expand; + return (height - SETTINGS) / 2; } - - private int getSettingsHitBox() { - return s + expand * 2; - } -} \ No newline at end of file +} diff --git a/src/main/java/org/saturnclient/ui/components/SectionNav.java b/src/main/java/org/saturnclient/ui/components/SectionNav.java new file mode 100644 index 0000000..307c785 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/SectionNav.java @@ -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 items; + private final int active; + private final IntConsumer onSelect; + + public SectionNav(String heading, List 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); + } +} diff --git a/src/main/java/org/saturnclient/ui/components/Sidebar.java b/src/main/java/org/saturnclient/ui/components/Sidebar.java index 006b5a5..1aabfae 100644 --- a/src/main/java/org/saturnclient/ui/components/Sidebar.java +++ b/src/main/java/org/saturnclient/ui/components/Sidebar.java @@ -2,56 +2,51 @@ package org.saturnclient.ui.components; import org.saturnclient.common.provider.Providers; import org.saturnclient.common.ref.asset.IdentifierRef; -import org.saturnclient.config.Config; import org.saturnclient.config.Theme; import org.saturnclient.ui.Element; import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.RenderScope; -import org.saturnclient.ui.Utils; +import org.saturnclient.ui.elements.Panel; import org.saturnclient.ui.resources.Textures; -import org.saturnclient.ui.screens.ConfigEditor; +import org.saturnclient.ui.screens.ClientSettings; +import org.saturnclient.ui.screens.HudEditor; import org.saturnclient.ui.screens.ModMenu; -import org.saturnclient.ui.screens.cosmetics.CloakMenu; -import org.saturnclient.ui.screens.store.CloakStore; +import org.saturnclient.ui.screens.CosmeticsMenu; +/** + * The navigation rail beside Saturn's screens: the logo, one button per screen, + * and a close button at the bottom. + */ public class Sidebar extends Element { - public static class SidebarComponent { - Runnable onClick; - boolean end; - IdentifierRef sprite; + public static final int WIDTH = 60; - public SidebarComponent(IdentifierRef sprite, Runnable onClick, boolean end) { - this.onClick = onClick; - this.end = end; - this.sprite = sprite; - } + // Values for the constructor's `active` + public static final int MODS = 0; + public static final int COSMETICS = 1; + public static final int HUD_EDITOR = 2; + public static final int SETTINGS = 3; + + private static final int LOGO = 32; + private static final int BUTTON = 44; + private static final int ICON = 22; + private static final int GAP = 8; + private static final int PADDING = 12; + + private record Item(IdentifierRef icon, Runnable onClick) { } - Runnable onClose; - SidebarComponent[] components = { - new Sidebar.SidebarComponent(Textures.MODS_TAB, () -> { - Providers.saturn.getClient().setScreen(new ModMenu()); - }, false), - new Sidebar.SidebarComponent(Textures.SETTINGS, () -> { - Providers.saturn.getClient().setScreen(new ConfigEditor(Config.config)); - }, false), - - new Sidebar.SidebarComponent(Textures.SHIRT, () -> { - Providers.saturn.getClient().setScreen(new CloakMenu()); - }, false), - - new Sidebar.SidebarComponent(Textures.CLOSE, () -> { - onClose.run(); - }, true), - - new Sidebar.SidebarComponent(Textures.STORE, () -> { - Providers.saturn.getClient().setScreen(new CloakStore()); - }, true), + 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 CosmeticsMenu())), + new Item(Textures.ICON_HUD_EDITOR, () -> Providers.saturn.getClient().setScreen(new HudEditor())), + new Item(Textures.ICON_SETTINGS, () -> Providers.saturn.getClient().setScreen(new ClientSettings())), }; - int active = 0; + + private final int active; + private final Runnable onClose; public Sidebar(int active, Runnable onClose) { - this.width = 30; + this.width = WIDTH; this.height = 350; this.active = active; this.onClose = onClose; @@ -59,63 +54,75 @@ public class Sidebar extends Element { @Override public void render(RenderScope renderScope, ElementContext ctx) { - int topRow = 0; - int bottomRow = 0; + Panel.drawBackground(renderScope, width, height); - int gap = 5; // 5px gap between icons - int padding = 10; - int sp = 30 - padding + gap; - int s = 30 - (padding * 2); - int s2 = s + padding; + renderScope.drawTexture(Textures.LOGO, (width - LOGO) / 2, PADDING, 0, 0, LOGO, LOGO); - renderScope.drawRoundedRectangle(0, 0, width, height, Theme.BG_RADIUS.value, Theme.BACKGROUND.value); + int x = getButtonX(); - int i = 0; + for (int i = 0; i < items.length; i++) { + int y = getItemY(i); + boolean selected = i == active; + boolean hovered = ctx.isHovering(x, y, BUTTON, BUTTON); - for (SidebarComponent component : components) { - int y = component.end ? ((height - (sp * bottomRow)) - (s * 2)) : (padding + (sp * topRow)); - boolean selected = ctx.isHovering(0, y - (padding / 2), s2, s2) || active == i; - - renderScope.drawRoundedRectangle(padding / 2, y - (padding / 2), s + padding, - s + padding, Theme.WIDGET_RADIUS.value, Theme.getBg(selected)); - - renderScope.drawTexture(component.sprite, padding, y, 0, 0, s, s, Theme.getFg(selected)); - - if (component.end) { - bottomRow += 1; - } else { - topRow += 1; + if (selected) { + renderScope.drawRoundedRectangle(x, y, BUTTON, BUTTON, Theme.WIDGET_RADIUS.value, Theme.ACCENT.value); + } else if (hovered) { + renderScope.drawRoundedRectangle(x, y, BUTTON, BUTTON, Theme.WIDGET_RADIUS.value, + Theme.withAlpha(0.08f, Theme.FOREGROUND.value)); } - i++; + drawIcon(renderScope, items[i].icon(), x, y, + selected ? Theme.ACCENT_FG.value + : hovered ? Theme.FOREGROUND.value : Theme.withAlpha(0.8f, Theme.FOREGROUND.value)); } + + int closeY = getCloseY(); + boolean closeHovered = ctx.isHovering(x, closeY, BUTTON, BUTTON); + + renderScope.drawRoundedRectangle(x, closeY, BUTTON, BUTTON, Theme.WIDGET_RADIUS.value, + Theme.withAlpha(closeHovered ? 0.12f : 0.06f, Theme.FOREGROUND.value)); + + drawIcon(renderScope, Textures.ICON_CLOSE, x, closeY, + closeHovered ? Theme.FOREGROUND.value : Theme.withAlpha(0.8f, Theme.FOREGROUND.value)); } @Override public void click(int mouseX, int mouseY) { - int topRow = 0; - int bottomRow = 0; + int x = getButtonX(); - int gap = 5; - int padding = 10; - int sp = 30 - padding + gap; - int s = 30 - (padding * 2); - int s2 = s + padding; + if (mouseX < x || mouseX > x + BUTTON) { + return; + } - for (SidebarComponent component : components) { - int y = component.end ? ((height - (sp * bottomRow)) - (s * 2)) : (padding + (sp * topRow)); - boolean hovering = Utils.isHovering(mouseX - (padding / 2), mouseY - (y - (padding / 2)), s2, s2, scale); + for (int i = 0; i < items.length; i++) { + int y = getItemY(i); - if (hovering) { - component.onClick.run(); - break; - } - - if (component.end) { - bottomRow += 1; - } else { - topRow += 1; + if (mouseY >= y && mouseY <= y + BUTTON) { + items[i].onClick().run(); + return; } } + + if (mouseY >= getCloseY() && mouseY <= getCloseY() + BUTTON) { + onClose.run(); + } + } + + private static void drawIcon(RenderScope renderScope, IdentifierRef icon, int x, int y, int color) { + int offset = (BUTTON - ICON) / 2; + renderScope.drawTexture(icon, x + offset, y + offset, 0, 0, ICON, ICON, color); + } + + private int getButtonX() { + return (width - BUTTON) / 2; + } + + private int getItemY(int index) { + return PADDING + LOGO + 14 + index * (BUTTON + GAP); + } + + private int getCloseY() { + return height - PADDING - BUTTON; } } diff --git a/src/main/java/org/saturnclient/ui/components/SkinPreview.java b/src/main/java/org/saturnclient/ui/components/SkinPreview.java index 1686c75..48a2b06 100644 --- a/src/main/java/org/saturnclient/ui/components/SkinPreview.java +++ b/src/main/java/org/saturnclient/ui/components/SkinPreview.java @@ -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); } } diff --git a/src/main/java/org/saturnclient/ui/components/ThemePresetCard.java b/src/main/java/org/saturnclient/ui/components/ThemePresetCard.java new file mode 100644 index 0000000..108f7ec --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/ThemePresetCard.java @@ -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(); + } +} diff --git a/src/main/java/org/saturnclient/ui/components/inputs/ColorInput.java b/src/main/java/org/saturnclient/ui/components/inputs/ColorInput.java index df2f0be..c050de0 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/ColorInput.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/ColorInput.java @@ -1,183 +1,62 @@ package org.saturnclient.ui.components.inputs; -import java.awt.Color; - +import org.saturnclient.config.Theme; import org.saturnclient.config.property.ColorProperty; import org.saturnclient.ui.Element; import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.Popup; import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.elements.Field; +/** + * Shows a color as a swatch and its hex code, and opens a {@link ColorPicker} + * beside it when clicked. + */ public class ColorInput extends Element { + private static final int SWATCH = 20; + private static final int PICKER_GAP = 8; + public ColorProperty prop; - // Picker state - private boolean open = false; - private float hue = 0f; - private float opacity = 1f; - private int selectedX = 0; - private int selectedY = 0; - - private boolean draggingOpacity = false; - - // Dimensions - private static final int PICKER_WIDTH = 100; - private static final int PICKER_HEIGHT = 100; - private static final int SLIDER_WIDTH = 100; - private static final int SLIDER_HEIGHT = 10; - private static final int HUE_SLIDER_HEIGHT = 10; + /** Where this was last drawn on the screen, to open the picker beside it */ + private int originX, originY; public ColorInput(ColorProperty prop) { this.prop = prop; - this.width = 58; - this.height = 20; - syncFromColor(); - } - - @Override - public void click(int mouseX, int mouseY) { - open = !open; - } - - @Override - public void mouseClicked(double mouseX, double mouseY, int button) { - if (open) { - handlePickerClick(mouseX - width - 8, mouseY); - } - } - - @Override - public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { - if (open) { - handlePickerDrag(mouseX - width - 8, mouseY); - } - } - - @Override - public void mouseReleased(double mouseX, double mouseY, int button) { - draggingOpacity = false; + this.width = 116; + this.height = Field.HEIGHT; } @Override public void render(RenderScope renderScope, ElementContext ctx) { - // Draw the main color rectangle - renderScope.drawRoundedRectangle(0, 0, width, height, 6, prop.value); + originX = ctx.originX; + originY = ctx.originY; - if (!open) - return; + Field.drawBox(renderScope, width, height, ctx.isHovering(), Popup.isOpenFor(this)); - // Draw the picker relative to (width + 8, 0) - int pickerX = width + 8; - int pickerY = 0; + int swatchInset = (height - SWATCH) / 2; + renderScope.drawRoundedRectangle(swatchInset, swatchInset, SWATCH, SWATCH, 5, 1, prop.value, + Theme.withAlpha(0.3f, Theme.FOREGROUND.value)); - renderColorSquare(renderScope, pickerX, pickerY); - renderOpacitySlider(renderScope, pickerX, pickerY - SLIDER_HEIGHT - 10); - renderHueSlider(renderScope, pickerX, pickerY + PICKER_HEIGHT + 10); + renderScope.drawText(Field.TEXT_SCALE, toHex(prop.value), swatchInset + SWATCH + 8, + Field.textY(height, Field.TEXT_SCALE), Theme.FONT.value, Theme.FOREGROUND.value); } - private void renderColorSquare(RenderScope renderScope, int xStart, int yStart) { - for (int x = 0; x < PICKER_WIDTH; x++) { - for (int y = 0; y < PICKER_HEIGHT; y++) { - float saturation = x / (float) PICKER_WIDTH; - float value = 1f - y / (float) PICKER_HEIGHT; - int color = Color.HSBtoRGB(hue, saturation, value); - int alpha = (int) (opacity * 255) & 0xFF; - color = (alpha << 24) | (color & 0xFFFFFF); - renderScope.fill(xStart + x, yStart + y, xStart + x + 1, yStart + y + 1, color); - } - } - - // Draw selection square - int selX = xStart + selectedX - 4; - int selY = yStart + selectedY - 4; - renderScope.fill(selX, selY, selX + 8, selY + 8, 0xFF000000 | prop.value); - renderScope.drawBorder(selX, selY, 8, 8, 0xFFFFFFFF); + @Override + public void click(int mouseX, int mouseY) { + // The input sits at the right of its row, so the picker opens to its left, + // centered on it. While it's open, a click here closes it instead. + ColorPicker picker = new ColorPicker(prop); + Popup.open(this, picker, originX - PICKER_GAP - picker.width, originY + (height - picker.height) / 2); } - private void renderOpacitySlider(RenderScope renderScope, int xStart, int yStart) { - for (int x = 0; x < SLIDER_WIDTH; x++) { - float localOpacity = x / (float) SLIDER_WIDTH; - int alpha = (int) (localOpacity * 255) & 0xFF; - int color = (alpha << 24) | (prop.value & 0xFFFFFF); - renderScope.fill(xStart + x, yStart, xStart + x + 1, yStart + SLIDER_HEIGHT, color); - } - - int knobX = xStart + (int) (opacity * SLIDER_WIDTH); - renderScope.fill(knobX - 1, yStart - 2, knobX + 1, yStart + SLIDER_HEIGHT + 2, 0xFF000000); + /** + * #RRGGBB, or #AARRGGBB when the color is translucent. + */ + private static String toHex(int color) { + int alpha = (color >>> 24) & 0xFF; + return alpha == 0xFF + ? String.format("#%06X", color & 0xFFFFFF) + : String.format("#%08X", color); } - - private void renderHueSlider(RenderScope renderScope, int xStart, int yStart) { - for (int x = 0; x < SLIDER_WIDTH; x++) { - float localHue = x / (float) SLIDER_WIDTH; - int color = Color.HSBtoRGB(localHue, 1f, 1f); - renderScope.fill(xStart + x, yStart, xStart + x + 1, yStart + HUE_SLIDER_HEIGHT, 0xFF000000 | color); - } - - int knobX = xStart + (int) (hue * SLIDER_WIDTH); - renderScope.fill(knobX - 1, yStart - 2, knobX + 1, yStart + HUE_SLIDER_HEIGHT + 2, 0xFF000000); - } - - private void handlePickerClick(double mouseX, double mouseY) { - // Color square - if (mouseX >= 0 && mouseX < PICKER_WIDTH && mouseY >= 0 && mouseY < PICKER_HEIGHT) { - selectedX = (int) mouseX; - selectedY = (int) mouseY; - updateColorFromSelection(); - } - - // Opacity slider - int opY = -SLIDER_HEIGHT - 10; - if (mouseX >= 0 && mouseX < SLIDER_WIDTH && mouseY >= opY && mouseY <= opY + SLIDER_HEIGHT) { - updateOpacity(mouseX); - draggingOpacity = true; - } - - // Hue slider - int hueY = PICKER_HEIGHT + 10; - if (mouseX >= 0 && mouseX < SLIDER_WIDTH && mouseY >= hueY && mouseY <= hueY + HUE_SLIDER_HEIGHT) { - updateHue(mouseX); - } - } - - private void handlePickerDrag(double mouseX, double mouseY) { - // Opacity - if (draggingOpacity) - updateOpacity(mouseX); - - // Color square - if (mouseX >= 0 && mouseX < PICKER_WIDTH && mouseY >= 0 && mouseY < PICKER_HEIGHT) { - selectedX = (int) mouseX; - selectedY = (int) mouseY; - updateColorFromSelection(); - } - } - - private void updateColorFromSelection() { - float saturation = selectedX / (float) PICKER_WIDTH; - float value = 1f - selectedY / (float) PICKER_HEIGHT; - int color = Color.HSBtoRGB(hue, saturation, value); - int alpha = (int) (opacity * 255) & 0xFF; - prop.value = (alpha << 24) | (color & 0xFFFFFF); - } - - private void updateHue(double mouseX) { - hue = (float) Math.max(0, Math.min(1, mouseX / SLIDER_WIDTH)); - updateColorFromSelection(); - } - - private void updateOpacity(double mouseX) { - opacity = (float) Math.max(0, Math.min(1, mouseX / SLIDER_WIDTH)); - updateColorFromSelection(); - } - - private void syncFromColor() { - int rgb = prop.value & 0xFFFFFF; - float[] hsb = Color.RGBtoHSB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF, null); - hue = hsb[0]; - selectedX = (int) (hsb[1] * PICKER_WIDTH); - selectedY = (int) ((1f - hsb[2]) * PICKER_HEIGHT); - int alpha = (prop.value >> 24) & 0xFF; - if (alpha == 0) - alpha = 255; - opacity = alpha / 255f; - } -} \ No newline at end of file +} diff --git a/src/main/java/org/saturnclient/ui/components/inputs/ColorPicker.java b/src/main/java/org/saturnclient/ui/components/inputs/ColorPicker.java new file mode 100644 index 0000000..8c7f701 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/inputs/ColorPicker.java @@ -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)); + } +} diff --git a/src/main/java/org/saturnclient/ui/components/inputs/CurveOptions.java b/src/main/java/org/saturnclient/ui/components/inputs/CurveOptions.java new file mode 100644 index 0000000..29a7f3d --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/inputs/CurveOptions.java @@ -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 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); + } +} diff --git a/src/main/java/org/saturnclient/ui/components/inputs/CurvePreview.java b/src/main/java/org/saturnclient/ui/components/inputs/CurvePreview.java new file mode 100644 index 0000000..a5f2b45 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/inputs/CurvePreview.java @@ -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 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); + } +} diff --git a/src/main/java/org/saturnclient/ui/components/inputs/FloatInput.java b/src/main/java/org/saturnclient/ui/components/inputs/FloatInput.java index cdabac0..bc2908e 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/FloatInput.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/FloatInput.java @@ -1,17 +1,29 @@ package org.saturnclient.ui.components.inputs; +import java.math.BigDecimal; +import java.math.RoundingMode; + import org.saturnclient.config.ConfigManager; import org.saturnclient.config.property.FloatProperty; -import org.saturnclient.ui.resources.Fonts; +import org.saturnclient.ui.elements.Field; public class FloatInput extends Input { public FloatProperty prop; public FloatInput(FloatProperty prop) { - this.width = 120; - this.height = Fonts.getHeight(); + this.width = 96; + this.height = Field.HEIGHT; this.prop = prop; - this.text = String.valueOf(prop.value); + this.text = format(prop.value); + } + + /** + * The value with at most three decimals and no trailing zeros, since floats + * like 1.2000000476837158 can't be stored exactly. + */ + private static String format(float value) { + return new BigDecimal(Float.toString(value)).setScale(3, RoundingMode.HALF_UP).stripTrailingZeros() + .toPlainString(); } @Override @@ -51,7 +63,7 @@ public class FloatInput extends Input { @Override public void checkReset() { if (prop.isReset) { - this.text = String.valueOf(prop.value); + this.text = format(prop.value); this.cursorPosition = 0; prop.isReset = false; } diff --git a/src/main/java/org/saturnclient/ui/components/inputs/Input.java b/src/main/java/org/saturnclient/ui/components/inputs/Input.java index 8947416..2cd215c 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/Input.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/Input.java @@ -5,12 +5,18 @@ import org.saturnclient.config.Theme; import org.saturnclient.ui.Element; import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.elements.Field; import org.saturnclient.ui.resources.Fonts; public abstract class Input extends Element { String text = ""; public int cursorPosition = 0; + /** Scale the text is drawn at */ + protected float textScale = Field.TEXT_SCALE; + /** Horizontal inset of the text on both sides */ + protected int textX = 10; + public abstract void checkReset(); public abstract void backspace(); @@ -41,6 +47,7 @@ public abstract class Input extends Element { @Override public void click(int mouseX, int mouseY) { + mouseX -= textX; int scrollOffset = getScrollOffset(); String visibleText = text.substring(scrollOffset); @@ -48,7 +55,7 @@ public abstract class Input extends Element { float textWidth = 0; for (int i = 0; i < visibleText.length(); i++) { - textWidth = Fonts.getWidth(visibleText.substring(0, i + 1), Theme.FONT.value) * 0.6f; + textWidth = Fonts.getWidth(visibleText.substring(0, i + 1), Theme.FONT.value) * textScale; if (textWidth > mouseX) { break; @@ -64,28 +71,44 @@ public abstract class Input extends Element { public void render(RenderScope renderScope, ElementContext ctx) { checkReset(); - renderScope.drawRoundedRectangle(0, 0, width, height, 10, Theme.PRIMARY.value); + Field.drawBox(renderScope, width, height, ctx.isHovering(), focused); + drawText(renderScope); + } + + /** + * Draws the visible part of the text, and the cursor while focused. + */ + protected void drawText(RenderScope renderScope) { + int textHeight = (int) (Fonts.getHeight() * textScale); + int textY = Field.textY(height, textScale); int scrollOffset = getScrollOffset(); String visibleText = getVisibleText(scrollOffset); - renderScope.drawText(0.6f, visibleText, 4, 4, Theme.FONT.value, Theme.PRIMARY_FG.value); + renderScope.drawText(textScale, visibleText, textX, textY, Theme.FONT.value, Theme.FOREGROUND.value); if (focused) { - int cursorX = 4 + - (int) (Fonts.getWidth(visibleText.substring(0, cursorPosition - scrollOffset), Theme.FONT.value) - * 0.6f); - - renderScope.drawRectangle(cursorX, 2, 1, height - 4, Theme.ACCENT.value); + renderScope.drawRectangle(getCursorX(visibleText, scrollOffset), textY - 1, 1, textHeight + 2, + Theme.ACCENT.value); } } + protected int getCursorX(String visibleText, int scrollOffset) { + return textX + (int) (Fonts.getWidth(visibleText.substring(0, cursorPosition - scrollOffset), + Theme.FONT.value) * textScale); + } + + /** Width available to the text, in unscaled font units */ + private float getTextSpace() { + return (width - textX * 2) / textScale; + } + protected int getScrollOffset() { if (text.isEmpty()) { return 0; } int offset = 0; - int maxWidth = width - 4; // Available width for text + float maxWidth = getTextSpace(); while (offset < cursorPosition) { // Ensure cursor remains visible String visibleText = text.substring(offset, cursorPosition); if (Fonts.getWidth(visibleText, Theme.FONT.value) > maxWidth) { @@ -102,7 +125,7 @@ public abstract class Input extends Element { // Ensure text does not overflow past the available width for (int i = 1; i <= visibleText.length(); i++) { - if (Fonts.getWidth(visibleText.substring(0, i), Theme.FONT.value) > width - 4) { + if (Fonts.getWidth(visibleText.substring(0, i), Theme.FONT.value) > getTextSpace()) { return visibleText.substring(0, i - 1); // Trim overflow } } diff --git a/src/main/java/org/saturnclient/ui/components/inputs/IntInput.java b/src/main/java/org/saturnclient/ui/components/inputs/IntInput.java index 365cd74..64f73a9 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/IntInput.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/IntInput.java @@ -2,14 +2,14 @@ package org.saturnclient.ui.components.inputs; import org.saturnclient.config.ConfigManager; import org.saturnclient.config.property.IntProperty; -import org.saturnclient.ui.resources.Fonts; +import org.saturnclient.ui.elements.Field; public class IntInput extends Input { public IntProperty prop; public IntInput(IntProperty prop) { - this.width = 120; - this.height = Fonts.getHeight(); + this.width = 96; + this.height = Field.HEIGHT; this.prop = prop; this.text = String.valueOf(prop.value); } diff --git a/src/main/java/org/saturnclient/ui/components/inputs/KeybindingSelector.java b/src/main/java/org/saturnclient/ui/components/inputs/KeybindingSelector.java index 67b21bd..60461ad 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/KeybindingSelector.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/KeybindingSelector.java @@ -7,27 +7,41 @@ import org.saturnclient.config.property.KeybindingProperty; import org.saturnclient.ui.Element; import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.RenderScope; -import org.saturnclient.ui.resources.Fonts; +import org.saturnclient.ui.elements.Field; +/** + * Shows a key binding. Click it, then press a key to bind it, or backspace to + * unbind. + */ public class KeybindingSelector extends Element { KeybindingProperty prop; public KeybindingSelector(KeybindingProperty prop) { this.prop = prop; - this.width = 120; - this.height = Fonts.getHeight(); + this.width = 96; + this.height = Field.HEIGHT; } @Override public void render(RenderScope renderScope, ElementContext ctx) { - int textColor = focused ? 0xFFFFFF : 0xAAAAAA; - renderScope.drawRoundedRectangle(0, 0, width, height, 10, 0xFF000000); - String name = Providers.GLFW.getKeyName(prop.value); + Field.drawBox(renderScope, width, height, ctx.isHovering(), focused); - renderScope.drawText(0.6f, - prop.value == -1 ? "" - : name != null ? name.toUpperCase() : "", - 4, 4, Theme.FONT.value, textColor); + if (focused) { + Field.drawCenteredText(renderScope, "Press a key", width, height, Theme.getBodyFont(), + Theme.getMutedFg()); + return; + } + + Field.drawCenteredText(renderScope, getKeyLabel(), width, height, Theme.FONT.value, Theme.FOREGROUND.value); + } + + private String getKeyLabel() { + if (prop.value == -1) { + return "None"; + } + + String name = Providers.GLFW.getKeyName(prop.value); + return name != null ? name.toUpperCase() : "Key " + prop.value; } @Override diff --git a/src/main/java/org/saturnclient/ui/components/inputs/OptionList.java b/src/main/java/org/saturnclient/ui/components/inputs/OptionList.java new file mode 100644 index 0000000..83a73d7 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/inputs/OptionList.java @@ -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))); + } +} diff --git a/src/main/java/org/saturnclient/ui/components/inputs/SearchInput.java b/src/main/java/org/saturnclient/ui/components/inputs/SearchInput.java new file mode 100644 index 0000000..c757f2b --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/inputs/SearchInput.java @@ -0,0 +1,64 @@ +package org.saturnclient.ui.components.inputs; + +import java.util.function.Consumer; + +import org.saturnclient.config.Theme; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.elements.Field; +import org.saturnclient.ui.resources.Textures; + +/** + * A free-text search box with an icon and placeholder, reporting every change. + */ +public class SearchInput extends Input { + private static final int ICON_SIZE = 16; + private static final int ICON_X = 12; + + private final String placeholder; + private final Consumer onChange; + + public SearchInput(String placeholder, Consumer onChange) { + this.placeholder = placeholder; + this.onChange = onChange; + this.textX = ICON_X + ICON_SIZE + 8; + this.width = 240; + this.height = 36; + } + + @Override + public void charTyped(char chr) { + if (text.length() >= 64) { + return; + } + + text = text.substring(0, cursorPosition) + chr + text.substring(cursorPosition); + cursorPosition++; + onChange.accept(text); + } + + @Override + public void backspace() { + onChange.accept(text); + } + + @Override + public void checkReset() { + } + + @Override + public void render(RenderScope renderScope, ElementContext ctx) { + Field.drawBox(renderScope, width, height, ctx.isHovering(), focused); + + renderScope.drawTexture(Textures.ICON_SEARCH, ICON_X, (height - ICON_SIZE) / 2, 0, 0, + ICON_SIZE, ICON_SIZE, Theme.getMutedFg()); + + if (text.isEmpty() && !focused) { + renderScope.drawText(textScale, placeholder, textX, Field.textY(height, textScale), + Theme.getBodyFont(), Theme.getMutedFg()); + return; + } + + drawText(renderScope); + } +} diff --git a/src/main/java/org/saturnclient/ui/components/inputs/Select.java b/src/main/java/org/saturnclient/ui/components/inputs/Select.java index 698b6cb..09ba70e 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/Select.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/Select.java @@ -1,40 +1,66 @@ package org.saturnclient.ui.components.inputs; import org.saturnclient.config.Theme; +import org.saturnclient.config.property.CurveProperty; import org.saturnclient.config.property.SelectProperty; import org.saturnclient.ui.Element; import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.Popup; import org.saturnclient.ui.RenderScope; -import org.saturnclient.ui.Utils; +import org.saturnclient.ui.elements.Field; import org.saturnclient.ui.resources.Fonts; +import org.saturnclient.ui.resources.Textures; +/** + * A dropdown for a {@link SelectProperty}: the current value, opening a list + * of every value when clicked. + */ public class Select extends Element { - private SelectProperty prop; + private static final int TEXT_X = 12; + private static final int ARROW = 16; + private static final int ARROW_INSET = 10; + + private final SelectProperty prop; + + /** The context this was last drawn with, to open the list next to it */ + private ElementContext lastContext; public Select(SelectProperty prop) { this.prop = prop; - this.width = 150; - this.height = 15; + this.width = 200; + this.height = Field.HEIGHT; } @Override public void render(RenderScope renderScope, ElementContext ctx) { - renderScope.drawText(0.8f, "<", 0, 0, Theme.FONT.value, Theme.FOREGROUND.value); + lastContext = ctx; + boolean open = Popup.isOpenFor(this); - String text = prop.getSelection(); + Field.drawBox(renderScope, width, height, ctx.isHovering(), open); - renderScope.drawText(0.6f, text, (int) (width - ((float) Fonts.getWidth(text, Theme.FONT.value) * 0.6f)) / 2, 1, - Theme.FONT.value, Theme.FOREGROUND.value); + int textWidth = width - TEXT_X - ARROW_INSET - ARROW - 8; + String text = Fonts.ellipsize(prop.getSelection(), Theme.FONT.value, Field.TEXT_SCALE, textWidth); + renderScope.drawText(Field.TEXT_SCALE, text, TEXT_X, Field.textY(height, Field.TEXT_SCALE), Theme.FONT.value, + Theme.FOREGROUND.value); - renderScope.drawText(0.8f, ">", width - 10, 0, Theme.FONT.value, Theme.FOREGROUND.value); + renderScope.drawTexture(Textures.ICON_CHEVRON_DOWN, width - ARROW_INSET - ARROW, (height - ARROW) / 2, 0, 0, + ARROW, ARROW, open ? Theme.ACCENT.value : Theme.getMutedFg()); } @Override public void click(int mouseX, int mouseY) { - if (Utils.isHovering(mouseX - (width - 10), mouseY, 10, height, 1.0f)) { - prop.next(); - } else if (Utils.isHovering(mouseX, mouseY, 10, height, 1.0f)) { - prop.prev(); + // While the list is open, a click here goes to the popup, which closes it. + OptionList list = new OptionList(prop.availableValues, prop.value, width, index -> { + prop.setSelection(index); + Popup.close(); + }); + + if (prop instanceof CurveProperty) { + // The preview goes left of the list, so the list still lines up under this. + CurveOptions options = new CurveOptions(list, prop.value); + Popup.openBelow(this, options, lastContext, -options.getListX()); + } else { + Popup.openBelow(this, list, lastContext); } } } diff --git a/src/main/java/org/saturnclient/ui/components/inputs/Slider.java b/src/main/java/org/saturnclient/ui/components/inputs/Slider.java new file mode 100644 index 0000000..13f1965 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/inputs/Slider.java @@ -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; + } +} diff --git a/src/main/java/org/saturnclient/ui/components/inputs/Toggle.java b/src/main/java/org/saturnclient/ui/components/inputs/Toggle.java index c55f940..ee4d445 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/Toggle.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/Toggle.java @@ -7,20 +7,37 @@ import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.RenderScope; public class Toggle extends Element { + public static final int SWITCH_WIDTH = 42; + public static final int SWITCH_HEIGHT = 24; + private BoolProperty prop; public Toggle(BoolProperty prop) { this.prop = prop; - this.width = 60; - this.height = 30; + this.width = SWITCH_WIDTH; + this.height = SWITCH_HEIGHT; } @Override public void render(RenderScope renderScope, ElementContext ctx) { - renderScope.drawRoundedRectangle(0, 0, 60, 30, Theme.WIDGET_RADIUS.value, - Theme.withAlpha(0.5f, prop.value ? Theme.ACCENT.value : Theme.PRIMARY.value)); - renderScope.drawRoundedRectangle(prop.value ? 30 : 0, 0, 30, 30, Theme.WIDGET_RADIUS.value, - prop.value ? Theme.ACCENT.value : Theme.PRIMARY.value); + drawSwitch(renderScope, 0, 0, prop.value); + } + + /** + * Draws an on/off switch: a pill track with a round knob on the right when on. + */ + public static void drawSwitch(RenderScope renderScope, int x, int y, boolean on) { + int inset = 3; + int knob = SWITCH_HEIGHT - inset * 2; + + renderScope.drawRoundedRectangle(x, y, SWITCH_WIDTH, SWITCH_HEIGHT, SWITCH_HEIGHT / 2, + on ? Theme.ACCENT.value : Theme.withAlpha(0.16f, Theme.FOREGROUND.value)); + + renderScope.drawRoundedRectangle( + on ? x + SWITCH_WIDTH - inset - knob : x + inset, + y + inset, + knob, knob, knob / 2, + on ? Theme.ACCENT_FG.value : Theme.withAlpha(0.8f, Theme.PRIMARY_FG.value)); } @Override diff --git a/src/main/java/org/saturnclient/ui/elements/ActionButton.java b/src/main/java/org/saturnclient/ui/elements/ActionButton.java new file mode 100644 index 0000000..8fef9d3 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/elements/ActionButton.java @@ -0,0 +1,56 @@ +package org.saturnclient.ui.elements; + +import org.saturnclient.common.ref.asset.IdentifierRef; +import org.saturnclient.config.Theme; +import org.saturnclient.ui.Element; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.resources.Fonts; + +/** + * A secondary button with an icon and a label: outlined, or plain text that + * lights up on hover (for links like "back"). + */ +public class ActionButton extends Element { + private static final int HEIGHT = 34; + private static final int PADDING = 12; + private static final int ICON = 15; + private static final int ICON_GAP = 7; + private static final float TEXT_SCALE = 0.66f; + + private final IdentifierRef icon; + private final String text; + private final boolean outlined; + private final Runnable onClick; + + public ActionButton(IdentifierRef icon, String text, boolean outlined, Runnable onClick) { + this.icon = icon; + this.text = text; + this.outlined = outlined; + this.onClick = onClick; + this.height = HEIGHT; + this.width = PADDING * 2 + ICON + ICON_GAP + (int) (Fonts.getWidth(text, Theme.FONT.value) * TEXT_SCALE); + } + + @Override + public void render(RenderScope renderScope, ElementContext ctx) { + boolean hovered = ctx.isHovering(); + + if (outlined) { + renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1, + hovered ? Theme.withAlpha(0.06f, Theme.FOREGROUND.value) : 0, + Theme.withAlpha(hovered ? 0.25f : 0.12f, Theme.FOREGROUND.value)); + } + + int color = hovered ? Theme.FOREGROUND.value : outlined ? Theme.PRIMARY_FG.value : Theme.getMutedFg(); + + renderScope.drawTexture(icon, PADDING, (height - ICON) / 2, 0, 0, ICON, ICON, color); + renderScope.drawText(TEXT_SCALE, text, PADDING + ICON + ICON_GAP, Field.textY(height, TEXT_SCALE), + Theme.FONT.value, color); + } + + @Override + public void click(int mouseX, int mouseY) { + onClick.run(); + } +} diff --git a/src/main/java/org/saturnclient/ui/elements/AnimationStagger.java b/src/main/java/org/saturnclient/ui/elements/AnimationStagger.java index 0a8f123..80dba99 100644 --- a/src/main/java/org/saturnclient/ui/elements/AnimationStagger.java +++ b/src/main/java/org/saturnclient/ui/elements/AnimationStagger.java @@ -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 diff --git a/src/main/java/org/saturnclient/ui/elements/ChipGroup.java b/src/main/java/org/saturnclient/ui/elements/ChipGroup.java new file mode 100644 index 0000000..6f23659 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/elements/ChipGroup.java @@ -0,0 +1,85 @@ +package org.saturnclient.ui.elements; + +import java.util.List; +import java.util.function.Consumer; + +import org.saturnclient.config.Theme; +import org.saturnclient.ui.Element; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.resources.Fonts; + +/** + * A row of pill buttons where exactly one is selected, such as category filters. + */ +public class ChipGroup extends Element { + private static final int HEIGHT = 28; + private static final int PADDING = 14; + private static final int GAP = 8; + private static final float TEXT_SCALE = 0.62f; + + private final List options; + private final Consumer onSelect; + private String selected; + + public ChipGroup(List options, String selected, Consumer onSelect) { + this.options = options; + this.selected = selected; + this.onSelect = onSelect; + this.height = HEIGHT; + this.width = chipX(options.size()) - GAP; + } + + @Override + public void render(RenderScope renderScope, ElementContext ctx) { + int font = Theme.FONT.value; + int textY = (HEIGHT - (int) (Fonts.getHeight() * TEXT_SCALE)) / 2; + + for (int i = 0; i < options.size(); i++) { + String option = options.get(i); + int x = chipX(i); + int w = chipWidth(option); + boolean active = option.equals(selected); + boolean hovered = ctx.isHovering(x, 0, w, HEIGHT); + + if (active) { + renderScope.drawRoundedRectangle(x, 0, w, HEIGHT, HEIGHT / 2, Theme.ACCENT.value); + } else { + renderScope.drawRoundedRectangle(x, 0, w, HEIGHT, HEIGHT / 2, 1, + Theme.withAlpha(0.05f, Theme.FOREGROUND.value), + hovered ? Theme.withAlpha(0.25f, Theme.FOREGROUND.value) : Theme.getBorder()); + } + + renderScope.drawText(TEXT_SCALE, option, x + PADDING, textY, font, + active ? Theme.ACCENT_FG.value : Theme.PRIMARY_FG.value); + } + } + + @Override + public void click(int mouseX, int mouseY) { + for (int i = 0; i < options.size(); i++) { + String option = options.get(i); + int x = chipX(i); + + if (mouseX >= x && mouseX <= x + chipWidth(option)) { + if (!option.equals(selected)) { + selected = option; + onSelect.accept(option); + } + return; + } + } + } + + private int chipX(int index) { + int x = 0; + for (int i = 0; i < index; i++) { + x += chipWidth(options.get(i)) + GAP; + } + return x; + } + + private static int chipWidth(String option) { + return (int) (Fonts.getWidth(option, Theme.FONT.value) * TEXT_SCALE) + PADDING * 2; + } +} diff --git a/src/main/java/org/saturnclient/ui/elements/Divider.java b/src/main/java/org/saturnclient/ui/elements/Divider.java new file mode 100644 index 0000000..664a112 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/elements/Divider.java @@ -0,0 +1,23 @@ +package org.saturnclient.ui.elements; + +import org.saturnclient.config.Theme; +import org.saturnclient.ui.Element; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; + +/** + * A faint line filling the element: horizontal by default, or vertical when + * given a width of 1 and a height. + */ +public class Divider extends Element { + + public Divider(int width) { + this.width = width; + this.height = 1; + } + + @Override + public void render(RenderScope renderScope, ElementContext ctx) { + renderScope.drawRectangle(0, 0, width, height, Theme.getBorder()); + } +} diff --git a/src/main/java/org/saturnclient/ui/elements/Field.java b/src/main/java/org/saturnclient/ui/elements/Field.java new file mode 100644 index 0000000..4c857f2 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/elements/Field.java @@ -0,0 +1,44 @@ +package org.saturnclient.ui.elements; + +import org.saturnclient.config.Theme; +import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.resources.Fonts; + +/** + * The shared look of input fields: a faint box whose outline brightens on hover + * and turns to the accent color while focused. + */ +public final class Field { + /** Height of the fields in settings rows */ + public static final int HEIGHT = 32; + public static final float TEXT_SCALE = 0.68f; + + private Field() { + } + + public static void drawBox(RenderScope renderScope, int width, int height, boolean hovered, boolean focused) { + int border = focused ? Theme.withAlpha(0.6f, Theme.ACCENT.value) + : hovered ? Theme.withAlpha(0.25f, Theme.FOREGROUND.value) + : Theme.withAlpha(0.1f, Theme.FOREGROUND.value); + + renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1, + Theme.withAlpha(0.06f, Theme.FOREGROUND.value), border); + } + + /** + * The y at which text drawn at {@code scale} is vertically centered in a + * field of the given height. + */ + public static int textY(int height, float scale) { + return (height - (int) (Fonts.getHeight() * scale)) / 2; + } + + /** + * Draws {@code text} centered in a field of the given size. + */ + public static void drawCenteredText(RenderScope renderScope, String text, int width, int height, int font, + int color) { + int x = (width - (int) (Fonts.getWidth(text, font) * TEXT_SCALE)) / 2; + renderScope.drawText(TEXT_SCALE, text, x, textY(height, TEXT_SCALE), font, color); + } +} diff --git a/src/main/java/org/saturnclient/ui/elements/Label.java b/src/main/java/org/saturnclient/ui/elements/Label.java new file mode 100644 index 0000000..a0be75a --- /dev/null +++ b/src/main/java/org/saturnclient/ui/elements/Label.java @@ -0,0 +1,33 @@ +package org.saturnclient.ui.elements; + +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +import org.saturnclient.ui.Element; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.resources.Fonts; + +/** + * A line of text whose content, font and color are read on every frame, so it + * follows state and theme changes without rebuilding the screen. + */ +public class Label extends Element { + private final Supplier text; + private final float textScale; + private final IntSupplier font; + private final IntSupplier color; + + public Label(Supplier text, float textScale, IntSupplier font, IntSupplier color) { + this.text = text; + this.textScale = textScale; + this.font = font; + this.color = color; + this.height = (int) (Fonts.getHeight() * textScale); + } + + @Override + public void render(RenderScope renderScope, ElementContext ctx) { + renderScope.drawText(textScale, text.get(), 0, 0, font.getAsInt(), color.getAsInt()); + } +} diff --git a/src/main/java/org/saturnclient/ui/elements/Panel.java b/src/main/java/org/saturnclient/ui/elements/Panel.java new file mode 100644 index 0000000..2b3369e --- /dev/null +++ b/src/main/java/org/saturnclient/ui/elements/Panel.java @@ -0,0 +1,22 @@ +package org.saturnclient.ui.elements; + +import org.saturnclient.config.Theme; +import org.saturnclient.ui.Element; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; + +/** + * The translucent, outlined surface that screens lay their content on. + */ +public class Panel extends Element { + + @Override + public void render(RenderScope renderScope, ElementContext ctx) { + drawBackground(renderScope, width, height); + } + + public static void drawBackground(RenderScope renderScope, int width, int height) { + renderScope.drawRoundedRectangle(0, 0, width, height, Theme.BG_RADIUS.value, 1, + Theme.getPanelBg(), Theme.getBorder()); + } +} diff --git a/src/main/java/org/saturnclient/ui/elements/Scroll.java b/src/main/java/org/saturnclient/ui/elements/Scroll.java index 31daaa3..4726555 100644 --- a/src/main/java/org/saturnclient/ui/elements/Scroll.java +++ b/src/main/java/org/saturnclient/ui/elements/Scroll.java @@ -4,23 +4,40 @@ import java.util.ArrayList; import java.util.List; import org.saturnclient.config.Theme; -import org.saturnclient.config.property.IntProperty; -import org.saturnclient.config.property.Property; import org.saturnclient.ui.Element; import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.ElementRenderer; import org.saturnclient.ui.RenderScope; +/** + * A vertically scrolling container. It scrolls with the mouse wheel or by + * dragging its scrollbar, which shows along its right edge when the children + * don't fit. + */ public class Scroll extends Element { - private static IntProperty scrollBarRadius = Property.integer(10); - private static IntProperty scrollBarWidth = Property.integer(5); - private static IntProperty scrollBarPadding = Property.integer(5); + /** Units scrolled per mouse wheel notch */ + private static final double SCROLL_SPEED = 24; + /** Space left below the last child when scrolled to the end */ + private static final int END_SPACE = 10; + + private static final int BAR_WIDTH = 5; + private static final int BAR_INSET = 5; + private static final int MIN_THUMB = 20; + + /** Width to keep clear at the right of the content for the scrollbar and its click area */ + public static final int SCROLLBAR_SPACE = BAR_WIDTH + BAR_INSET * 2 + 4; int padding = 0; + boolean background = true; protected List children = new ArrayList<>(); - int scroll = 0; - int maxScroll = 0; + + /** Kept fractional so small trackpad deltas add up instead of being dropped */ + private double scroll = 0; + private int maxScroll = 0; + + /** Where in the thumb it was grabbed, while it's being dragged */ + private Integer thumbGrab = null; public Scroll(int padding) { this.padding = padding; @@ -30,84 +47,57 @@ public class Scroll extends Element { ElementRenderer.draw(children, element); } + /** + * Skips the scroll's own background, for when it sits on a {@link Panel}. + */ + public Scroll noBackground() { + this.background = false; + return this; + } + + /** + * Removes every child and scrolls back to the top. + */ + public void clear() { + children.clear(); + scroll = 0; + maxScroll = 0; + } + @Override public void render(RenderScope renderScope, ElementContext ctx) { - calculateMaxScroll(); + updateMaxScroll(); + int offset = getOffset(); - renderScope.drawRoundedRectangle(0, 0, width, height, - Theme.BG_RADIUS.value, Theme.BACKGROUND.value); + if (background) { + renderScope.drawRoundedRectangle(0, 0, width, height, + Theme.BG_RADIUS.value, Theme.BACKGROUND.value); + } renderScope.enableScissor(padding, padding, width - padding, height - padding); renderScope.getMatrixStack().push(); - renderScope.getMatrixStack().translate(padding, -scroll + padding); + renderScope.getMatrixStack().translate(padding, -offset + padding); ElementRenderer.render(children, ctx.elapsed, renderScope, ctx.mouseX - padding, - ctx.mouseY - padding + scroll); + ctx.mouseY - padding + offset, + ctx.childFrame(padding, padding - offset, padding, height - padding)); renderScope.getMatrixStack().pop(); renderScope.disableScissor(); if (maxScroll > 0) { - renderScope.drawRoundedRectangle( - width - scrollBarWidth.value - scrollBarPadding.value, - calculateScrollBarY(), - scrollBarWidth.value, - calculateScrollBarHeight(), - scrollBarRadius.value, - Theme.SCROLL.value); + boolean active = thumbGrab != null || ctx.isHovering(getBarX() - BAR_INSET, 0, BAR_WIDTH + BAR_INSET * 2, + height); + + renderScope.drawRoundedRectangle(getBarX(), getThumbY(), BAR_WIDTH, getThumbHeight(), BAR_WIDTH / 2, + active ? Theme.SCROLL.value : Theme.withAlpha(0.7f, Theme.SCROLL.value)); } } @Override public void scroll(int mouseX, int mouseY, double horizontalAmount, double verticalAmount) { - scroll -= verticalAmount; - if (scroll < 0) { - scroll = 0; - } else if (scroll > maxScroll) { - scroll = maxScroll; - } - } - - @Override - public Element dimensions(int width, int height) { - super.dimensions(width, height); - - maxScroll = 0; - for (Element element : children) { - maxScroll = Math.max(maxScroll, (element.y + element.height + padding) - (height - padding * 2)); - } - - return this; - } - - int calculateScrollBarHeight() { - if (maxScroll <= 0) - return height - (scrollBarPadding.value * 2); - return Math.max(20, (height * height) / (height + maxScroll + 10)) - (scrollBarPadding.value * 2); - } - - int calculateScrollBarY() { - int scrollBarHeight = calculateScrollBarHeight(); - if (maxScroll <= 0) - return scrollBarPadding.value; - int y = (scroll * (height - scrollBarHeight - scrollBarPadding.value * 2)) / maxScroll + scrollBarPadding.value; - return Math.min(y, height - scrollBarHeight - scrollBarPadding.value); - } - - private void calculateMaxScroll() { - maxScroll = 0; - - for (Element element : children) { - int bottom = element.y + element.height; - maxScroll = Math.max(maxScroll, bottom - height + padding); - } - - if (maxScroll > 0) { - maxScroll = maxScroll + 10; - } else { - maxScroll = 0; - } + setScroll(scroll - verticalAmount * SCROLL_SPEED); } @Override @@ -117,17 +107,89 @@ public class Scroll extends Element { @Override public void mouseClicked(double mouseX, double mouseY, int button) { - ElementRenderer.mouseClicked(children, mouseX - padding, mouseY - padding + scroll, button); + // Children scrolled out of view still have positions under the rest of the + // screen, so only clicks inside the scroll's own bounds may reach them. + if (mouseX < 0 || mouseY < 0 || mouseX > width || mouseY > height) { + for (Element child : children) { + child.focused = false; + } + return; + } + + if (maxScroll > 0 && mouseX >= getBarX() - BAR_INSET) { + int thumbY = getThumbY(); + int thumbHeight = getThumbHeight(); + + // Grab the thumb where it was clicked, or center it on a click in the track. + boolean onThumb = mouseY >= thumbY && mouseY <= thumbY + thumbHeight; + thumbGrab = onThumb ? (int) mouseY - thumbY : thumbHeight / 2; + dragThumb(mouseY); + return; + } + + ElementRenderer.mouseClicked(children, mouseX - padding, mouseY - padding + getOffset(), button); } @Override public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { - ElementRenderer.mouseDragged(children, mouseX - padding, mouseY - padding + scroll, button, deltaX, + if (thumbGrab != null) { + dragThumb(mouseY); + return; + } + + ElementRenderer.mouseDragged(children, mouseX - padding, mouseY - padding + getOffset(), button, deltaX, deltaY); } @Override public void mouseReleased(double mouseX, double mouseY, int button) { - ElementRenderer.mouseReleased(children, mouseX - padding, mouseY - padding + scroll, button); + thumbGrab = null; + ElementRenderer.mouseReleased(children, mouseX - padding, mouseY - padding + getOffset(), button); + } + + private void dragThumb(double mouseY) { + int travel = getTrackHeight() - getThumbHeight(); + if (travel > 0) { + setScroll((mouseY - thumbGrab - BAR_INSET) / travel * maxScroll); + } + } + + private void setScroll(double value) { + scroll = Math.max(0, Math.min(maxScroll, value)); + } + + private int getOffset() { + return (int) Math.round(scroll); + } + + private void updateMaxScroll() { + int bottom = 0; + for (Element element : children) { + bottom = Math.max(bottom, element.y + element.height); + } + + int overflow = bottom + padding * 2 - height; + maxScroll = overflow > 0 ? overflow + END_SPACE : 0; + + // The content may have shrunk, such as after filtering. + setScroll(scroll); + } + + private int getBarX() { + return width - BAR_WIDTH - BAR_INSET; + } + + private int getTrackHeight() { + return height - BAR_INSET * 2; + } + + private int getThumbHeight() { + int track = getTrackHeight(); + return Math.max(MIN_THUMB, (int) ((long) track * height / (height + maxScroll))); + } + + private int getThumbY() { + int travel = getTrackHeight() - getThumbHeight(); + return BAR_INSET + (int) (travel * (scroll / maxScroll)); } } diff --git a/src/main/java/org/saturnclient/ui/elements/SwitchButton.java b/src/main/java/org/saturnclient/ui/elements/SwitchButton.java new file mode 100644 index 0000000..a5bb6ba --- /dev/null +++ b/src/main/java/org/saturnclient/ui/elements/SwitchButton.java @@ -0,0 +1,59 @@ +package org.saturnclient.ui.elements; + +import java.util.function.BooleanSupplier; + +import org.saturnclient.config.Theme; +import org.saturnclient.ui.Element; +import org.saturnclient.ui.ElementContext; +import org.saturnclient.ui.RenderScope; +import org.saturnclient.ui.components.inputs.Toggle; +import org.saturnclient.ui.resources.Fonts; + +/** + * A labelled on/off switch ("Enabled" / "Disabled") that tints itself with the + * accent color while on. + */ +public class SwitchButton extends Element { + private static final int HEIGHT = 34; + private static final int PADDING_LEFT = 14; + private static final int PADDING_RIGHT = 5; + private static final int GAP = 10; + private static final float TEXT_SCALE = 0.66f; + + private final String onText; + private final String offText; + private final BooleanSupplier value; + private final Runnable onToggle; + + public SwitchButton(String onText, String offText, BooleanSupplier value, Runnable onToggle) { + this.onText = onText; + this.offText = offText; + this.value = value; + this.onToggle = onToggle; + this.height = HEIGHT; + + // Sized for the longer label, so the button doesn't change width when toggled + int textWidth = Math.max(Fonts.getWidth(onText, Theme.FONT.value), Fonts.getWidth(offText, Theme.FONT.value)); + this.width = PADDING_LEFT + (int) (textWidth * TEXT_SCALE) + GAP + Toggle.SWITCH_WIDTH + PADDING_RIGHT; + } + + @Override + public void render(RenderScope renderScope, ElementContext ctx) { + boolean on = value.getAsBoolean(); + float tint = ctx.isHovering() ? 0.1f : 0.06f; + + renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, + on ? Theme.withAlpha(tint * 2.5f, Theme.ACCENT.value) : Theme.withAlpha(tint, Theme.FOREGROUND.value)); + + renderScope.drawText(TEXT_SCALE, on ? onText : offText, PADDING_LEFT, Field.textY(height, TEXT_SCALE), + Theme.FONT.value, on ? Theme.ACCENT.value : Theme.PRIMARY_FG.value); + + Toggle.drawSwitch(renderScope, width - PADDING_RIGHT - Toggle.SWITCH_WIDTH, + (height - Toggle.SWITCH_HEIGHT) / 2, on); + } + + @Override + public void click(int mouseX, int mouseY) { + onToggle.run(); + } +} diff --git a/src/main/java/org/saturnclient/ui/elements/TabMenu.java b/src/main/java/org/saturnclient/ui/elements/TabMenu.java deleted file mode 100644 index 6998d11..0000000 --- a/src/main/java/org/saturnclient/ui/elements/TabMenu.java +++ /dev/null @@ -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++; - } - } -} diff --git a/src/main/java/org/saturnclient/ui/resources/Fonts.java b/src/main/java/org/saturnclient/ui/resources/Fonts.java index 4c66770..def6d4b 100644 --- a/src/main/java/org/saturnclient/ui/resources/Fonts.java +++ b/src/main/java/org/saturnclient/ui/resources/Fonts.java @@ -44,6 +44,24 @@ public class Fonts { return 18; } + /** + * Shortens {@code text} with a trailing "…" so it fits in {@code maxWidth} + * when drawn at {@code scale}. + */ + public static String ellipsize(String text, int font, float scale, int maxWidth) { + if (getWidth(text, font) * scale <= maxWidth) { + return text; + } + + String ellipsis = "…"; + int end = text.length(); + while (end > 0 && getWidth(text.substring(0, end).stripTrailing() + ellipsis, font) * scale > maxWidth) { + end--; + } + + return text.substring(0, end).stripTrailing() + ellipsis; + } + public static int centerX(int w, String text, int font) { return (w - Fonts.getWidth(text, font)) / 2; } diff --git a/src/main/java/org/saturnclient/ui/resources/Textures.java b/src/main/java/org/saturnclient/ui/resources/Textures.java index 6058e95..8372396 100644 --- a/src/main/java/org/saturnclient/ui/resources/Textures.java +++ b/src/main/java/org/saturnclient/ui/resources/Textures.java @@ -25,6 +25,26 @@ public class Textures { public static final IdentifierRef INFO = IdentifierRef.ofSaturn("textures/gui/components/info.png"); public static final IdentifierRef CHECK = IdentifierRef.ofSaturn("textures/gui/components/check.png"); + // Lucide icons (SVG, drawn in white so they can be tinted) + public static final IdentifierRef ICON_MODS = IdentifierRef.ofSaturn("textures/gui/components/mods.svg"); + public static final IdentifierRef ICON_COSMETICS = IdentifierRef.ofSaturn("textures/gui/components/cosmetics.svg"); + public static final IdentifierRef ICON_HUD_EDITOR = IdentifierRef.ofSaturn("textures/gui/components/hud_editor.svg"); + public static final IdentifierRef ICON_SETTINGS = IdentifierRef.ofSaturn("textures/gui/components/settings.svg"); + public static final IdentifierRef ICON_CLOSE = IdentifierRef.ofSaturn("textures/gui/components/close.svg"); + public static final IdentifierRef ICON_SEARCH = IdentifierRef.ofSaturn("textures/gui/components/search.svg"); + public static final IdentifierRef ICON_SLIDERS = IdentifierRef.ofSaturn("textures/gui/components/sliders.svg"); + public static final IdentifierRef ICON_PALETTE = IdentifierRef.ofSaturn("textures/gui/components/palette.svg"); + public static final IdentifierRef ICON_ANIMATIONS = IdentifierRef + .ofSaturn("textures/gui/components/animations.svg"); + public static final IdentifierRef ICON_RESET = IdentifierRef.ofSaturn("textures/gui/components/reset.svg"); + public static final IdentifierRef ICON_CHEVRON_LEFT = IdentifierRef + .ofSaturn("textures/gui/components/chevron_left.svg"); + public static final IdentifierRef ICON_CHEVRON_RIGHT = IdentifierRef + .ofSaturn("textures/gui/components/chevron_right.svg"); + public static final IdentifierRef ICON_CHEVRON_DOWN = IdentifierRef + .ofSaturn("textures/gui/components/chevron_down.svg"); + public static final IdentifierRef ICON_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"); public static final IdentifierRef LOGO_TEXT = IdentifierRef.ofSaturn("textures/logo/text.png"); diff --git a/src/main/java/org/saturnclient/ui/screens/ClientSettings.java b/src/main/java/org/saturnclient/ui/screens/ClientSettings.java new file mode 100644 index 0000000..e6cfbb3 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/screens/ClientSettings.java @@ -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 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 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 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 getNames(Map properties, Predicate 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 properties = switch (section) { + case GENERAL -> getGeneralProperties(); + case UI_STYLE -> Theme.config.getProperties(); + case ANIMATIONS -> AnimationConfig.config.getProperties(); + }; + + properties.values().forEach(Property::reset); + init(); + } +} diff --git a/src/main/java/org/saturnclient/ui/screens/ConfigEditor.java b/src/main/java/org/saturnclient/ui/screens/ConfigEditor.java deleted file mode 100644 index e21245a..0000000 --- a/src/main/java/org/saturnclient/ui/screens/ConfigEditor.java +++ /dev/null @@ -1,173 +0,0 @@ -package org.saturnclient.ui.screens; - -import java.util.Map; - -import org.saturnclient.config.ConfigManager; -import org.saturnclient.config.Theme; -import org.saturnclient.config.property.BoolProperty; -import org.saturnclient.config.property.ColorProperty; -import org.saturnclient.config.property.FloatProperty; -import org.saturnclient.config.property.IntProperty; -import org.saturnclient.config.property.KeybindingProperty; -import org.saturnclient.config.property.NamespaceProperty; -import org.saturnclient.config.property.Property; -import org.saturnclient.config.property.SelectProperty; -import org.saturnclient.mod.HudMod; -import org.saturnclient.mod.ModLayout; -import org.saturnclient.ui.RenderScope; -import org.saturnclient.ui.SaturnScreen; -import org.saturnclient.ui.components.HudModPreview; -import org.saturnclient.ui.components.Sidebar; -import org.saturnclient.ui.components.inputs.FloatInput; -import org.saturnclient.ui.components.inputs.ColorInput; -import org.saturnclient.ui.components.inputs.IntInput; -import org.saturnclient.ui.components.inputs.KeybindingSelector; -import org.saturnclient.ui.components.inputs.Select; -import org.saturnclient.ui.components.inputs.Toggle; -import org.saturnclient.ui.elements.Scroll; -import org.saturnclient.ui.elements.Text; -import org.saturnclient.ui.resources.Fonts; - -public class ConfigEditor extends SaturnScreen { - private ConfigManager config; - private HudMod hudMod; - private HudModPreview preview; - - public ConfigEditor(ConfigManager config) { - super("Config Editor"); - - this.config = config; - } - - public ConfigEditor(ConfigManager config, HudMod hudMod) { - super("Config Editor"); - - this.config = config; - this.hudMod = hudMod; - } - - @Override - public void ui() { - int p = 10; - int g = 10; - - Scroll scroll = new Scroll(p); - - drawProperties(scroll, config.getProperties(), 0, 0, 480 + 10 + (g * 2)); - - int scrollWidth = 480 + 10 + (g * 2) + (p * 2); - - if (this.hudMod != null) { - this.preview = new HudModPreview(this.hudMod); - } - - draw(scroll.dimensions(scrollWidth, 350).center(width, height)); - - draw(new Sidebar(1, this.provider::close).center(width, height, -(scrollWidth / 2 + 20), 0)); - } - - private int drawProperties(Scroll configScroll, Map properties, int row, int col, int w) { - for (Map.Entry propEntry : properties.entrySet()) { - Property prop = propEntry.getValue(); - String propName = propEntry.getKey(); - boolean full = isFull(prop); - - // Always reset to col 0 if full-width component needs to be drawn mid-row - if (full && col != 0) { - col = 0; - row++; - } - - int modX = (w / 2) * col; // two-column layout - int modY = 25 * row; - - if (prop instanceof NamespaceProperty) { - configScroll.draw( - new Text(propName) - .position(Fonts.centerX(520, propName, Theme.FONT.value), modY + 1) - .scale(0.7f)); - } else { - configScroll.draw( - new Text(propName) - .position(modX, modY + 1) - .scale(0.7f)); - } - - // instanceof dispatch - if (prop instanceof BoolProperty p) { - configScroll.draw( - new Toggle(p) - .position(modX + (w / 2) - 40, modY) - .scale(0.5f)); - } - - else if (prop instanceof ColorProperty p) { - configScroll.draw( - new ColorInput(p) - .position(w / 2, modY)); - } - - else if (prop instanceof IntProperty p) { - configScroll.draw( - new IntInput(p) - .position(w / 2, modY)); - } - - else if (prop instanceof FloatProperty p) { - configScroll.draw( - new FloatInput(p) - .position(w / 2, modY)); - } - - else if (prop instanceof NamespaceProperty p) { - Map nestedProperties = p.value; - row = drawProperties(configScroll, nestedProperties, row + 1, col, w); - } - - else if (prop instanceof SelectProperty p) { - configScroll.draw( - new Select(p) - .position(w / 2, modY)); - } - - else if (prop instanceof KeybindingProperty p) { - configScroll.draw( - new KeybindingSelector(p) - .position(w / 2, modY)); - } - - // STRING → intentionally no component (same as before) - - if (full) { - row++; // full-width takes a whole row - col = 0; - } else { - col++; - if (col > 1) { // max 2 columns - col = 0; - row++; - } - } - } - - return row; - } - - public static boolean isFull(Property prop) { - return !(prop instanceof BoolProperty); - } - - @Override - public void render(RenderScope renderScope, int mouseX, int mouseY, float delta, long elapsed) { - if (this.preview != null) { - ModLayout layout = hudMod.getDimensions(); - draw(this.preview - .dimensions(layout.width, (int) (layout.height * layout.scale.value * 2.0f)) - .scale(1.0f) // Because the centering won't work properly - .center(width, height, (530 / 2) + 40, 0) - .scale(layout.scale.value * 2.0f)); - } - - super.render(renderScope, mouseX, mouseY, delta, elapsed); - } -} diff --git a/src/main/java/org/saturnclient/ui/screens/CosmeticsMenu.java b/src/main/java/org/saturnclient/ui/screens/CosmeticsMenu.java new file mode 100644 index 0000000..8fa0556 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/screens/CosmeticsMenu.java @@ -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> owned; + final Function equipped; + final Consumer equip; + final Consumer claim; + final Function 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> owned, + Function equipped, Consumer equip, Consumer claim, + Function 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 ids() { + List 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 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 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()); + } + } +} diff --git a/src/main/java/org/saturnclient/ui/screens/ModMenu.java b/src/main/java/org/saturnclient/ui/screens/ModMenu.java index a531eca..05f433a 100644 --- a/src/main/java/org/saturnclient/ui/screens/ModMenu.java +++ b/src/main/java/org/saturnclient/ui/screens/ModMenu.java @@ -1,56 +1,147 @@ package org.saturnclient.ui.screens; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + import org.saturnclient.config.AnimationConfig; +import org.saturnclient.config.Theme; import org.saturnclient.mod.Mod; import org.saturnclient.mod.ModManager; -import org.saturnclient.ui.SaturnScreen; +import org.saturnclient.ui.PanelScreen; import org.saturnclient.ui.anim.SlideFade; import org.saturnclient.ui.components.SaturnModule; import org.saturnclient.ui.components.Sidebar; +import org.saturnclient.ui.components.inputs.SearchInput; import org.saturnclient.ui.elements.AnimationStagger; -import org.saturnclient.ui.elements.ImageTexture; +import org.saturnclient.ui.elements.ChipGroup; +import org.saturnclient.ui.elements.Label; import org.saturnclient.ui.elements.Scroll; -import org.saturnclient.ui.resources.Textures; +import org.saturnclient.ui.resources.Fonts; + +public class ModMenu extends PanelScreen { + private static final String ALL = "All"; + + private static final int COLUMNS = 3; + private static final int CARD_GAP = 10; + + private String category = ALL; + private String query = ""; + + private Scroll scroll; + private int gridWidth; -public class ModMenu extends SaturnScreen { public ModMenu() { - super("Mod Menu"); + super("Mod Menu", Sidebar.MODS); } @Override - public void ui() { - int p = 10; - int g = 10; + protected void panel() { + // The search box starts empty after a resize, so the query does too. + query = ""; + gridWidth = scrollContentWidth(contentWidth); - Scroll scroll = new Scroll(p); + // Header + draw(new Label(() -> "Mods", 1.15f, () -> Theme.FONT.value, () -> Theme.FOREGROUND.value) + .position(contentX, contentY + 2)); - int row = 0; - int col = 0; + draw(new Label(this::getEnabledSummary, 0.66f, Theme::getBodyFont, Theme::getMutedFg) + .position(contentX, contentY + 28)); - AnimationStagger moduleStagger = new AnimationStagger(AnimationConfig.modMenu); + SearchInput search = new SearchInput("Search mods", text -> { + query = text; + refresh(false); + }); + draw(search.position(contentX + contentWidth - search.width, contentY + 2)); + + // Categories + int chipsY = contentY + 56; + ChipGroup categories = new ChipGroup(getCategories(), category, selected -> { + category = selected; + refresh(false); + }); + draw(categories.position(contentX, chipsY)); + + // Mods + int scrollY = chipsY + categories.height + 14; + scroll = new Scroll(0).noBackground(); + scroll.dimensions(scrollWidth(contentWidth), panelY + panelHeight - 12 - scrollY); + refresh(true); + draw(scroll.position(contentX, scrollY)); + } + + /** + * Fills the grid with the mods that match the category and search query. + */ + private void refresh(boolean animate) { + scroll.clear(); + + List mods = ModManager.MODS.stream().filter(this::matches).toList(); + + if (mods.isEmpty()) { + String message = "No mods match “" + query + "”"; + float scale = 0.7f; + int x = (gridWidth - (int) (Fonts.getWidth(message, Theme.getBodyFont()) * scale)) / 2; + + scroll.draw(new Label(() -> message, scale, Theme::getBodyFont, Theme::getMutedFg).position(x, 60)); + return; + } + + int cardWidth = (gridWidth - CARD_GAP * (COLUMNS - 1)) / COLUMNS; + AnimationStagger grid = new AnimationStagger(AnimationConfig.modMenu); + + for (int i = 0; i < mods.size(); i++) { + int col = i % COLUMNS; + int row = i / COLUMNS; + + SaturnModule card = new SaturnModule(mods.get(i), cardWidth); + card.position((cardWidth + CARD_GAP) * col, (SaturnModule.HEIGHT + CARD_GAP) * row); + + if (animate) { + card.animation(new SlideFade(AnimationConfig.modMenu, -12)); + } + + grid.draw(card); + } + + scroll.draw(grid); + } + + private boolean matches(Mod mod) { + if (!category.equals(ALL) && !List.of(mod.getTags()).contains(category)) { + return false; + } + + String q = query.trim().toLowerCase(Locale.ROOT); + if (q.isEmpty()) { + return true; + } + + String description = mod.getDescription() == null ? "" : mod.getDescription(); + return mod.getName().toLowerCase(Locale.ROOT).contains(q) + || description.toLowerCase(Locale.ROOT).contains(q); + } + + /** + * "All", then every mod tag in the order it first appears. + */ + private static List getCategories() { + List categories = new ArrayList<>(); + categories.add(ALL); for (Mod mod : ModManager.MODS) { - moduleStagger.draw( - new SaturnModule(mod) - .position((160 + g) * col, (50 + g) * row) - .animation(new SlideFade(AnimationConfig.modMenu, -12))); - - if (col == 2) { - col = 0; - row++; - } else { - col++; + for (String tag : mod.getTags()) { + if (!categories.contains(tag)) { + categories.add(tag); + } } } - scroll.draw(moduleStagger); + return categories; + } - int scrollWidth = 480 + 10 + (g * 2) + (p * 2); - - draw(scroll.dimensions(scrollWidth, 350).center(width, height)); - - draw(new Sidebar(0, this.provider::close).center(width, height, -(scrollWidth / 2 + 20), 0)); - - draw(new ImageTexture(Textures.LOGO_TEXT_BIG).dimensions(180, 18).position(width - 180 - 20, height - 18 - 20)); + private String getEnabledSummary() { + long enabled = ModManager.MODS.stream().filter(Mod::isEnabled).count(); + return enabled + " of " + ModManager.MODS.size() + " enabled"; } } diff --git a/src/main/java/org/saturnclient/ui/screens/ModSettings.java b/src/main/java/org/saturnclient/ui/screens/ModSettings.java new file mode 100644 index 0000000..f6b096a --- /dev/null +++ b/src/main/java/org/saturnclient/ui/screens/ModSettings.java @@ -0,0 +1,97 @@ +package org.saturnclient.ui.screens; + +import java.util.Map; +import java.util.Set; + +import org.saturnclient.common.provider.Providers; +import org.saturnclient.config.property.Property; +import org.saturnclient.mod.HudMod; +import org.saturnclient.mod.Mod; +import org.saturnclient.ui.PanelScreen; +import org.saturnclient.ui.components.HudPreviewCard; +import org.saturnclient.ui.components.ModHeader; +import org.saturnclient.ui.components.PropertyList; +import org.saturnclient.ui.components.Sidebar; +import org.saturnclient.ui.elements.ActionButton; +import org.saturnclient.ui.elements.Divider; +import org.saturnclient.ui.elements.Scroll; +import org.saturnclient.ui.elements.SwitchButton; +import org.saturnclient.ui.resources.Textures; + +/** + * One mod's settings: a header with the mod's details and its on/off switch, + * the rest of its properties below, and a live preview for HUD mods. + */ +public class ModSettings extends PanelScreen { + /** + * Every mod's first property. It's shown as the header's switch, so the list + * leaves it out. + */ + private static final String ENABLED = "Enabled"; + + private static final int HEADER_Y = 40; + private static final int PREVIEW_WIDTH = 280; + private static final int PREVIEW_MAX_HEIGHT = 220; + private static final int COLUMN_GAP = 22; + + private final Mod mod; + + public ModSettings(Mod mod) { + super(mod.getName() + " Settings", Sidebar.MODS); + this.mod = mod; + } + + @Override + protected void panel() { + draw(new ActionButton(Textures.ICON_CHEVRON_LEFT, "Mods", false, + () -> Providers.saturn.getClient().setScreen(new ModMenu())) + .position(contentX - 12, contentY - 4)); + + // Header: the mod's details, then Reset and the on/off switch on the right + int headerY = contentY + HEADER_Y; + + SwitchButton enabled = new SwitchButton("Enabled", "Disabled", mod::isEnabled, + () -> mod.setEnabled(!mod.isEnabled())); + int buttonY = headerY + (ModHeader.HEIGHT - enabled.height) / 2; + draw(enabled.position(contentX + contentWidth - enabled.width, buttonY)); + + ActionButton reset = new ActionButton(Textures.ICON_RESET, "Reset", true, this::reset); + draw(reset.position(enabled.x - 10 - reset.width, buttonY)); + + draw(new ModHeader(mod, reset.x - 16 - contentX).position(contentX, headerY)); + + int dividerY = headerY + ModHeader.HEIGHT + 16; + draw(new Divider(contentWidth).position(contentX, dividerY)); + + // Body: the properties, with a preview beside them for HUD mods + int bodyY = dividerY + 1; + int bodyHeight = contentY + contentHeight - bodyY; + int listWidth = contentWidth; + + if (mod instanceof HudMod hudMod) { + listWidth -= PREVIEW_WIDTH + COLUMN_GAP; + draw(new HudPreviewCard(hudMod) + .dimensions(PREVIEW_WIDTH, Math.min(PREVIEW_MAX_HEIGHT, bodyHeight - 16)) + .position(contentX + contentWidth - PREVIEW_WIDTH, bodyY + 16)); + } + + Scroll scroll = new Scroll(0).noBackground(); + PropertyList.draw(scroll, mod.getConfig().getProperties(), Set.of(ENABLED), scrollContentWidth(listWidth), + 0); + draw(scroll.dimensions(scrollWidth(listWidth), bodyHeight).position(contentX, bodyY)); + } + + /** + * Puts every setting except the on/off switch back to its default, then + * rebuilds the screen so the inputs show the new values. + */ + private void reset() { + for (Map.Entry entry : mod.getConfig().getProperties().entrySet()) { + if (!entry.getKey().equals(ENABLED)) { + entry.getValue().reset(); + } + } + + init(); + } +} diff --git a/src/main/java/org/saturnclient/ui/screens/ShiftMenu.java b/src/main/java/org/saturnclient/ui/screens/ShiftMenu.java index c727068..2915e68 100644 --- a/src/main/java/org/saturnclient/ui/screens/ShiftMenu.java +++ b/src/main/java/org/saturnclient/ui/screens/ShiftMenu.java @@ -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))); diff --git a/src/main/java/org/saturnclient/ui/screens/TitleMenu.java b/src/main/java/org/saturnclient/ui/screens/TitleMenu.java index a2bbace..d134334 100644 --- a/src/main/java/org/saturnclient/ui/screens/TitleMenu.java +++ b/src/main/java/org/saturnclient/ui/screens/TitleMenu.java @@ -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))); diff --git a/src/main/java/org/saturnclient/ui/screens/cosmetics/CloakMenu.java b/src/main/java/org/saturnclient/ui/screens/cosmetics/CloakMenu.java deleted file mode 100644 index db0b936..0000000 --- a/src/main/java/org/saturnclient/ui/screens/cosmetics/CloakMenu.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.saturnclient.ui.screens.cosmetics; - -import org.saturnclient.client.player.SaturnPlayer; -import org.saturnclient.common.provider.Providers; -import org.saturnclient.config.AnimationConfig; -import org.saturnclient.cosmetics.Cloaks; -import org.saturnclient.ui.SaturnScreen; -import org.saturnclient.ui.anim.Fade; -import org.saturnclient.ui.anim.SlideY; -import org.saturnclient.ui.components.CosmeticPreview; -import org.saturnclient.ui.components.Sidebar; -import org.saturnclient.ui.components.SkinPreview; -import org.saturnclient.ui.elements.AnimationStagger; -import org.saturnclient.ui.elements.Scroll; -import org.saturnclient.ui.elements.TabMenu; -import org.saturnclient.ui.elements.TabMenu.TabMenuComponent; -import org.saturnclient.ui.resources.Textures; - -public class CloakMenu extends SaturnScreen { - public CloakMenu() { - super("Cloaks Menu"); - } - - @Override - public void ui() { - int p = 10; - int g = 10; - - Scroll scroll = new Scroll(p); - - int row = 0; - int col = 0; - - SaturnPlayer player = SaturnPlayer.get(); - - AnimationStagger stagger = new AnimationStagger(AnimationConfig.cosmeticsMenu); - - if (player != null) { - for (String cloak : Cloaks.availableCloaks) { - - stagger.draw( - new CosmeticPreview( - cloak == player.cloak, - Textures.getCloakPreview(cloak), - () -> Cloaks.setCloak(cloak)) - .position((50 + g) * col, (111 + g) * row) - .animation(new SlideY(AnimationConfig.cosmeticsMenu, 14))); - - if (col == 5) { - col = 0; - row++; - } else { - col++; - } - } - } - - scroll.draw(stagger); - - int scrollWidth = 480 + 10 + (g * 2) + (p * 2); - - draw(scroll - .dimensions(scrollWidth, 350) - .center(width, height)); - - draw(new SkinPreview(-30.0f, false) - .scale(3.5f) - .position(scroll.x + (scrollWidth - 220), scroll.y + 40) - .animation(new Fade(500))); - - draw(new Sidebar(2, this.provider::close) - .center(width, height, -(scrollWidth / 2 + 20), 0)); - - draw(new TabMenu(0, - new TabMenuComponent(Textures.CLOAK, () -> { - }), - new TabMenuComponent(Textures.HAT, () -> { - Providers.saturn.getClient().setScreen(new HatMenu()); - })) - .center(width, height, 0, -195)); - } -} diff --git a/src/main/java/org/saturnclient/ui/screens/cosmetics/HatMenu.java b/src/main/java/org/saturnclient/ui/screens/cosmetics/HatMenu.java deleted file mode 100644 index 6493e23..0000000 --- a/src/main/java/org/saturnclient/ui/screens/cosmetics/HatMenu.java +++ /dev/null @@ -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(2, this.provider::close) - .center(width, height, -(scrollWidth / 2 + 20), 0)); - - draw(new TabMenu(1, - new TabMenuComponent(Textures.CLOAK, () -> { - Providers.saturn.getClient().setScreen(new CloakMenu()); - }), - new TabMenuComponent(Textures.HAT, () -> { - })) - .center(width, height, 0, -195)); - } -} diff --git a/src/main/java/org/saturnclient/ui/screens/store/CloakStore.java b/src/main/java/org/saturnclient/ui/screens/store/CloakStore.java deleted file mode 100644 index bc62890..0000000 --- a/src/main/java/org/saturnclient/ui/screens/store/CloakStore.java +++ /dev/null @@ -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(5, this.provider::close) - .center(width, height, -((scrollWidth - 30) / 2 + 20), 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"); - } - } -} diff --git a/src/main/java/org/saturnclient/ui/screens/store/HatStore.java b/src/main/java/org/saturnclient/ui/screens/store/HatStore.java deleted file mode 100644 index c2540ec..0000000 --- a/src/main/java/org/saturnclient/ui/screens/store/HatStore.java +++ /dev/null @@ -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(5, this.provider::close) - .center(width, height, -((scrollWidth - 30) / 2 + 20), 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"); - } - } -} diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/animations.svg b/src/main/resources/assets/saturnclient/textures/gui/components/animations.svg new file mode 100644 index 0000000..7c5fed6 --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/animations.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/check.svg b/src/main/resources/assets/saturnclient/textures/gui/components/check.svg new file mode 100644 index 0000000..6bb8a38 --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/check.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/chevron_down.svg b/src/main/resources/assets/saturnclient/textures/gui/components/chevron_down.svg new file mode 100644 index 0000000..7ac2e2a --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/chevron_down.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/chevron_left.svg b/src/main/resources/assets/saturnclient/textures/gui/components/chevron_left.svg new file mode 100644 index 0000000..f8492e1 --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/chevron_left.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/chevron_right.svg b/src/main/resources/assets/saturnclient/textures/gui/components/chevron_right.svg new file mode 100644 index 0000000..a8cbb0e --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/chevron_right.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/close.svg b/src/main/resources/assets/saturnclient/textures/gui/components/close.svg new file mode 100644 index 0000000..c509233 --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/close.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/cosmetics.svg b/src/main/resources/assets/saturnclient/textures/gui/components/cosmetics.svg new file mode 100644 index 0000000..8eca3bb --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/cosmetics.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/hud_editor.svg b/src/main/resources/assets/saturnclient/textures/gui/components/hud_editor.svg new file mode 100644 index 0000000..3978faf --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/hud_editor.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/mods.svg b/src/main/resources/assets/saturnclient/textures/gui/components/mods.svg new file mode 100644 index 0000000..65d3912 --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/mods.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/palette.svg b/src/main/resources/assets/saturnclient/textures/gui/components/palette.svg new file mode 100644 index 0000000..4993031 --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/palette.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/reset.svg b/src/main/resources/assets/saturnclient/textures/gui/components/reset.svg new file mode 100644 index 0000000..6d27dbc --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/reset.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/search.svg b/src/main/resources/assets/saturnclient/textures/gui/components/search.svg new file mode 100644 index 0000000..8c7fb8d --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/search.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/settings.svg b/src/main/resources/assets/saturnclient/textures/gui/components/settings.svg new file mode 100644 index 0000000..75ba448 --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/settings.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/sliders.svg b/src/main/resources/assets/saturnclient/textures/gui/components/sliders.svg new file mode 100644 index 0000000..a433070 --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/sliders.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/mod/clock.svg b/src/main/resources/assets/saturnclient/textures/gui/mod/clock.svg index c928bd6..a260617 100644 --- a/src/main/resources/assets/saturnclient/textures/gui/mod/clock.svg +++ b/src/main/resources/assets/saturnclient/textures/gui/mod/clock.svg @@ -1,7 +1 @@ - - - - - - - \ No newline at end of file + diff --git a/src/main/resources/assets/saturnclient/textures/gui/mod/crosshair.svg b/src/main/resources/assets/saturnclient/textures/gui/mod/crosshair.svg index bb0cb50..572ac84 100644 --- a/src/main/resources/assets/saturnclient/textures/gui/mod/crosshair.svg +++ b/src/main/resources/assets/saturnclient/textures/gui/mod/crosshair.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/main/resources/assets/saturnclient/textures/gui/mod/nametags.svg b/src/main/resources/assets/saturnclient/textures/gui/mod/nametags.svg index 876a0f0..233afdd 100644 --- a/src/main/resources/assets/saturnclient/textures/gui/mod/nametags.svg +++ b/src/main/resources/assets/saturnclient/textures/gui/mod/nametags.svg @@ -1,7 +1 @@ - - - - - - - \ No newline at end of file + diff --git a/src/main/resources/assets/saturnclient/textures/gui/mod/ping.svg b/src/main/resources/assets/saturnclient/textures/gui/mod/ping.svg index 6eaafc9..a230b11 100644 --- a/src/main/resources/assets/saturnclient/textures/gui/mod/ping.svg +++ b/src/main/resources/assets/saturnclient/textures/gui/mod/ping.svg @@ -1,7 +1 @@ - - - - - - - \ No newline at end of file + diff --git a/src/main/resources/assets/saturnclient/textures/gui/mod/sprint.svg b/src/main/resources/assets/saturnclient/textures/gui/mod/sprint.svg index 35e91e7..11515b0 100644 --- a/src/main/resources/assets/saturnclient/textures/gui/mod/sprint.svg +++ b/src/main/resources/assets/saturnclient/textures/gui/mod/sprint.svg @@ -1,35 +1 @@ - - - - -Created by potrace 1.15, written by Peter Selinger 2001-2017 - - - - - - + diff --git a/src/main/resources/assets/saturnclient/textures/gui/mod/tps.svg b/src/main/resources/assets/saturnclient/textures/gui/mod/tps.svg index f2ad6c9..7c5fed6 100644 --- a/src/main/resources/assets/saturnclient/textures/gui/mod/tps.svg +++ b/src/main/resources/assets/saturnclient/textures/gui/mod/tps.svg @@ -1,7 +1 @@ - - - - - - performance_line - \ No newline at end of file + diff --git a/src/main/resources/assets/saturnclient/textures/gui/mod/zoom.svg b/src/main/resources/assets/saturnclient/textures/gui/mod/zoom.svg index 320a59d..e0c3ce8 100644 --- a/src/main/resources/assets/saturnclient/textures/gui/mod/zoom.svg +++ b/src/main/resources/assets/saturnclient/textures/gui/mod/zoom.svg @@ -1 +1 @@ - \ No newline at end of file +