Edit ranged settings with sliders

IntProperty and FloatProperty can take an optional range, step and unit.
PropertyList shows those settings as a Slider: a track to click or drag,
snapping to the step, with the value and unit beside it. Zoom and freelook
levels and durations, fullbright brightness, the theme radii, animation
timings and HUD scale and radius get ranges; the rest stay number boxes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-27 17:39:27 +02:00
co-authored by claude
parent 7264c7b38b
commit 06a90d8e51
10 changed files with 204 additions and 15 deletions
@@ -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;
}
}