diff --git a/src/main/java/org/saturnclient/config/AnimationConfig.java b/src/main/java/org/saturnclient/config/AnimationConfig.java index acaadda..5451773 100644 --- a/src/main/java/org/saturnclient/config/AnimationConfig.java +++ b/src/main/java/org/saturnclient/config/AnimationConfig.java @@ -42,8 +42,8 @@ public class AnimationConfig { public final SelectProperty curve; public AnimationConfig(boolean enabled, int delay, int stagger) { - this.duration = Property.integer(delay); - this.stagger = Property.integer(stagger); + 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.select(1, "Ease In Out Cubic", "Ease Out Cubic", "Ease In Out Back"); } diff --git a/src/main/java/org/saturnclient/config/Theme.java b/src/main/java/org/saturnclient/config/Theme.java index dbfca5f..67f79e6 100644 --- a/src/main/java/org/saturnclient/config/Theme.java +++ b/src/main/java/org/saturnclient/config/Theme.java @@ -32,8 +32,8 @@ 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); diff --git a/src/main/java/org/saturnclient/config/property/FloatProperty.java b/src/main/java/org/saturnclient/config/property/FloatProperty.java index 316e90c..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,36 @@ public class FloatProperty extends Property { public float value; public final float defaultValue; + /** Bounds for a slider, when the setting has a sensible range */ + public Float min, max; + public float step = 0.1f; + /** Shown after the value, like "×" */ + public String unit = ""; + public FloatProperty(float defaultValue) { this.value = defaultValue; this.defaultValue = defaultValue; } + /** + * Gives the setting a range, so it's edited with a slider. + */ + public FloatProperty range(float min, float max, float step) { + this.min = min; + this.max = max; + this.step = step; + return this; + } + + public FloatProperty unit(String unit) { + this.unit = unit; + return this; + } + + public boolean hasRange() { + return min != null && max != null; + } + @Override public void reset() { this.value = defaultValue; diff --git a/src/main/java/org/saturnclient/config/property/IntProperty.java b/src/main/java/org/saturnclient/config/property/IntProperty.java index 66d1a84..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,40 @@ public class IntProperty extends Property { public int value; public final int defaultValue; + /** Bounds for a slider, when the setting has a sensible range */ + public Integer min, max; + public int step = 1; + /** Shown after the value, like "ms" */ + public String unit = ""; + public IntProperty(int defaultValue) { this.value = defaultValue; this.defaultValue = defaultValue; } + /** + * Gives the setting a range, so it's edited with a slider. + */ + public IntProperty range(int min, int max) { + return range(min, max, 1); + } + + public IntProperty range(int min, int max, int step) { + this.min = min; + this.max = max; + this.step = step; + return this; + } + + public IntProperty unit(String unit) { + this.unit = unit; + return this; + } + + public boolean hasRange() { + return min != null && max != null; + } + @Override public void reset() { this.value = defaultValue; diff --git a/src/main/java/org/saturnclient/mod/ModLayout.java b/src/main/java/org/saturnclient/mod/ModLayout.java index e7df485..2f79c30 100644 --- a/src/main/java/org/saturnclient/mod/ModLayout.java +++ b/src/main/java/org/saturnclient/mod/ModLayout.java @@ -13,10 +13,10 @@ import org.saturnclient.config.property.NamedProperty; public class ModLayout { public IntProperty x = Property.integer(0); public IntProperty y = Property.integer(0); - public FloatProperty scale = Property.floatProp(1.0f); + public FloatProperty scale = Property.floatProp(1.0f).range(0.5f, 4f, 0.1f).unit("×"); public ColorProperty bgColor = Property.color(0x00000000); public ColorProperty fgColor = Property.color(0xFFffffff); - public IntProperty radius = Property.integer(0); + public IntProperty radius = Property.integer(0).range(0, 20); public SelectProperty font = Property.font(0); public boolean renderBackground = true; diff --git a/src/main/java/org/saturnclient/mod/mods/FreelookMod.java b/src/main/java/org/saturnclient/mod/mods/FreelookMod.java index 53b6067..46f75d1 100644 --- a/src/main/java/org/saturnclient/mod/mods/FreelookMod.java +++ b/src/main/java/org/saturnclient/mod/mods/FreelookMod.java @@ -22,9 +22,9 @@ 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 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 SelectProperty curve = Property.select(1, "Ease In Out Cubic", "Ease Out Cubic", "Ease In Out Back"); // --- State Variables --- diff --git a/src/main/java/org/saturnclient/mod/mods/FullbrightMod.java b/src/main/java/org/saturnclient/mod/mods/FullbrightMod.java index 9b34c5b..1ca00d9 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( diff --git a/src/main/java/org/saturnclient/mod/mods/ZoomMod.java b/src/main/java/org/saturnclient/mod/mods/ZoomMod.java index 92a66c8..f2f899f 100644 --- a/src/main/java/org/saturnclient/mod/mods/ZoomMod.java +++ b/src/main/java/org/saturnclient/mod/mods/ZoomMod.java @@ -18,9 +18,9 @@ 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 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 SelectProperty curve = Property.select(1, "Ease In Out Cubic", "Ease Out Cubic", "Ease In Out Back"); diff --git a/src/main/java/org/saturnclient/ui/components/PropertyList.java b/src/main/java/org/saturnclient/ui/components/PropertyList.java index 91e1a39..96b49b9 100644 --- a/src/main/java/org/saturnclient/ui/components/PropertyList.java +++ b/src/main/java/org/saturnclient/ui/components/PropertyList.java @@ -23,6 +23,7 @@ import org.saturnclient.ui.components.inputs.FloatInput; import org.saturnclient.ui.components.inputs.IntInput; import org.saturnclient.ui.components.inputs.KeybindingSelector; import org.saturnclient.ui.components.inputs.Select; +import org.saturnclient.ui.components.inputs.Slider; import org.saturnclient.ui.components.inputs.Toggle; import org.saturnclient.ui.elements.Label; import org.saturnclient.ui.elements.Scroll; @@ -104,9 +105,9 @@ public final class PropertyList { } else if (prop instanceof ColorProperty p) { return new ColorInput(p); } else if (prop instanceof IntProperty p) { - return new IntInput(p); + return p.hasRange() ? Slider.of(p) : new IntInput(p); } else if (prop instanceof FloatProperty p) { - return new FloatInput(p); + return p.hasRange() ? Slider.of(p) : new FloatInput(p); } else if (prop instanceof SelectProperty p) { return new Select(p); } else if (prop instanceof KeybindingProperty p) { 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..b9f24ab --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/inputs/Slider.java @@ -0,0 +1,134 @@ +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 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; + + private Slider(double min, double max, double step, String unit, DoubleSupplier value, DoubleConsumer onChange) { + this.min = min; + this.max = max; + this.step = step; + this.unit = unit; + this.value = value; + this.onChange = onChange; + this.decimals = Math.max(0, BigDecimal.valueOf(step).stripTrailingZeros().scale()); + this.width = 280; + this.height = Field.HEIGHT; + } + + public static Slider of(IntProperty prop) { + return new Slider(prop.min, prop.max, 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, 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; + } +}