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:
2026-09-27 17:25:47 +02:00
co-authored by claude
parent df09271e81
commit a413648be9
12 changed files with 436 additions and 49 deletions
@@ -0,0 +1,151 @@
package org.saturnclient.ui.screens;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.saturnclient.common.ref.asset.IdentifierRef;
import org.saturnclient.config.AnimationConfig;
import org.saturnclient.config.Config;
import org.saturnclient.config.Theme;
import org.saturnclient.config.property.ColorProperty;
import org.saturnclient.config.property.NamespaceProperty;
import org.saturnclient.config.property.Property;
import org.saturnclient.ui.PanelScreen;
import org.saturnclient.ui.components.ColorGrid;
import org.saturnclient.ui.components.PropertyList;
import org.saturnclient.ui.components.SectionNav;
import org.saturnclient.ui.components.Sidebar;
import org.saturnclient.ui.components.ThemePresetCard;
import org.saturnclient.ui.elements.ActionButton;
import org.saturnclient.ui.elements.Divider;
import org.saturnclient.ui.elements.Label;
import org.saturnclient.ui.elements.Scroll;
import org.saturnclient.ui.resources.Textures;
/**
* The client's own settings, split into sections picked from a list on the left.
*/
public class ClientSettings extends PanelScreen {
private enum Section {
GENERAL("General", "Title screen, emotes and cosmetics", Textures.ICON_SLIDERS),
UI_STYLE("UI Style", "Colors, corners and font for every Saturn screen", Textures.ICON_PALETTE),
ANIMATIONS("Animations", "How long menu animations take, and their easing", Textures.ICON_ANIMATIONS);
final String title;
final String description;
final IdentifierRef icon;
Section(String title, String description, IdentifierRef icon) {
this.title = title;
this.description = description;
this.icon = icon;
}
}
private static final int NAV_WIDTH = 170;
private static final int NAV_GAP = 16;
private static final int LIST_Y = 58;
private static final int SECTION_GAP = 22;
private static final int PRESET_GAP = 10;
private static final int SCROLLBAR_SPACE = 14;
private Section section = Section.GENERAL;
public ClientSettings() {
super("Settings", Sidebar.SETTINGS);
}
@Override
protected void panel() {
List<SectionNav.Item> items = Stream.of(Section.values())
.map(s -> new SectionNav.Item(s.icon, s.title))
.toList();
draw(new SectionNav("SATURN CLIENT", items, section.ordinal(), NAV_WIDTH, i -> {
section = Section.values()[i];
init();
}).position(contentX, contentY + 4));
int dividerX = contentX + NAV_WIDTH + NAV_GAP;
draw(new Divider(1).dimensions(1, panelHeight).position(dividerX, panelY));
int x = dividerX + 1 + NAV_GAP + 4;
int width = contentX + contentWidth - x;
// Header
draw(new Label(() -> section.title, 1.15f, () -> Theme.FONT.value, () -> Theme.FOREGROUND.value)
.position(x, contentY + 2));
draw(new Label(() -> section.description, 0.66f, Theme::getBodyFont, Theme::getMutedFg)
.position(x, contentY + 28));
ActionButton reset = new ActionButton(Textures.ICON_RESET, "Reset", true, this::reset);
draw(reset.position(x + width - reset.width, contentY + 4));
// The section's settings
Scroll scroll = new Scroll(0).noBackground();
switch (section) {
case GENERAL -> PropertyList.draw(scroll, getGeneralProperties(), Set.of(), width, 0);
case UI_STYLE -> drawUiStyle(scroll, width);
case ANIMATIONS -> PropertyList.draw(scroll, AnimationConfig.config.getProperties(), Set.of(), width, 0);
}
draw(scroll.dimensions(width + SCROLLBAR_SPACE, contentHeight - LIST_Y).position(x, contentY + LIST_Y));
}
private void drawUiStyle(Scroll scroll, int width) {
Map<String, Property> properties = Theme.config.getProperties();
int y = PropertyList.drawSection(scroll, "Presets", 0) + 10;
int cardWidth = (width - PRESET_GAP * (Theme.PRESETS.size() - 1)) / Theme.PRESETS.size();
for (int i = 0; i < Theme.PRESETS.size(); i++) {
// Rebuild so the color inputs show the preset's colors
scroll.draw(new ThemePresetCard(Theme.PRESETS.get(i), cardWidth, this::init)
.position(i * (cardWidth + PRESET_GAP), y));
}
y += ThemePresetCard.HEIGHT + SECTION_GAP;
y = PropertyList.drawSection(scroll, "Colors", y) + 10;
y = ColorGrid.draw(scroll, properties, width, y) + SECTION_GAP;
y = PropertyList.drawSection(scroll, "Shape & type", y) + 4;
PropertyList.draw(scroll, properties, getNames(properties, p -> p instanceof ColorProperty), width, y);
}
/**
* The top-level settings. The namespaces have sections of their own.
*/
private static Map<String, Property> getGeneralProperties() {
return Config.config.getProperties().entrySet().stream()
.filter(e -> !(e.getValue() instanceof NamespaceProperty))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a,
LinkedHashMap::new));
}
private static Set<String> getNames(Map<String, Property> properties, Predicate<Property> filter) {
return properties.entrySet().stream()
.filter(e -> filter.test(e.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}
/**
* Puts the current section's settings back to their defaults.
*/
private void reset() {
Map<String, Property> properties = switch (section) {
case GENERAL -> getGeneralProperties();
case UI_STYLE -> Theme.config.getProperties();
case ANIMATIONS -> AnimationConfig.config.getProperties();
};
properties.values().forEach(Property::reset);
init();
}
}