Redesign mod settings

A mod's settings button now opens ModSettings: a header with the mod's
icon, name, version, tags and description, a Reset button and an on/off
switch, then its properties as rows, with nested namespaces as titled
sections and a live preview card beside them for HUD mods.

PanelScreen holds the sidebar-and-panel frame that the mod menu, mod
settings and config editor share. PropertyList lays out any config as
rows. The inputs share one field look: Toggle is a switch, Select steps
with arrows, and the number, keybinding and color inputs are boxes. The
color picker opens to the left and closes on outside clicks.

Every property type now implements reset(), which was a no-op, and
ModLayout keeps its settings in order instead of Map.of's. ConfigEditor
moves onto the same frame and rows and drops its HUD preview, which added
a new element every frame.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-09-27 17:13:39 +02:00
co-authored by claude
parent dfebd2367d
commit df09271e81
33 changed files with 860 additions and 277 deletions
@@ -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();
}
}
@@ -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;
/**
* A faint horizontal line across the element's width.
*/
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, 1, Theme.getBorder());
}
}
@@ -0,0 +1,44 @@
package org.saturnclient.ui.elements;
import org.saturnclient.config.Theme;
import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts;
/**
* The shared look of input fields: a faint box whose outline brightens on hover
* and turns to the accent color while focused.
*/
public final class Field {
/** Height of the fields in settings rows */
public static final int HEIGHT = 32;
public static final float TEXT_SCALE = 0.68f;
private Field() {
}
public static void drawBox(RenderScope renderScope, int width, int height, boolean hovered, boolean focused) {
int border = focused ? Theme.withAlpha(0.6f, Theme.ACCENT.value)
: hovered ? Theme.withAlpha(0.25f, Theme.FOREGROUND.value)
: Theme.withAlpha(0.1f, Theme.FOREGROUND.value);
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
Theme.withAlpha(0.06f, Theme.FOREGROUND.value), border);
}
/**
* The y at which text drawn at {@code scale} is vertically centered in a
* field of the given height.
*/
public static int textY(int height, float scale) {
return (height - (int) (Fonts.getHeight() * scale)) / 2;
}
/**
* Draws {@code text} centered in a field of the given size.
*/
public static void drawCenteredText(RenderScope renderScope, String text, int width, int height, int font,
int color) {
int x = (width - (int) (Fonts.getWidth(text, font) * TEXT_SCALE)) / 2;
renderScope.drawText(TEXT_SCALE, text, x, textY(height, TEXT_SCALE), font, color);
}
}
@@ -0,0 +1,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();
}
}