Redesign mod settings
A mod's settings button now opens ModSettings: a header with the mod's icon, name, version, tags and description, a Reset button and an on/off switch, then its properties as rows, with nested namespaces as titled sections and a live preview card beside them for HUD mods. PanelScreen holds the sidebar-and-panel frame that the mod menu, mod settings and config editor share. PropertyList lays out any config as rows. The inputs share one field look: Toggle is a switch, Select steps with arrows, and the number, keybinding and color inputs are boxes. The color picker opens to the left and closes on outside clicks. Every property type now implements reset(), which was a no-op, and ModLayout keeps its settings in order instead of Map.of's. ConfigEditor moves onto the same frame and rows and drops its HUD preview, which added a new element every frame. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -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