Redesign the mod menu, settings and cosmetics screens #6
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -12,6 +12,12 @@ public class FloatProperty 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);
|
||||
|
||||
@@ -12,6 +12,12 @@ public class IntProperty 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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.saturnclient.mod;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.saturnclient.config.property.Property;
|
||||
@@ -23,14 +24,17 @@ 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<String, Property> props = new LinkedHashMap<>();
|
||||
props.put("X", x);
|
||||
props.put("Y", y);
|
||||
props.put("Scale", scale);
|
||||
props.put("Background Color", bgColor);
|
||||
props.put("Foreground Color", fgColor);
|
||||
props.put("Corner Radius", radius);
|
||||
props.put("Font", font);
|
||||
|
||||
return Property.namespace(props).named("In-Game Display");
|
||||
}
|
||||
|
||||
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.config.Theme;
|
||||
import org.saturnclient.mod.HudMod;
|
||||
import org.saturnclient.mod.Mod;
|
||||
import org.saturnclient.ui.Element;
|
||||
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.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
|
||||
@@ -101,11 +100,7 @@ public class SaturnModule extends Element {
|
||||
&& mouseY >= getSettingsY() && mouseY <= getSettingsY() + SETTINGS;
|
||||
|
||||
if (settingsClicked) {
|
||||
if (mod instanceof HudMod h) {
|
||||
Providers.saturn.getClient().setScreen(new ConfigEditor(mod.getConfig(), h));
|
||||
} else {
|
||||
Providers.saturn.getClient().setScreen(new ConfigEditor(mod.getConfig()));
|
||||
}
|
||||
Providers.saturn.getClient().setScreen(new ModSettings(mod));
|
||||
} else {
|
||||
mod.setEnabled(!mod.isEnabled());
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ 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;
|
||||
import org.saturnclient.ui.elements.Field;
|
||||
|
||||
public class ColorInput extends Element {
|
||||
public ColorProperty prop;
|
||||
@@ -26,10 +28,19 @@ public class ColorInput extends Element {
|
||||
private static final int 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) {
|
||||
this.prop = prop;
|
||||
this.width = 58;
|
||||
this.height = 20;
|
||||
this.width = 116;
|
||||
this.height = Field.HEIGHT;
|
||||
syncFromColor();
|
||||
}
|
||||
|
||||
@@ -40,15 +51,23 @@ public class ColorInput extends Element {
|
||||
|
||||
@Override
|
||||
public void mouseClicked(double mouseX, double mouseY, int button) {
|
||||
if (open) {
|
||||
handlePickerClick(mouseX - width - 8, mouseY);
|
||||
if (!open) {
|
||||
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
|
||||
public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
|
||||
if (open) {
|
||||
handlePickerDrag(mouseX - width - 8, mouseY);
|
||||
handlePickerDrag(mouseX - PICKER_X, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,16 +78,26 @@ public class ColorInput extends Element {
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
// Draw the main color rectangle
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, 6, prop.value);
|
||||
Field.drawBox(renderScope, width, height, ctx.isHovering(), open);
|
||||
|
||||
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)
|
||||
return;
|
||||
|
||||
// Draw the picker relative to (width + 8, 0)
|
||||
int pickerX = width + 8;
|
||||
int pickerX = PICKER_X;
|
||||
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);
|
||||
renderOpacitySlider(renderScope, pickerX, pickerY - SLIDER_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) {
|
||||
// Opacity
|
||||
if (draggingOpacity)
|
||||
@@ -169,6 +203,16 @@ public class ColorInput extends Element {
|
||||
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() {
|
||||
int rgb = prop.value & 0xFFFFFF;
|
||||
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.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);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 {
|
||||
@@ -12,9 +13,9 @@ public abstract class Input extends Element {
|
||||
public int cursorPosition = 0;
|
||||
|
||||
/** 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 */
|
||||
protected int textX = 4;
|
||||
protected int textX = 10;
|
||||
|
||||
public abstract void checkReset();
|
||||
|
||||
@@ -70,16 +71,24 @@ 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(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) {
|
||||
int cursorX = getCursorX(visibleText, scrollOffset);
|
||||
|
||||
renderScope.drawRectangle(cursorX, 2, 1, height - 4, Theme.ACCENT.value);
|
||||
renderScope.drawRectangle(getCursorX(visibleText, scrollOffset), textY - 1, 1, textHeight + 2,
|
||||
Theme.ACCENT.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 ? "<NONE>"
|
||||
: name != null ? name.toUpperCase() : "<Invalid-Key>",
|
||||
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
|
||||
|
||||
@@ -5,7 +5,7 @@ import java.util.function.Consumer;
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
import org.saturnclient.ui.elements.Field;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
|
||||
/**
|
||||
@@ -21,7 +21,6 @@ public class SearchInput extends Input {
|
||||
public SearchInput(String placeholder, Consumer<String> onChange) {
|
||||
this.placeholder = placeholder;
|
||||
this.onChange = onChange;
|
||||
this.textScale = 0.68f;
|
||||
this.textX = ICON_X + ICON_SIZE + 8;
|
||||
this.width = 240;
|
||||
this.height = 36;
|
||||
@@ -49,31 +48,17 @@ public class SearchInput extends Input {
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
int border = focused ? Theme.withAlpha(0.6f, Theme.ACCENT.value)
|
||||
: 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);
|
||||
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());
|
||||
|
||||
int textHeight = (int) (Fonts.getHeight() * textScale);
|
||||
int textY = (height - textHeight) / 2;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
int scrollOffset = getScrollOffset();
|
||||
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);
|
||||
}
|
||||
drawText(renderScope);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,36 +5,50 @@ 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.Utils;
|
||||
import org.saturnclient.ui.elements.Field;
|
||||
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 {
|
||||
private SelectProperty prop;
|
||||
private static final int ARROW = 16;
|
||||
private static final int ARROW_INSET = 8;
|
||||
|
||||
private final SelectProperty prop;
|
||||
|
||||
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);
|
||||
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,
|
||||
Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
renderScope.drawTexture(Textures.ICON_CHEVRON_LEFT, ARROW_INSET, arrowY, 0, 0, ARROW, ARROW,
|
||||
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
|
||||
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)) {
|
||||
if (mouseX < width / 3) {
|
||||
prop.prev();
|
||||
} else {
|
||||
prop.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,25 +7,22 @@ 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);
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -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_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_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 REALISTIC_LOGO = IdentifierRef.ofSaturn("textures/logo/realistic.png");
|
||||
|
||||
@@ -1,173 +1,37 @@
|
||||
package org.saturnclient.ui.screens;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
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.PanelScreen;
|
||||
import org.saturnclient.ui.components.PropertyList;
|
||||
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.Label;
|
||||
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;
|
||||
/**
|
||||
* Edits every property of a config, such as the client's own settings.
|
||||
*/
|
||||
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) {
|
||||
super("Config Editor");
|
||||
|
||||
super("Config Editor", Sidebar.SETTINGS);
|
||||
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;
|
||||
protected void panel() {
|
||||
draw(new Label(() -> "Settings", 1.15f, () -> Theme.FONT.value, () -> Theme.FOREGROUND.value)
|
||||
.position(contentX, contentY + 2));
|
||||
|
||||
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(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);
|
||||
Scroll scroll = new Scroll(0).noBackground();
|
||||
PropertyList.draw(scroll, config.getProperties(), Set.of(), contentWidth, 0);
|
||||
draw(scroll.dimensions(contentWidth + SCROLLBAR_SPACE, contentHeight - LIST_Y)
|
||||
.position(contentX, contentY + LIST_Y));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,28 +8,20 @@ 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.ChipGroup;
|
||||
import org.saturnclient.ui.elements.ImageTexture;
|
||||
import org.saturnclient.ui.elements.Label;
|
||||
import org.saturnclient.ui.elements.Panel;
|
||||
import org.saturnclient.ui.elements.Scroll;
|
||||
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 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 CARD_GAP = 10;
|
||||
private static final int SCROLLBAR_SPACE = 14;
|
||||
@@ -41,43 +33,30 @@ public class ModMenu extends SaturnScreen {
|
||||
private int gridWidth;
|
||||
|
||||
public ModMenu() {
|
||||
super("Mod Menu");
|
||||
super("Mod Menu", Sidebar.MODS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ui() {
|
||||
protected void panel() {
|
||||
// The search box starts empty after a resize, so the query does too.
|
||||
query = "";
|
||||
|
||||
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));
|
||||
gridWidth = contentWidth;
|
||||
|
||||
// Header
|
||||
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)
|
||||
.position(contentX, top + PADDING + 28));
|
||||
.position(contentX, contentY + 28));
|
||||
|
||||
SearchInput search = new SearchInput("Search mods", text -> {
|
||||
query = text;
|
||||
refresh(false);
|
||||
});
|
||||
draw(search.position(panelX + panelWidth - PADDING - search.width, top + PADDING + 2));
|
||||
draw(search.position(contentX + contentWidth - search.width, contentY + 2));
|
||||
|
||||
// Categories
|
||||
int chipsY = top + PADDING + 56;
|
||||
int chipsY = contentY + 56;
|
||||
ChipGroup categories = new ChipGroup(getCategories(), category, selected -> {
|
||||
category = selected;
|
||||
refresh(false);
|
||||
@@ -87,11 +66,9 @@ public class ModMenu extends SaturnScreen {
|
||||
// Mods
|
||||
int scrollY = chipsY + categories.height + 14;
|
||||
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);
|
||||
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 |
Reference in New Issue
Block a user