feat(theme): enhance theme management with storage capabilities and add unit tests
This commit is contained in:
@@ -1,12 +1,20 @@
|
||||
package de.extio.game_engine.renderer;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import de.extio.game_engine.resource.StaticResource;
|
||||
import de.extio.game_engine.renderer.g2d.theme.Theme;
|
||||
|
||||
public interface ThemeManager {
|
||||
|
||||
List<String> getAvailableThemeNames();
|
||||
|
||||
Optional<Theme> loadThemeFromStorage(String themeName);
|
||||
|
||||
void saveThemeToStorage(Theme theme);
|
||||
|
||||
Optional<Theme> loadThemeFromStaticResource(StaticResource themeResource);
|
||||
|
||||
Theme getCurrentTheme();
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -26,6 +25,7 @@ import de.extio.game_engine.renderer.g2d.theme.Theme;
|
||||
import de.extio.game_engine.renderer.g2d.theme.UrbanThemeFactoryBean;
|
||||
import de.extio.game_engine.renderer.g2d.theme.VintageThemeFactoryBean;
|
||||
import de.extio.game_engine.resource.StaticResourceService;
|
||||
import de.extio.game_engine.storage.StorageService;
|
||||
|
||||
@AutoConfiguration
|
||||
@ConditionalOnProperty(name = "game-engine.renderer.enabled", havingValue = "true", matchIfMissing = true)
|
||||
@@ -113,10 +113,11 @@ public class G2DAutoConfiguration {
|
||||
@Bean
|
||||
G2DThemeManager g2dThemeManager(
|
||||
final StaticResourceService staticResourceService,
|
||||
final StorageService storageService,
|
||||
final List<PatternRenderer> patternRendererList,
|
||||
final Map<String, Theme> themes,
|
||||
@Qualifier("spacecraftTheme") final Theme defaultTheme) {
|
||||
return new G2DThemeManager(staticResourceService, patternRendererList, themes, defaultTheme);
|
||||
@Value("${game-engine.renderer.default-theme:urbanTheme}") final String defaultThemeName) {
|
||||
return new G2DThemeManager(staticResourceService, storageService, patternRendererList, themes, defaultThemeName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package de.extio.game_engine.renderer.g2d.theme;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -11,15 +13,21 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import de.extio.game_engine.renderer.ThemeManager;
|
||||
import de.extio.game_engine.renderer.g2d.G2DRenderer;
|
||||
import de.extio.game_engine.resource.StaticResource;
|
||||
import de.extio.game_engine.resource.StaticResourceService;
|
||||
import de.extio.game_engine.storage.StorageService;
|
||||
|
||||
public class G2DThemeManager implements ThemeManager {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(G2DThemeManager.class);
|
||||
|
||||
private static final List<String> THEME_STORAGE_PATH = List.of("gameEngine", "themes");
|
||||
private static final String THEME_STORAGE_NAME = "lastTheme";
|
||||
|
||||
private G2DRenderer g2dRenderer;
|
||||
|
||||
private final StaticResourceService staticResourceService;
|
||||
private final StorageService storageService;
|
||||
|
||||
private final Map<String, PatternRenderer> patternRenderers;
|
||||
private final Map<String, Theme> themes;
|
||||
@@ -29,20 +37,48 @@ public class G2DThemeManager implements ThemeManager {
|
||||
|
||||
public G2DThemeManager(
|
||||
final StaticResourceService staticResourceService,
|
||||
final StorageService storageService,
|
||||
final List<PatternRenderer> patternRendererList,
|
||||
final Map<String, Theme> themes,
|
||||
final Theme defaultTheme) {
|
||||
final String defaultThemeName) {
|
||||
this.staticResourceService = staticResourceService;
|
||||
this.storageService = storageService;
|
||||
this.patternRenderers = patternRendererList.stream()
|
||||
.collect(Collectors.toMap(
|
||||
renderer -> renderer.getClass().getSimpleName().substring(0, 1).toLowerCase() + renderer.getClass().getSimpleName().substring(1),
|
||||
Function.identity()));
|
||||
this.themes = Objects.requireNonNull(themes, "themes cannot be null");
|
||||
this.defaultTheme = Objects.requireNonNull(defaultTheme, "defaultTheme cannot be null");
|
||||
this.themes = new ConcurrentHashMap<>(Objects.requireNonNull(themes, "themes cannot be null"));
|
||||
this.defaultTheme = Objects.requireNonNull(this.themes.get(defaultThemeName), "defaultTheme cannot be null");
|
||||
|
||||
LOGGER.info("G2DThemeManager initialized with {} pattern renderers: {}", this.patternRenderers.size(), this.patternRenderers.keySet());
|
||||
LOGGER.info("G2DThemeManager initialized with {} themes: {}", this.themes.size(), this.themes.keySet());
|
||||
this.currentTheme = this.defaultTheme;
|
||||
this.loadLastUsedTheme();
|
||||
}
|
||||
|
||||
private void registerTheme(final Theme theme) {
|
||||
if (theme == null || theme.getName() == null || theme.getName().isBlank()) {
|
||||
return;
|
||||
}
|
||||
this.themes.put(theme.getName(), theme);
|
||||
}
|
||||
|
||||
private void loadLastUsedTheme() {
|
||||
if (this.storageService == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final Optional<Theme> persistedTheme = this.storageService.loadByPath(Theme.class, THEME_STORAGE_PATH, THEME_STORAGE_NAME);
|
||||
if (persistedTheme.isPresent()) {
|
||||
this.currentTheme = persistedTheme.get();
|
||||
this.registerTheme(this.currentTheme);
|
||||
LOGGER.info("Loaded last used theme: {}", this.currentTheme.getName());
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (final Exception e) {
|
||||
LOGGER.warn("Could not load last used theme. Using default.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,6 +90,44 @@ public class G2DThemeManager implements ThemeManager {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Theme> loadThemeFromStorage(final String themeName) {
|
||||
if (themeName == null || themeName.isBlank()) {
|
||||
throw new IllegalArgumentException("themeName cannot be null/blank");
|
||||
}
|
||||
if (this.storageService == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
final Optional<Theme> loaded = this.storageService.loadByPath(Theme.class, THEME_STORAGE_PATH, themeName);
|
||||
loaded.ifPresent(this::registerTheme);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveThemeToStorage(final Theme theme) {
|
||||
if (theme == null) {
|
||||
throw new IllegalArgumentException("theme cannot be null");
|
||||
}
|
||||
if (theme.getName() == null || theme.getName().isBlank()) {
|
||||
throw new IllegalArgumentException("theme.name cannot be null/blank");
|
||||
}
|
||||
if (this.storageService == null) {
|
||||
throw new IllegalStateException("storageService not available");
|
||||
}
|
||||
this.storageService.store(THEME_STORAGE_PATH, theme.getName(), theme);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Theme> loadThemeFromStaticResource(final StaticResource themeResource) {
|
||||
Objects.requireNonNull(themeResource, "themeResource");
|
||||
if (this.staticResourceService == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
final Optional<Theme> loaded = this.staticResourceService.loadByPath(Theme.class, themeResource);
|
||||
loaded.ifPresent(this::registerTheme);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCurrentTheme(final String themeName) {
|
||||
if (themeName == null || themeName.isBlank()) {
|
||||
@@ -94,14 +168,40 @@ public class G2DThemeManager implements ThemeManager {
|
||||
}
|
||||
|
||||
this.currentTheme = theme;
|
||||
this.registerTheme(theme);
|
||||
LOGGER.info("Theme switched to: {}", theme.getName());
|
||||
this.persistLastUsedTheme(theme);
|
||||
|
||||
this.g2dRenderer.reset();
|
||||
if (this.g2dRenderer != null) {
|
||||
this.g2dRenderer.reset();
|
||||
}
|
||||
}
|
||||
|
||||
private void persistLastUsedTheme(final Theme theme) {
|
||||
if (this.storageService == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.storageService.store(THEME_STORAGE_PATH, THEME_STORAGE_NAME, theme);
|
||||
}
|
||||
catch (final Exception e) {
|
||||
LOGGER.warn("Could not persist last used theme", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Theme loadTheme(final String themeName) {
|
||||
Objects.requireNonNull(this.staticResourceService, "staticResourceService");
|
||||
LOGGER.warn("Theme loading from StaticResourceService not yet implemented. Requested theme: {}. Using default theme.", themeName);
|
||||
try {
|
||||
final Optional<Theme> fromStorage = this.loadThemeFromStorage(themeName);
|
||||
if (fromStorage.isPresent()) {
|
||||
LOGGER.info("Loaded theme '{}' from storage", themeName);
|
||||
return fromStorage.get();
|
||||
}
|
||||
}
|
||||
catch (final Exception e) {
|
||||
LOGGER.warn("Could not load theme '{}' from storage. Using default.", themeName, e);
|
||||
}
|
||||
|
||||
LOGGER.warn("Theme '{}' not found in storage. Using default theme.", themeName);
|
||||
return this.defaultTheme;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ package de.extio.game_engine.renderer.g2d.theme;
|
||||
import java.awt.Color;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Represents a color in HSB (Hue, Saturation, Brightness) color space.
|
||||
* This allows for easier runtime color manipulation (e.g., brightening on hover).
|
||||
@@ -20,7 +23,11 @@ public final class HSBColor {
|
||||
* @param saturation Saturation component (0.0 to 1.0)
|
||||
* @param brightness Brightness component (0.0 to 1.0)
|
||||
*/
|
||||
public HSBColor(final float hue, final float saturation, final float brightness) {
|
||||
@JsonCreator
|
||||
public HSBColor(
|
||||
@JsonProperty("hue") final float hue,
|
||||
@JsonProperty("saturation") final float saturation,
|
||||
@JsonProperty("brightness") final float brightness) {
|
||||
this.hue = clamp(hue);
|
||||
this.saturation = clamp(saturation);
|
||||
this.brightness = clamp(brightness);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package de.extio.game_engine.renderer.g2d.theme;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Represents a complete UI theme with all color definitions and pattern renderer.
|
||||
* Themes are element-based rather than component-based, allowing consistent styling across different components.
|
||||
@@ -33,21 +36,54 @@ public class Theme {
|
||||
private final float hoverBrightnessAdjustment;
|
||||
private final float pressedBrightnessAdjustment;
|
||||
|
||||
@JsonCreator
|
||||
public Theme(
|
||||
@JsonProperty("name") final String name,
|
||||
@JsonProperty("patternRendererName") final String patternRendererName,
|
||||
@JsonProperty("borderOuter") final HSBColor borderOuter,
|
||||
@JsonProperty("borderInner") final HSBColor borderInner,
|
||||
@JsonProperty("borderInnerDisabled") final HSBColor borderInnerDisabled,
|
||||
@JsonProperty("backgroundNormal") final HSBColor backgroundNormal,
|
||||
@JsonProperty("backgroundSelected") final HSBColor backgroundSelected,
|
||||
@JsonProperty("textNormal") final HSBColor textNormal,
|
||||
@JsonProperty("textDisabled") final HSBColor textDisabled,
|
||||
@JsonProperty("selectionPrimary") final HSBColor selectionPrimary,
|
||||
@JsonProperty("selectionSecondary") final HSBColor selectionSecondary,
|
||||
@JsonProperty("windowBackground") final HSBColor windowBackground,
|
||||
@JsonProperty("hoverBrightnessAdjustment") final float hoverBrightnessAdjustment,
|
||||
@JsonProperty("pressedBrightnessAdjustment") final float pressedBrightnessAdjustment) {
|
||||
this.name = name;
|
||||
this.patternRendererName = patternRendererName;
|
||||
this.borderOuter = borderOuter;
|
||||
this.borderInner = borderInner;
|
||||
this.borderInnerDisabled = borderInnerDisabled;
|
||||
this.backgroundNormal = backgroundNormal;
|
||||
this.backgroundSelected = backgroundSelected;
|
||||
this.textNormal = textNormal;
|
||||
this.textDisabled = textDisabled;
|
||||
this.selectionPrimary = selectionPrimary;
|
||||
this.selectionSecondary = selectionSecondary;
|
||||
this.windowBackground = windowBackground;
|
||||
this.hoverBrightnessAdjustment = hoverBrightnessAdjustment;
|
||||
this.pressedBrightnessAdjustment = pressedBrightnessAdjustment;
|
||||
}
|
||||
|
||||
private Theme(final Builder builder) {
|
||||
this.name = builder.name;
|
||||
this.patternRendererName = builder.patternRendererName;
|
||||
this.borderOuter = builder.borderOuter;
|
||||
this.borderInner = builder.borderInner;
|
||||
this.borderInnerDisabled = builder.borderInnerDisabled;
|
||||
this.backgroundNormal = builder.backgroundNormal;
|
||||
this.backgroundSelected = builder.backgroundSelected;
|
||||
this.textNormal = builder.textNormal;
|
||||
this.textDisabled = builder.textDisabled;
|
||||
this.selectionPrimary = builder.selectionPrimary;
|
||||
this.selectionSecondary = builder.selectionSecondary;
|
||||
this.windowBackground = builder.windowBackground;
|
||||
this.hoverBrightnessAdjustment = builder.hoverBrightnessAdjustment;
|
||||
this.pressedBrightnessAdjustment = builder.pressedBrightnessAdjustment;
|
||||
this(
|
||||
builder.name,
|
||||
builder.patternRendererName,
|
||||
builder.borderOuter,
|
||||
builder.borderInner,
|
||||
builder.borderInnerDisabled,
|
||||
builder.backgroundNormal,
|
||||
builder.backgroundSelected,
|
||||
builder.textNormal,
|
||||
builder.textDisabled,
|
||||
builder.selectionPrimary,
|
||||
builder.selectionSecondary,
|
||||
builder.windowBackground,
|
||||
builder.hoverBrightnessAdjustment,
|
||||
builder.pressedBrightnessAdjustment);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
|
||||
Reference in New Issue
Block a user