Redesign the mod menu, settings and cosmetics screens #6

Merged
selimaj-dev merged 13 commits from ui-redesign into master 2026-09-27 16:27:35 +00:00
33 changed files with 860 additions and 277 deletions
Showing only changes of commit df09271e81 - Show all commits
@@ -12,6 +12,12 @@ public class BoolProperty extends Property {
this.defaultValue = defaultValue; this.defaultValue = defaultValue;
} }
@Override
public void reset() {
this.value = defaultValue;
this.isReset = true;
}
@Override @Override
public JsonNode toJson() { public JsonNode toJson() {
return JsonNodeFactory.instance.booleanNode(this.value); return JsonNodeFactory.instance.booleanNode(this.value);
@@ -12,6 +12,12 @@ public class ColorProperty extends Property {
this.defaultValue = defaultValue; this.defaultValue = defaultValue;
} }
@Override
public void reset() {
this.value = defaultValue;
this.isReset = true;
}
@Override @Override
public JsonNode toJson() { public JsonNode toJson() {
return JsonNodeFactory.instance.textNode( return JsonNodeFactory.instance.textNode(
@@ -12,6 +12,12 @@ public class FloatProperty extends Property {
this.defaultValue = defaultValue; this.defaultValue = defaultValue;
} }
@Override
public void reset() {
this.value = defaultValue;
this.isReset = true;
}
@Override @Override
public JsonNode toJson() { public JsonNode toJson() {
return JsonNodeFactory.instance.numberNode(this.value); return JsonNodeFactory.instance.numberNode(this.value);
@@ -12,6 +12,12 @@ public class IntProperty extends Property {
this.defaultValue = defaultValue; this.defaultValue = defaultValue;
} }
@Override
public void reset() {
this.value = defaultValue;
this.isReset = true;
}
@Override @Override
public JsonNode toJson() { public JsonNode toJson() {
return JsonNodeFactory.instance.numberNode(this.value); return JsonNodeFactory.instance.numberNode(this.value);
@@ -15,6 +15,12 @@ public class KeybindingProperty extends Property {
this.defaultValue = defaultValue; this.defaultValue = defaultValue;
} }
@Override
public void reset() {
this.value = defaultValue;
this.isReset = true;
}
@Override @Override
public JsonNode toJson() { public JsonNode toJson() {
return JsonNodeFactory.instance.numberNode(this.value); return JsonNodeFactory.instance.numberNode(this.value);
@@ -13,6 +13,11 @@ public class NamespaceProperty extends Property {
this.value = value; this.value = value;
} }
@Override
public void reset() {
this.value.values().forEach(Property::reset);
}
@Override @Override
public JsonNode toJson() { public JsonNode toJson() {
ObjectNode nested = JsonNodeFactory.instance.objectNode(); ObjectNode nested = JsonNodeFactory.instance.objectNode();
@@ -14,6 +14,12 @@ public class SelectProperty extends Property {
this.availableValues = availableValues; this.availableValues = availableValues;
} }
@Override
public void reset() {
this.value = defaultValue;
this.isReset = true;
}
@Override @Override
public JsonNode toJson() { public JsonNode toJson() {
return JsonNodeFactory.instance.numberNode(this.value); return JsonNodeFactory.instance.numberNode(this.value);
@@ -12,6 +12,12 @@ public class StringProperty extends Property {
this.defaultValue = defaultValue; this.defaultValue = defaultValue;
} }
@Override
public void reset() {
this.value = defaultValue;
this.isReset = true;
}
@Override @Override
public JsonNode toJson() { public JsonNode toJson() {
return JsonNodeFactory.instance.textNode(this.value); return JsonNodeFactory.instance.textNode(this.value);
@@ -1,5 +1,6 @@
package org.saturnclient.mod; package org.saturnclient.mod;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import org.saturnclient.config.property.Property; import org.saturnclient.config.property.Property;
@@ -23,14 +24,17 @@ public class ModLayout {
public int height = 0; public int height = 0;
public NamedProperty prop() { public NamedProperty prop() {
return Property.namespace(Map.of( // A LinkedHashMap, unlike Map.of, keeps the order the settings are shown in.
"X", x, Map<String, Property> props = new LinkedHashMap<>();
"Y", y, props.put("X", x);
"Scale", scale, props.put("Y", y);
"Background Color", bgColor, props.put("Scale", scale);
"Foreground Color", fgColor, props.put("Background Color", bgColor);
"Corner Radius", radius, props.put("Foreground Color", fgColor);
"Font", font)).named("In-Game Display"); props.put("Corner Radius", radius);
props.put("Font", font);
return Property.namespace(props).named("In-Game Display");
} }
public ModLayout() { public ModLayout() {
@@ -0,0 +1,64 @@
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.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;
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();
}
@@ -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();
}
}
@@ -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;
}
}
@@ -0,0 +1,138 @@
package org.saturnclient.ui.components;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
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.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<String, Property> properties, Set<String> skip, int width, int y) {
// Controls go on after every row, so an open color picker covers the rows below it.
List<Element> controls = new ArrayList<>();
y = drawRows(scroll, properties, skip, width, y, controls);
controls.forEach(scroll::draw);
return y;
}
private static int drawRows(Scroll scroll, Map<String, Property> properties, Set<String> skip, int width, int y,
List<Element> controls) {
for (Map.Entry<String, Property> entry : properties.entrySet()) {
String name = entry.getKey();
Property prop = entry.getValue();
if (skip.contains(name)) {
continue;
}
if (prop instanceof NamespaceProperty namespace) {
y += SECTION_GAP;
String title = name.toUpperCase(Locale.ROOT);
scroll.draw(new Label(() -> title, SECTION_SCALE, () -> Theme.FONT.value, Theme::getMutedFg)
.position(0, y));
y += (int) (Fonts.getHeight() * SECTION_SCALE) + 4;
y = drawRows(scroll, namespace.value, Set.of(), width, y, controls);
continue;
}
Element control = createControl(prop);
if (control == null) {
continue;
}
scroll.draw(new Row(name, width, control.width).position(0, y));
controls.add(control.position(width - control.width, y + (ROW_HEIGHT - control.height) / 2));
y += ROW_HEIGHT;
}
return y;
}
/**
* 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 new IntInput(p);
} else if (prop instanceof FloatProperty p) {
return 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 and the line under it. 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 final String name;
private final int controlWidth;
Row(String name, int width, int controlWidth) {
this.name = name;
this.controlWidth = controlWidth;
this.width = width;
this.height = ROW_HEIGHT;
}
@Override
public void render(RenderScope renderScope, ElementContext ctx) {
int nameY = (height - (int) (Fonts.getHeight() * NAME_SCALE)) / 2;
String text = Fonts.ellipsize(name, Theme.FONT.value, NAME_SCALE, width - controlWidth - 16);
renderScope.drawText(NAME_SCALE, text, 0, nameY, Theme.FONT.value, Theme.FOREGROUND.value);
renderScope.drawRectangle(0, height - 1, width, 1, Theme.withAlpha(0.06f, Theme.FOREGROUND.value));
}
}
}
@@ -2,7 +2,6 @@ package org.saturnclient.ui.components;
import org.saturnclient.common.provider.Providers; import org.saturnclient.common.provider.Providers;
import org.saturnclient.config.Theme; import org.saturnclient.config.Theme;
import org.saturnclient.mod.HudMod;
import org.saturnclient.mod.Mod; import org.saturnclient.mod.Mod;
import org.saturnclient.ui.Element; import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.ElementContext;
@@ -10,7 +9,7 @@ import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.components.inputs.Toggle; import org.saturnclient.ui.components.inputs.Toggle;
import org.saturnclient.ui.resources.Fonts; import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.ui.resources.Textures; 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 * A mod's card in the mod menu: icon, name, description, a settings button and
@@ -101,11 +100,7 @@ public class SaturnModule extends Element {
&& mouseY >= getSettingsY() && mouseY <= getSettingsY() + SETTINGS; && mouseY >= getSettingsY() && mouseY <= getSettingsY() + SETTINGS;
if (settingsClicked) { if (settingsClicked) {
if (mod instanceof HudMod h) { Providers.saturn.getClient().setScreen(new ModSettings(mod));
Providers.saturn.getClient().setScreen(new ConfigEditor(mod.getConfig(), h));
} else {
Providers.saturn.getClient().setScreen(new ConfigEditor(mod.getConfig()));
}
} else { } else {
mod.setEnabled(!mod.isEnabled()); mod.setEnabled(!mod.isEnabled());
} }
@@ -2,10 +2,12 @@ package org.saturnclient.ui.components.inputs;
import java.awt.Color; import java.awt.Color;
import org.saturnclient.config.Theme;
import org.saturnclient.config.property.ColorProperty; import org.saturnclient.config.property.ColorProperty;
import org.saturnclient.ui.Element; import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope; import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.elements.Field;
public class ColorInput extends Element { public class ColorInput extends Element {
public ColorProperty prop; public ColorProperty prop;
@@ -26,10 +28,19 @@ public class ColorInput extends Element {
private static final int SLIDER_HEIGHT = 10; private static final int SLIDER_HEIGHT = 10;
private static final int HUE_SLIDER_HEIGHT = 10; private static final int HUE_SLIDER_HEIGHT = 10;
// The picker opens to the left, since the input sits at the right of its row.
private static final int PICKER_X = -PICKER_WIDTH - 12;
private static final int SWATCH = 20;
// Bounds of the picker's backdrop, relative to the color square
private static final int BACKDROP_PADDING = 8;
private static final int BACKDROP_TOP = -SLIDER_HEIGHT - 10 - BACKDROP_PADDING;
private static final int BACKDROP_BOTTOM = PICKER_HEIGHT + 10 + HUE_SLIDER_HEIGHT + BACKDROP_PADDING;
public ColorInput(ColorProperty prop) { public ColorInput(ColorProperty prop) {
this.prop = prop; this.prop = prop;
this.width = 58; this.width = 116;
this.height = 20; this.height = Field.HEIGHT;
syncFromColor(); syncFromColor();
} }
@@ -40,15 +51,23 @@ public class ColorInput extends Element {
@Override @Override
public void mouseClicked(double mouseX, double mouseY, int button) { public void mouseClicked(double mouseX, double mouseY, int button) {
if (open) { if (!open) {
handlePickerClick(mouseX - width - 8, mouseY); return;
} }
boolean onInput = mouseX >= 0 && mouseX <= width && mouseY >= 0 && mouseY <= height;
if (!onInput && !isOverPicker(mouseX - PICKER_X, mouseY)) {
open = false;
return;
}
handlePickerClick(mouseX - PICKER_X, mouseY);
} }
@Override @Override
public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
if (open) { if (open) {
handlePickerDrag(mouseX - width - 8, mouseY); handlePickerDrag(mouseX - PICKER_X, mouseY);
} }
} }
@@ -59,16 +78,26 @@ public class ColorInput extends Element {
@Override @Override
public void render(RenderScope renderScope, ElementContext ctx) { public void render(RenderScope renderScope, ElementContext ctx) {
// Draw the main color rectangle Field.drawBox(renderScope, width, height, ctx.isHovering(), open);
renderScope.drawRoundedRectangle(0, 0, width, height, 6, prop.value);
int swatchY = (height - SWATCH) / 2;
renderScope.drawRoundedRectangle(swatchY, swatchY, SWATCH, SWATCH, 5, 1, prop.value,
Theme.withAlpha(0.3f, Theme.FOREGROUND.value));
renderScope.drawText(Field.TEXT_SCALE, toHex(prop.value), swatchY + SWATCH + 8,
Field.textY(height, Field.TEXT_SCALE), Theme.FONT.value, Theme.FOREGROUND.value);
if (!open) if (!open)
return; return;
// Draw the picker relative to (width + 8, 0) int pickerX = PICKER_X;
int pickerX = width + 8;
int pickerY = 0; int pickerY = 0;
// Backdrop behind the picker and its two sliders
renderScope.drawRoundedRectangle(pickerX - BACKDROP_PADDING, BACKDROP_TOP,
PICKER_WIDTH + BACKDROP_PADDING * 2, BACKDROP_BOTTOM - BACKDROP_TOP,
Theme.WIDGET_RADIUS.value, 1, Theme.withAlpha(0.95f, Theme.BACKGROUND.value), Theme.getBorder());
renderColorSquare(renderScope, pickerX, pickerY); renderColorSquare(renderScope, pickerX, pickerY);
renderOpacitySlider(renderScope, pickerX, pickerY - SLIDER_HEIGHT - 10); renderOpacitySlider(renderScope, pickerX, pickerY - SLIDER_HEIGHT - 10);
renderHueSlider(renderScope, pickerX, pickerY + PICKER_HEIGHT + 10); renderHueSlider(renderScope, pickerX, pickerY + PICKER_HEIGHT + 10);
@@ -138,6 +167,11 @@ public class ColorInput extends Element {
} }
} }
private static boolean isOverPicker(double x, double y) {
return x >= -BACKDROP_PADDING && x <= PICKER_WIDTH + BACKDROP_PADDING
&& y >= BACKDROP_TOP && y <= BACKDROP_BOTTOM;
}
private void handlePickerDrag(double mouseX, double mouseY) { private void handlePickerDrag(double mouseX, double mouseY) {
// Opacity // Opacity
if (draggingOpacity) if (draggingOpacity)
@@ -169,6 +203,16 @@ public class ColorInput extends Element {
updateColorFromSelection(); updateColorFromSelection();
} }
/**
* #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 syncFromColor() { private void syncFromColor() {
int rgb = prop.value & 0xFFFFFF; int rgb = prop.value & 0xFFFFFF;
float[] hsb = Color.RGBtoHSB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF, null); float[] hsb = Color.RGBtoHSB((rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF, null);
@@ -2,14 +2,14 @@ package org.saturnclient.ui.components.inputs;
import org.saturnclient.config.ConfigManager; import org.saturnclient.config.ConfigManager;
import org.saturnclient.config.property.FloatProperty; import org.saturnclient.config.property.FloatProperty;
import org.saturnclient.ui.resources.Fonts; import org.saturnclient.ui.elements.Field;
public class FloatInput extends Input { public class FloatInput extends Input {
public FloatProperty prop; public FloatProperty prop;
public FloatInput(FloatProperty prop) { public FloatInput(FloatProperty prop) {
this.width = 120; this.width = 96;
this.height = Fonts.getHeight(); this.height = Field.HEIGHT;
this.prop = prop; this.prop = prop;
this.text = String.valueOf(prop.value); this.text = String.valueOf(prop.value);
} }
@@ -5,6 +5,7 @@ import org.saturnclient.config.Theme;
import org.saturnclient.ui.Element; import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope; import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.elements.Field;
import org.saturnclient.ui.resources.Fonts; import org.saturnclient.ui.resources.Fonts;
public abstract class Input extends Element { public abstract class Input extends Element {
@@ -12,9 +13,9 @@ public abstract class Input extends Element {
public int cursorPosition = 0; public int cursorPosition = 0;
/** Scale the text is drawn at */ /** Scale the text is drawn at */
protected float textScale = 0.6f; protected float textScale = Field.TEXT_SCALE;
/** Horizontal inset of the text on both sides */ /** Horizontal inset of the text on both sides */
protected int textX = 4; protected int textX = 10;
public abstract void checkReset(); public abstract void checkReset();
@@ -70,16 +71,24 @@ public abstract class Input extends Element {
public void render(RenderScope renderScope, ElementContext ctx) { public void render(RenderScope renderScope, ElementContext ctx) {
checkReset(); 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(); int scrollOffset = getScrollOffset();
String visibleText = getVisibleText(scrollOffset); String visibleText = getVisibleText(scrollOffset);
renderScope.drawText(textScale, visibleText, textX, 4, Theme.FONT.value, Theme.PRIMARY_FG.value); renderScope.drawText(textScale, visibleText, textX, textY, Theme.FONT.value, Theme.FOREGROUND.value);
if (focused) { if (focused) {
int cursorX = getCursorX(visibleText, scrollOffset); renderScope.drawRectangle(getCursorX(visibleText, scrollOffset), textY - 1, 1, textHeight + 2,
Theme.ACCENT.value);
renderScope.drawRectangle(cursorX, 2, 1, height - 4, Theme.ACCENT.value);
} }
} }
@@ -2,14 +2,14 @@ package org.saturnclient.ui.components.inputs;
import org.saturnclient.config.ConfigManager; import org.saturnclient.config.ConfigManager;
import org.saturnclient.config.property.IntProperty; import org.saturnclient.config.property.IntProperty;
import org.saturnclient.ui.resources.Fonts; import org.saturnclient.ui.elements.Field;
public class IntInput extends Input { public class IntInput extends Input {
public IntProperty prop; public IntProperty prop;
public IntInput(IntProperty prop) { public IntInput(IntProperty prop) {
this.width = 120; this.width = 96;
this.height = Fonts.getHeight(); this.height = Field.HEIGHT;
this.prop = prop; this.prop = prop;
this.text = String.valueOf(prop.value); this.text = String.valueOf(prop.value);
} }
@@ -7,27 +7,41 @@ import org.saturnclient.config.property.KeybindingProperty;
import org.saturnclient.ui.Element; import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope; 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 { public class KeybindingSelector extends Element {
KeybindingProperty prop; KeybindingProperty prop;
public KeybindingSelector(KeybindingProperty prop) { public KeybindingSelector(KeybindingProperty prop) {
this.prop = prop; this.prop = prop;
this.width = 120; this.width = 96;
this.height = Fonts.getHeight(); this.height = Field.HEIGHT;
} }
@Override @Override
public void render(RenderScope renderScope, ElementContext ctx) { public void render(RenderScope renderScope, ElementContext ctx) {
int textColor = focused ? 0xFFFFFF : 0xAAAAAA; Field.drawBox(renderScope, width, height, ctx.isHovering(), focused);
renderScope.drawRoundedRectangle(0, 0, width, height, 10, 0xFF000000);
String name = Providers.GLFW.getKeyName(prop.value);
renderScope.drawText(0.6f, if (focused) {
prop.value == -1 ? "<NONE>" Field.drawCenteredText(renderScope, "Press a key", width, height, Theme.getBodyFont(),
: name != null ? name.toUpperCase() : "<Invalid-Key>", Theme.getMutedFg());
4, 4, Theme.FONT.value, textColor); 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 @Override
@@ -5,7 +5,7 @@ import java.util.function.Consumer;
import org.saturnclient.config.Theme; import org.saturnclient.config.Theme;
import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope; import org.saturnclient.ui.RenderScope;
import org.saturnclient.ui.resources.Fonts; import org.saturnclient.ui.elements.Field;
import org.saturnclient.ui.resources.Textures; import org.saturnclient.ui.resources.Textures;
/** /**
@@ -21,7 +21,6 @@ public class SearchInput extends Input {
public SearchInput(String placeholder, Consumer<String> onChange) { public SearchInput(String placeholder, Consumer<String> onChange) {
this.placeholder = placeholder; this.placeholder = placeholder;
this.onChange = onChange; this.onChange = onChange;
this.textScale = 0.68f;
this.textX = ICON_X + ICON_SIZE + 8; this.textX = ICON_X + ICON_SIZE + 8;
this.width = 240; this.width = 240;
this.height = 36; this.height = 36;
@@ -49,31 +48,17 @@ public class SearchInput extends Input {
@Override @Override
public void render(RenderScope renderScope, ElementContext ctx) { public void render(RenderScope renderScope, ElementContext ctx) {
int border = focused ? Theme.withAlpha(0.6f, Theme.ACCENT.value) Field.drawBox(renderScope, width, height, ctx.isHovering(), focused);
: ctx.isHovering() ? 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);
renderScope.drawTexture(Textures.ICON_SEARCH, ICON_X, (height - ICON_SIZE) / 2, 0, 0, renderScope.drawTexture(Textures.ICON_SEARCH, ICON_X, (height - ICON_SIZE) / 2, 0, 0,
ICON_SIZE, ICON_SIZE, Theme.getMutedFg()); ICON_SIZE, ICON_SIZE, Theme.getMutedFg());
int textHeight = (int) (Fonts.getHeight() * textScale);
int textY = (height - textHeight) / 2;
if (text.isEmpty() && !focused) { if (text.isEmpty() && !focused) {
renderScope.drawText(textScale, placeholder, textX, textY, Theme.getBodyFont(), Theme.getMutedFg()); renderScope.drawText(textScale, placeholder, textX, Field.textY(height, textScale),
Theme.getBodyFont(), Theme.getMutedFg());
return; return;
} }
int scrollOffset = getScrollOffset(); drawText(renderScope);
String visibleText = getVisibleText(scrollOffset);
renderScope.drawText(textScale, visibleText, textX, textY, Theme.FONT.value, Theme.FOREGROUND.value);
if (focused) {
renderScope.drawRectangle(getCursorX(visibleText, scrollOffset), textY - 1, 1, textHeight + 2,
Theme.ACCENT.value);
}
} }
} }
@@ -5,36 +5,50 @@ import org.saturnclient.config.property.SelectProperty;
import org.saturnclient.ui.Element; import org.saturnclient.ui.Element;
import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope; 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.Fonts;
import org.saturnclient.ui.resources.Textures;
/**
* Steps through a {@link SelectProperty}'s values with arrows on either side.
* Clicking the left third goes back; anywhere else goes forward.
*/
public class Select extends Element { public class Select extends Element {
private SelectProperty prop; private static final int ARROW = 16;
private static final int ARROW_INSET = 8;
private final SelectProperty prop;
public Select(SelectProperty prop) { public Select(SelectProperty prop) {
this.prop = prop; this.prop = prop;
this.width = 150; this.width = 200;
this.height = 15; this.height = Field.HEIGHT;
} }
@Override @Override
public void render(RenderScope renderScope, ElementContext ctx) { public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawText(0.8f, "<", 0, 0, Theme.FONT.value, Theme.FOREGROUND.value); Field.drawBox(renderScope, width, height, ctx.isHovering(), false);
String text = prop.getSelection(); boolean prevHovered = ctx.isHovering(0, 0, width / 3, height);
boolean nextHovered = ctx.isHovering() && !prevHovered;
int arrowY = (height - ARROW) / 2;
renderScope.drawText(0.6f, text, (int) (width - ((float) Fonts.getWidth(text, Theme.FONT.value) * 0.6f)) / 2, 1, renderScope.drawTexture(Textures.ICON_CHEVRON_LEFT, ARROW_INSET, arrowY, 0, 0, ARROW, ARROW,
Theme.FONT.value, Theme.FOREGROUND.value); prevHovered ? Theme.ACCENT.value : Theme.getMutedFg());
renderScope.drawTexture(Textures.ICON_CHEVRON_RIGHT, width - ARROW_INSET - ARROW, arrowY, 0, 0, ARROW, ARROW,
nextHovered ? Theme.ACCENT.value : Theme.getMutedFg());
renderScope.drawText(0.8f, ">", width - 10, 0, Theme.FONT.value, Theme.FOREGROUND.value); int textSpace = width - (ARROW_INSET + ARROW + 4) * 2;
String text = Fonts.ellipsize(prop.getSelection(), Theme.FONT.value, Field.TEXT_SCALE, textSpace);
Field.drawCenteredText(renderScope, text, width, height, Theme.FONT.value, Theme.FOREGROUND.value);
} }
@Override @Override
public void click(int mouseX, int mouseY) { public void click(int mouseX, int mouseY) {
if (Utils.isHovering(mouseX - (width - 10), mouseY, 10, height, 1.0f)) { if (mouseX < width / 3) {
prop.next();
} else if (Utils.isHovering(mouseX, mouseY, 10, height, 1.0f)) {
prop.prev(); prop.prev();
} else {
prop.next();
} }
} }
} }
@@ -7,25 +7,22 @@ import org.saturnclient.ui.ElementContext;
import org.saturnclient.ui.RenderScope; import org.saturnclient.ui.RenderScope;
public class Toggle extends Element { public class Toggle extends Element {
public static final int SWITCH_WIDTH = 42;
public static final int SWITCH_HEIGHT = 24;
private BoolProperty prop; private BoolProperty prop;
public Toggle(BoolProperty prop) { public Toggle(BoolProperty prop) {
this.prop = prop; this.prop = prop;
this.width = 60; this.width = SWITCH_WIDTH;
this.height = 30; this.height = SWITCH_HEIGHT;
} }
@Override @Override
public void render(RenderScope renderScope, ElementContext ctx) { public void render(RenderScope renderScope, ElementContext ctx) {
renderScope.drawRoundedRectangle(0, 0, 60, 30, Theme.WIDGET_RADIUS.value, drawSwitch(renderScope, 0, 0, prop.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);
} }
public static final int SWITCH_WIDTH = 42;
public static final int SWITCH_HEIGHT = 24;
/** /**
* Draws an on/off switch: a pill track with a round knob on the right when on. * Draws an on/off switch: a pill track with a round knob on the right when on.
*/ */
@@ -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();
}
}
@@ -33,6 +33,11 @@ public class Textures {
public static final IdentifierRef ICON_CLOSE = IdentifierRef.ofSaturn("textures/gui/components/close.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_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_SLIDERS = IdentifierRef.ofSaturn("textures/gui/components/sliders.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 LOGO = IdentifierRef.ofSaturn("textures/logo/logo.png"); 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 REALISTIC_LOGO = IdentifierRef.ofSaturn("textures/logo/realistic.png");
@@ -1,173 +1,37 @@
package org.saturnclient.ui.screens; package org.saturnclient.ui.screens;
import java.util.Map; import java.util.Set;
import org.saturnclient.config.ConfigManager; import org.saturnclient.config.ConfigManager;
import org.saturnclient.config.Theme; import org.saturnclient.config.Theme;
import org.saturnclient.config.property.BoolProperty; import org.saturnclient.ui.PanelScreen;
import org.saturnclient.config.property.ColorProperty; import org.saturnclient.ui.components.PropertyList;
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.Sidebar;
import org.saturnclient.ui.components.inputs.FloatInput; import org.saturnclient.ui.elements.Label;
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.Scroll;
import org.saturnclient.ui.elements.Text;
import org.saturnclient.ui.resources.Fonts;
public class ConfigEditor extends SaturnScreen { /**
private ConfigManager config; * Edits every property of a config, such as the client's own settings.
private HudMod hudMod; */
private HudModPreview preview; public class ConfigEditor extends PanelScreen {
private static final int SCROLLBAR_SPACE = 14;
private static final int LIST_Y = 44;
private final ConfigManager config;
public ConfigEditor(ConfigManager config) { public ConfigEditor(ConfigManager config) {
super("Config Editor"); super("Config Editor", Sidebar.SETTINGS);
this.config = config; this.config = config;
} }
public ConfigEditor(ConfigManager config, HudMod hudMod) {
super("Config Editor");
this.config = config;
this.hudMod = hudMod;
}
@Override @Override
public void ui() { protected void panel() {
int p = 10; draw(new Label(() -> "Settings", 1.15f, () -> Theme.FONT.value, () -> Theme.FOREGROUND.value)
int g = 10; .position(contentX, contentY + 2));
Scroll scroll = new Scroll(p); Scroll scroll = new Scroll(0).noBackground();
PropertyList.draw(scroll, config.getProperties(), Set.of(), contentWidth, 0);
drawProperties(scroll, config.getProperties(), 0, 0, 480 + 10 + (g * 2)); draw(scroll.dimensions(contentWidth + SCROLLBAR_SPACE, contentHeight - LIST_Y)
.position(contentX, contentY + LIST_Y));
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(Sidebar.SETTINGS, this.provider::close).center(width, height, -(scrollWidth / 2 + 35), 0));
}
private int drawProperties(Scroll configScroll, Map<String, Property> properties, int row, int col, int w) {
for (Map.Entry<String, Property> 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<String, Property> 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);
} }
} }
@@ -8,28 +8,20 @@ import org.saturnclient.config.AnimationConfig;
import org.saturnclient.config.Theme; import org.saturnclient.config.Theme;
import org.saturnclient.mod.Mod; import org.saturnclient.mod.Mod;
import org.saturnclient.mod.ModManager; 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.anim.SlideFade;
import org.saturnclient.ui.components.SaturnModule; import org.saturnclient.ui.components.SaturnModule;
import org.saturnclient.ui.components.Sidebar; import org.saturnclient.ui.components.Sidebar;
import org.saturnclient.ui.components.inputs.SearchInput; import org.saturnclient.ui.components.inputs.SearchInput;
import org.saturnclient.ui.elements.AnimationStagger; import org.saturnclient.ui.elements.AnimationStagger;
import org.saturnclient.ui.elements.ChipGroup; import org.saturnclient.ui.elements.ChipGroup;
import org.saturnclient.ui.elements.ImageTexture;
import org.saturnclient.ui.elements.Label; import org.saturnclient.ui.elements.Label;
import org.saturnclient.ui.elements.Panel;
import org.saturnclient.ui.elements.Scroll; import org.saturnclient.ui.elements.Scroll;
import org.saturnclient.ui.resources.Fonts; import org.saturnclient.ui.resources.Fonts;
import org.saturnclient.ui.resources.Textures;
public class ModMenu extends SaturnScreen { public class ModMenu extends PanelScreen {
private static final String ALL = "All"; private static final String ALL = "All";
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;
private static final int PADDING = 20;
private static final int COLUMNS = 3; private static final int COLUMNS = 3;
private static final int CARD_GAP = 10; private static final int CARD_GAP = 10;
private static final int SCROLLBAR_SPACE = 14; private static final int SCROLLBAR_SPACE = 14;
@@ -41,43 +33,30 @@ public class ModMenu extends SaturnScreen {
private int gridWidth; private int gridWidth;
public ModMenu() { public ModMenu() {
super("Mod Menu"); super("Mod Menu", Sidebar.MODS);
} }
@Override @Override
public void ui() { protected void panel() {
// The search box starts empty after a resize, so the query does too. // The search box starts empty after a resize, so the query does too.
query = ""; query = "";
gridWidth = contentWidth;
int panelWidth = Math.min(MAX_WIDTH, width - MARGIN * 2 - Sidebar.WIDTH - GAP);
int panelHeight = Math.min(MAX_HEIGHT, height - MARGIN * 2);
int left = (width - (Sidebar.WIDTH + GAP + panelWidth)) / 2;
int top = (height - panelHeight) / 2;
int panelX = left + Sidebar.WIDTH + GAP;
int contentX = panelX + PADDING;
gridWidth = panelWidth - PADDING * 2;
draw(new Sidebar(Sidebar.MODS, this.provider::close)
.dimensions(Sidebar.WIDTH, panelHeight)
.position(left, top));
draw(new Panel().dimensions(panelWidth, panelHeight).position(panelX, top));
// Header // Header
draw(new Label(() -> "Mods", 1.15f, () -> Theme.FONT.value, () -> Theme.FOREGROUND.value) draw(new Label(() -> "Mods", 1.15f, () -> Theme.FONT.value, () -> Theme.FOREGROUND.value)
.position(contentX, top + PADDING + 2)); .position(contentX, contentY + 2));
draw(new Label(this::getEnabledSummary, 0.66f, Theme::getBodyFont, Theme::getMutedFg) draw(new Label(this::getEnabledSummary, 0.66f, Theme::getBodyFont, Theme::getMutedFg)
.position(contentX, top + PADDING + 28)); .position(contentX, contentY + 28));
SearchInput search = new SearchInput("Search mods", text -> { SearchInput search = new SearchInput("Search mods", text -> {
query = text; query = text;
refresh(false); refresh(false);
}); });
draw(search.position(panelX + panelWidth - PADDING - search.width, top + PADDING + 2)); draw(search.position(contentX + contentWidth - search.width, contentY + 2));
// Categories // Categories
int chipsY = top + PADDING + 56; int chipsY = contentY + 56;
ChipGroup categories = new ChipGroup(getCategories(), category, selected -> { ChipGroup categories = new ChipGroup(getCategories(), category, selected -> {
category = selected; category = selected;
refresh(false); refresh(false);
@@ -87,11 +66,9 @@ public class ModMenu extends SaturnScreen {
// Mods // Mods
int scrollY = chipsY + categories.height + 14; int scrollY = chipsY + categories.height + 14;
scroll = new Scroll(0).noBackground(); scroll = new Scroll(0).noBackground();
scroll.dimensions(gridWidth + SCROLLBAR_SPACE, top + panelHeight - 12 - scrollY); scroll.dimensions(gridWidth + SCROLLBAR_SPACE, panelY + panelHeight - 12 - scrollY);
refresh(true); refresh(true);
draw(scroll.position(contentX, scrollY)); draw(scroll.position(contentX, scrollY));
draw(new ImageTexture(Textures.LOGO_TEXT_BIG).dimensions(180, 18).position(width - 180 - 20, height - 18 - 20));
} }
/** /**
@@ -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 static final int SCROLLBAR_SPACE = 14;
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), listWidth, 0);
draw(scroll.dimensions(listWidth + SCROLLBAR_SPACE, 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<String, Property> entry : mod.getConfig().getProperties().entrySet()) {
if (!entry.getKey().equals(ENABLED)) {
entry.getValue().reset();
}
}
init();
}
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>

After

Width:  |  Height:  |  Size: 209 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>

After

Width:  |  Height:  |  Size: 208 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>

After

Width:  |  Height:  |  Size: 264 B