Redesign client settings
ClientSettings replaces ConfigEditor: a section list on the left (General, UI Style, Animations) and the chosen section's settings on the right, each with a Reset for that section. UI Style shows four theme presets, the theme colors as a two-column grid, then the radii and font. New pieces: SectionNav, ThemePresetCard, ColorGrid and Theme.PRESETS. Theme keeps its config like AnimationConfig does, Divider can be vertical, and PropertyList's section titles are public. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
package org.saturnclient.ui.components;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.config.property.ColorProperty;
|
||||
import org.saturnclient.config.property.Property;
|
||||
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.elements.Scroll;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
|
||||
/**
|
||||
* Lays out the color properties of a config as a two-column grid of tiles,
|
||||
* each with the color's name and its input.
|
||||
*/
|
||||
public final class ColorGrid {
|
||||
private static final int COLUMNS = 2;
|
||||
private static final int TILE_HEIGHT = 48;
|
||||
private static final int GAP = 8;
|
||||
private static final int INSET = 8;
|
||||
|
||||
private ColorGrid() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tile for every {@link ColorProperty} in {@code properties} to
|
||||
* {@code scroll}, starting at {@code y}.
|
||||
*
|
||||
* @return the y below the grid
|
||||
*/
|
||||
public static int draw(Scroll scroll, Map<String, Property> properties, int width, int y) {
|
||||
int tileWidth = (width - GAP * (COLUMNS - 1)) / COLUMNS;
|
||||
List<Element> inputs = new ArrayList<>();
|
||||
int i = 0;
|
||||
|
||||
for (Map.Entry<String, Property> entry : properties.entrySet()) {
|
||||
if (!(entry.getValue() instanceof ColorProperty color)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int x = (i % COLUMNS) * (tileWidth + GAP);
|
||||
int tileY = y + (i / COLUMNS) * (TILE_HEIGHT + GAP);
|
||||
|
||||
ColorInput input = new ColorInput(color);
|
||||
scroll.draw(new Tile(entry.getKey(), tileWidth, input.width).position(x, tileY));
|
||||
inputs.add(input.position(x + tileWidth - INSET - input.width, tileY + (TILE_HEIGHT - input.height) / 2));
|
||||
i++;
|
||||
}
|
||||
|
||||
// Inputs go on after every tile, so an open color picker covers the tiles below it.
|
||||
inputs.forEach(scroll::draw);
|
||||
|
||||
int rows = (i + COLUMNS - 1) / COLUMNS;
|
||||
return y + rows * TILE_HEIGHT + Math.max(0, rows - 1) * GAP;
|
||||
}
|
||||
|
||||
private static class Tile extends Element {
|
||||
private static final float NAME_SCALE = 0.7f;
|
||||
|
||||
private final String name;
|
||||
private final int inputWidth;
|
||||
|
||||
Tile(String name, int width, int inputWidth) {
|
||||
this.name = name;
|
||||
this.inputWidth = inputWidth;
|
||||
this.width = width;
|
||||
this.height = TILE_HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
|
||||
Theme.withAlpha(0.72f, Theme.PRIMARY.value), Theme.withAlpha(0.06f, Theme.FOREGROUND.value));
|
||||
|
||||
String text = Fonts.ellipsize(name, Theme.FONT.value, NAME_SCALE, width - inputWidth - INSET * 2 - 12);
|
||||
int nameY = (height - (int) (Fonts.getHeight() * NAME_SCALE)) / 2;
|
||||
renderScope.drawText(NAME_SCALE, text, 12, nameY, Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,11 +66,7 @@ public final class PropertyList {
|
||||
}
|
||||
|
||||
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 = drawSection(scroll, name, y + SECTION_GAP) + 4;
|
||||
y = drawRows(scroll, namespace.value, Set.of(), width, y, controls);
|
||||
continue;
|
||||
}
|
||||
@@ -88,6 +84,17 @@ public final class PropertyList {
|
||||
return y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a section title, such as a namespace's name, at {@code y}.
|
||||
*
|
||||
* @return the y below the title
|
||||
*/
|
||||
public static int drawSection(Scroll scroll, String name, int y) {
|
||||
String title = name.toUpperCase(Locale.ROOT);
|
||||
scroll.draw(new Label(() -> title, SECTION_SCALE, () -> Theme.FONT.value, Theme::getMutedFg).position(0, y));
|
||||
return y + (int) (Fonts.getHeight() * SECTION_SCALE);
|
||||
}
|
||||
|
||||
/**
|
||||
* The input for a property, or null for types without one (strings).
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.saturnclient.ui.components;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
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.elements.Field;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
|
||||
/**
|
||||
* A vertical list of a screen's sections under a small heading, with the
|
||||
* current one highlighted.
|
||||
*/
|
||||
public class SectionNav extends Element {
|
||||
public record Item(IdentifierRef icon, String name) {
|
||||
}
|
||||
|
||||
private static final float HEADING_SCALE = 0.6f;
|
||||
private static final int HEADING_HEIGHT = 26;
|
||||
private static final int ITEM_HEIGHT = 40;
|
||||
private static final int ITEM_GAP = 4;
|
||||
private static final int ICON = 17;
|
||||
private static final float TEXT_SCALE = 0.7f;
|
||||
|
||||
private final String heading;
|
||||
private final List<Item> items;
|
||||
private final int active;
|
||||
private final IntConsumer onSelect;
|
||||
|
||||
public SectionNav(String heading, List<Item> items, int active, int width, IntConsumer onSelect) {
|
||||
this.heading = heading;
|
||||
this.items = items;
|
||||
this.active = active;
|
||||
this.onSelect = onSelect;
|
||||
this.width = width;
|
||||
this.height = HEADING_HEIGHT + items.size() * (ITEM_HEIGHT + ITEM_GAP);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
renderScope.drawText(HEADING_SCALE, heading, 10, 0, Theme.FONT.value, Theme.getMutedFg());
|
||||
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
Item item = items.get(i);
|
||||
int y = getItemY(i);
|
||||
boolean selected = i == active;
|
||||
boolean hovered = ctx.isHovering(0, y, width, ITEM_HEIGHT);
|
||||
|
||||
if (selected || hovered) {
|
||||
renderScope.drawRoundedRectangle(0, y, width, ITEM_HEIGHT, Theme.WIDGET_RADIUS.value,
|
||||
Theme.withAlpha(selected ? 0.08f : 0.04f, Theme.FOREGROUND.value));
|
||||
}
|
||||
|
||||
renderScope.drawTexture(item.icon(), 12, y + (ITEM_HEIGHT - ICON) / 2, 0, 0, ICON, ICON,
|
||||
selected ? Theme.ACCENT.value : Theme.getMutedFg());
|
||||
|
||||
String name = Fonts.ellipsize(item.name(), Theme.FONT.value, TEXT_SCALE, width - 12 - ICON - 10 - 8);
|
||||
renderScope.drawText(TEXT_SCALE, name, 12 + ICON + 10, y + Field.textY(ITEM_HEIGHT, TEXT_SCALE),
|
||||
Theme.FONT.value, selected ? Theme.FOREGROUND.value : Theme.PRIMARY_FG.value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
int y = getItemY(i);
|
||||
|
||||
if (mouseY >= y && mouseY <= y + ITEM_HEIGHT) {
|
||||
if (i != active) {
|
||||
onSelect.accept(i);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getItemY(int index) {
|
||||
return HEADING_HEIGHT + index * (ITEM_HEIGHT + ITEM_GAP);
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,13 @@ package org.saturnclient.ui.components;
|
||||
|
||||
import org.saturnclient.common.provider.Providers;
|
||||
import org.saturnclient.common.ref.asset.IdentifierRef;
|
||||
import org.saturnclient.config.Config;
|
||||
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.Panel;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
import org.saturnclient.ui.screens.ConfigEditor;
|
||||
import org.saturnclient.ui.screens.ClientSettings;
|
||||
import org.saturnclient.ui.screens.HudEditor;
|
||||
import org.saturnclient.ui.screens.ModMenu;
|
||||
import org.saturnclient.ui.screens.cosmetics.CloakMenu;
|
||||
@@ -40,8 +39,7 @@ public class Sidebar extends Element {
|
||||
new Item(Textures.ICON_MODS, () -> Providers.saturn.getClient().setScreen(new ModMenu())),
|
||||
new Item(Textures.ICON_COSMETICS, () -> Providers.saturn.getClient().setScreen(new CloakMenu())),
|
||||
new Item(Textures.ICON_HUD_EDITOR, () -> Providers.saturn.getClient().setScreen(new HudEditor())),
|
||||
new Item(Textures.ICON_SETTINGS,
|
||||
() -> Providers.saturn.getClient().setScreen(new ConfigEditor(Config.config))),
|
||||
new Item(Textures.ICON_SETTINGS, () -> Providers.saturn.getClient().setScreen(new ClientSettings())),
|
||||
};
|
||||
|
||||
private final int active;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.saturnclient.ui.components;
|
||||
|
||||
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 theme preset: a small mock-up of a panel in its colors, and its name.
|
||||
* Clicking it applies the preset.
|
||||
*/
|
||||
public class ThemePresetCard extends Element {
|
||||
public static final int HEIGHT = 92;
|
||||
|
||||
private static final int PADDING = 10;
|
||||
private static final int PREVIEW_HEIGHT = 44;
|
||||
private static final float NAME_SCALE = 0.68f;
|
||||
|
||||
private final Theme.Preset preset;
|
||||
private final Runnable onApply;
|
||||
|
||||
/**
|
||||
* @param onApply runs after the preset is applied, to refresh what shows the
|
||||
* old colors
|
||||
*/
|
||||
public ThemePresetCard(Theme.Preset preset, int width, Runnable onApply) {
|
||||
this.preset = preset;
|
||||
this.onApply = onApply;
|
||||
this.width = width;
|
||||
this.height = HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
boolean active = preset.isActive();
|
||||
int border = active ? Theme.ACCENT.value
|
||||
: Theme.withAlpha(ctx.isHovering() ? 0.22f : 0.06f, Theme.FOREGROUND.value);
|
||||
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, active ? 2 : 1,
|
||||
Theme.withAlpha(0.72f, Theme.PRIMARY.value), border);
|
||||
|
||||
// A panel in the preset's colors: a line of text over an accent and a widget
|
||||
int previewWidth = width - PADDING * 2;
|
||||
renderScope.drawRoundedRectangle(PADDING, PADDING, previewWidth, PREVIEW_HEIGHT, 8, preset.background());
|
||||
renderScope.drawRoundedRectangle(PADDING + 8, PADDING + 8, previewWidth * 6 / 10, 5, 3,
|
||||
Theme.withAlpha(0.8f, preset.foreground()));
|
||||
renderScope.drawRoundedRectangle(PADDING + 8, PADDING + PREVIEW_HEIGHT - 20, 34, 12, 4, preset.accent());
|
||||
renderScope.drawRoundedRectangle(PADDING + 47, PADDING + PREVIEW_HEIGHT - 20, 22, 12, 4, preset.primary());
|
||||
|
||||
int nameY = PADDING + PREVIEW_HEIGHT + (height - PADDING - PREVIEW_HEIGHT - (int) (Fonts.getHeight() * NAME_SCALE)) / 2;
|
||||
renderScope.drawText(NAME_SCALE, preset.name(), PADDING, nameY, Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
preset.apply();
|
||||
onApply.run();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user