diff --git a/src/main/java/org/saturnclient/config/property/BoolProperty.java b/src/main/java/org/saturnclient/config/property/BoolProperty.java index 231f314..45025d3 100644 --- a/src/main/java/org/saturnclient/config/property/BoolProperty.java +++ b/src/main/java/org/saturnclient/config/property/BoolProperty.java @@ -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); diff --git a/src/main/java/org/saturnclient/config/property/ColorProperty.java b/src/main/java/org/saturnclient/config/property/ColorProperty.java index e25de45..5ee2cab 100644 --- a/src/main/java/org/saturnclient/config/property/ColorProperty.java +++ b/src/main/java/org/saturnclient/config/property/ColorProperty.java @@ -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( diff --git a/src/main/java/org/saturnclient/config/property/FloatProperty.java b/src/main/java/org/saturnclient/config/property/FloatProperty.java index 6b1dd6e..316e90c 100644 --- a/src/main/java/org/saturnclient/config/property/FloatProperty.java +++ b/src/main/java/org/saturnclient/config/property/FloatProperty.java @@ -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); diff --git a/src/main/java/org/saturnclient/config/property/IntProperty.java b/src/main/java/org/saturnclient/config/property/IntProperty.java index 8160455..66d1a84 100644 --- a/src/main/java/org/saturnclient/config/property/IntProperty.java +++ b/src/main/java/org/saturnclient/config/property/IntProperty.java @@ -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); diff --git a/src/main/java/org/saturnclient/config/property/KeybindingProperty.java b/src/main/java/org/saturnclient/config/property/KeybindingProperty.java index 10f4422..ec98a5c 100644 --- a/src/main/java/org/saturnclient/config/property/KeybindingProperty.java +++ b/src/main/java/org/saturnclient/config/property/KeybindingProperty.java @@ -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); diff --git a/src/main/java/org/saturnclient/config/property/NamespaceProperty.java b/src/main/java/org/saturnclient/config/property/NamespaceProperty.java index bf71ef7..8b52176 100644 --- a/src/main/java/org/saturnclient/config/property/NamespaceProperty.java +++ b/src/main/java/org/saturnclient/config/property/NamespaceProperty.java @@ -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(); diff --git a/src/main/java/org/saturnclient/config/property/SelectProperty.java b/src/main/java/org/saturnclient/config/property/SelectProperty.java index 4edb555..7676732 100644 --- a/src/main/java/org/saturnclient/config/property/SelectProperty.java +++ b/src/main/java/org/saturnclient/config/property/SelectProperty.java @@ -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); diff --git a/src/main/java/org/saturnclient/config/property/StringProperty.java b/src/main/java/org/saturnclient/config/property/StringProperty.java index e09d387..70e2462 100644 --- a/src/main/java/org/saturnclient/config/property/StringProperty.java +++ b/src/main/java/org/saturnclient/config/property/StringProperty.java @@ -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); diff --git a/src/main/java/org/saturnclient/mod/ModLayout.java b/src/main/java/org/saturnclient/mod/ModLayout.java index ab0c8b6..e7df485 100644 --- a/src/main/java/org/saturnclient/mod/ModLayout.java +++ b/src/main/java/org/saturnclient/mod/ModLayout.java @@ -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 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() { diff --git a/src/main/java/org/saturnclient/ui/PanelScreen.java b/src/main/java/org/saturnclient/ui/PanelScreen.java new file mode 100644 index 0000000..65d8fbb --- /dev/null +++ b/src/main/java/org/saturnclient/ui/PanelScreen.java @@ -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(); +} diff --git a/src/main/java/org/saturnclient/ui/components/HudPreviewCard.java b/src/main/java/org/saturnclient/ui/components/HudPreviewCard.java new file mode 100644 index 0000000..d1e062c --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/HudPreviewCard.java @@ -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(); + } +} diff --git a/src/main/java/org/saturnclient/ui/components/ModHeader.java b/src/main/java/org/saturnclient/ui/components/ModHeader.java new file mode 100644 index 0000000..c0b70bf --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/ModHeader.java @@ -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; + } +} diff --git a/src/main/java/org/saturnclient/ui/components/PropertyList.java b/src/main/java/org/saturnclient/ui/components/PropertyList.java new file mode 100644 index 0000000..b576c02 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/components/PropertyList.java @@ -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 properties, Set skip, int width, int y) { + // Controls go on after every row, so an open color picker covers the rows below it. + List controls = new ArrayList<>(); + y = drawRows(scroll, properties, skip, width, y, controls); + controls.forEach(scroll::draw); + return y; + } + + private static int drawRows(Scroll scroll, Map properties, Set skip, int width, int y, + List controls) { + for (Map.Entry 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)); + } + } +} diff --git a/src/main/java/org/saturnclient/ui/components/SaturnModule.java b/src/main/java/org/saturnclient/ui/components/SaturnModule.java index 1af513f..280df8e 100644 --- a/src/main/java/org/saturnclient/ui/components/SaturnModule.java +++ b/src/main/java/org/saturnclient/ui/components/SaturnModule.java @@ -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()); } diff --git a/src/main/java/org/saturnclient/ui/components/inputs/ColorInput.java b/src/main/java/org/saturnclient/ui/components/inputs/ColorInput.java index df2f0be..f8dc4cc 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/ColorInput.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/ColorInput.java @@ -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); diff --git a/src/main/java/org/saturnclient/ui/components/inputs/FloatInput.java b/src/main/java/org/saturnclient/ui/components/inputs/FloatInput.java index cdabac0..4e10b34 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/FloatInput.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/FloatInput.java @@ -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); } diff --git a/src/main/java/org/saturnclient/ui/components/inputs/Input.java b/src/main/java/org/saturnclient/ui/components/inputs/Input.java index 421c759..2cd215c 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/Input.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/Input.java @@ -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); } } diff --git a/src/main/java/org/saturnclient/ui/components/inputs/IntInput.java b/src/main/java/org/saturnclient/ui/components/inputs/IntInput.java index 365cd74..64f73a9 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/IntInput.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/IntInput.java @@ -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); } diff --git a/src/main/java/org/saturnclient/ui/components/inputs/KeybindingSelector.java b/src/main/java/org/saturnclient/ui/components/inputs/KeybindingSelector.java index 67b21bd..60461ad 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/KeybindingSelector.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/KeybindingSelector.java @@ -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 ? "" - : name != null ? name.toUpperCase() : "", - 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 diff --git a/src/main/java/org/saturnclient/ui/components/inputs/SearchInput.java b/src/main/java/org/saturnclient/ui/components/inputs/SearchInput.java index f515408..c757f2b 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/SearchInput.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/SearchInput.java @@ -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 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); } } diff --git a/src/main/java/org/saturnclient/ui/components/inputs/Select.java b/src/main/java/org/saturnclient/ui/components/inputs/Select.java index 698b6cb..d6faa39 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/Select.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/Select.java @@ -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(); } } } diff --git a/src/main/java/org/saturnclient/ui/components/inputs/Toggle.java b/src/main/java/org/saturnclient/ui/components/inputs/Toggle.java index 0987723..ee4d445 100644 --- a/src/main/java/org/saturnclient/ui/components/inputs/Toggle.java +++ b/src/main/java/org/saturnclient/ui/components/inputs/Toggle.java @@ -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. */ diff --git a/src/main/java/org/saturnclient/ui/elements/ActionButton.java b/src/main/java/org/saturnclient/ui/elements/ActionButton.java new file mode 100644 index 0000000..8fef9d3 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/elements/ActionButton.java @@ -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(); + } +} diff --git a/src/main/java/org/saturnclient/ui/elements/Divider.java b/src/main/java/org/saturnclient/ui/elements/Divider.java new file mode 100644 index 0000000..abb2c26 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/elements/Divider.java @@ -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()); + } +} diff --git a/src/main/java/org/saturnclient/ui/elements/Field.java b/src/main/java/org/saturnclient/ui/elements/Field.java new file mode 100644 index 0000000..4c857f2 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/elements/Field.java @@ -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); + } +} diff --git a/src/main/java/org/saturnclient/ui/elements/SwitchButton.java b/src/main/java/org/saturnclient/ui/elements/SwitchButton.java new file mode 100644 index 0000000..a5bb6ba --- /dev/null +++ b/src/main/java/org/saturnclient/ui/elements/SwitchButton.java @@ -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(); + } +} diff --git a/src/main/java/org/saturnclient/ui/resources/Textures.java b/src/main/java/org/saturnclient/ui/resources/Textures.java index 68bb46b..f6194e7 100644 --- a/src/main/java/org/saturnclient/ui/resources/Textures.java +++ b/src/main/java/org/saturnclient/ui/resources/Textures.java @@ -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"); diff --git a/src/main/java/org/saturnclient/ui/screens/ConfigEditor.java b/src/main/java/org/saturnclient/ui/screens/ConfigEditor.java index fb31348..1bb38ab 100644 --- a/src/main/java/org/saturnclient/ui/screens/ConfigEditor.java +++ b/src/main/java/org/saturnclient/ui/screens/ConfigEditor.java @@ -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 properties, int row, int col, int w) { - for (Map.Entry 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 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)); } } diff --git a/src/main/java/org/saturnclient/ui/screens/ModMenu.java b/src/main/java/org/saturnclient/ui/screens/ModMenu.java index ef6eadb..eb2e742 100644 --- a/src/main/java/org/saturnclient/ui/screens/ModMenu.java +++ b/src/main/java/org/saturnclient/ui/screens/ModMenu.java @@ -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)); } /** diff --git a/src/main/java/org/saturnclient/ui/screens/ModSettings.java b/src/main/java/org/saturnclient/ui/screens/ModSettings.java new file mode 100644 index 0000000..9d924b9 --- /dev/null +++ b/src/main/java/org/saturnclient/ui/screens/ModSettings.java @@ -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 entry : mod.getConfig().getProperties().entrySet()) { + if (!entry.getKey().equals(ENABLED)) { + entry.getValue().reset(); + } + } + + init(); + } +} diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/chevron_left.svg b/src/main/resources/assets/saturnclient/textures/gui/components/chevron_left.svg new file mode 100644 index 0000000..f8492e1 --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/chevron_left.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/chevron_right.svg b/src/main/resources/assets/saturnclient/textures/gui/components/chevron_right.svg new file mode 100644 index 0000000..a8cbb0e --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/chevron_right.svg @@ -0,0 +1 @@ + diff --git a/src/main/resources/assets/saturnclient/textures/gui/components/reset.svg b/src/main/resources/assets/saturnclient/textures/gui/components/reset.svg new file mode 100644 index 0000000..6d27dbc --- /dev/null +++ b/src/main/resources/assets/saturnclient/textures/gui/components/reset.svg @@ -0,0 +1 @@ +