Refactor changing accent color via pseudo classes

This commit is contained in:
mkpaz 2022-09-03 21:42:59 +04:00
parent c102d25196
commit 9a59e25ec1
7 changed files with 188 additions and 193 deletions

@ -1,14 +1,24 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.layout;
import atlantafx.sampler.util.Containers;
import javafx.geometry.Insets;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
public class ApplicationWindow extends StackPane {
public class ApplicationWindow extends AnchorPane {
public ApplicationWindow() {
getChildren().setAll(
// this is the place to apply user custom CSS,
// one level below the ':root'
var body = new StackPane();
body.getStyleClass().add("body");
body.getChildren().setAll(
new Overlay(),
new MainLayer()
);
Containers.setAnchors(body, Insets.EMPTY);
getChildren().setAll(body);
}
}

@ -28,9 +28,9 @@ public class AccentColorSelector extends HBox {
setAlignment(Pos.CENTER_LEFT);
getChildren().setAll(
colorButton(AccentColor.PURPLE),
colorButton(AccentColor.PINK),
colorButton(AccentColor.CORAL),
colorButton(AccentColor.primerPurple()),
colorButton(AccentColor.primerPink()),
colorButton(AccentColor.primerCoral()),
resetBtn
);
getStyleClass().add("accent-color-selector");
@ -40,11 +40,9 @@ public class AccentColorSelector extends HBox {
var icon = new Region();
icon.getStyleClass().add("icon");
var colorMap = accentColor.getColorMap();
var btn = new Button("", icon);
btn.getStyleClass().addAll(BUTTON_ICON, FLAT, "color-button");
btn.setStyle("-color-primary:" + JColorUtils.toHexWithAlpha(colorMap.getPrimaryColor()) + ";");
btn.setStyle("-color-primary:" + JColorUtils.toHexWithAlpha(accentColor.primaryColor()) + ";");
btn.setUserData(accentColor);
btn.setOnAction(e -> ThemeManager.getInstance().setAccentColor((AccentColor) btn.getUserData()));

@ -1,18 +1,19 @@
package atlantafx.sampler.theme;
public enum AccentColor {
import javafx.css.PseudoClass;
import javafx.scene.paint.Color;
PURPLE(ColorMap.primerPurple()),
PINK(ColorMap.primerPink()),
CORAL(ColorMap.primerCoral());
public record AccentColor(Color primaryColor, PseudoClass pseudoClass) {
private final ColorMap colorMap;
AccentColor(ColorMap colorMap) {
this.colorMap = colorMap;
public static AccentColor primerPurple() {
return new AccentColor(Color.web("#8250df"), PseudoClass.getPseudoClass("accent-primer-purple"));
}
public ColorMap getColorMap() {
return colorMap;
public static AccentColor primerPink() {
return new AccentColor(Color.web("#bf3989"), PseudoClass.getPseudoClass("accent-primer-pink"));
}
public static AccentColor primerCoral() {
return new AccentColor(Color.web("#c4432b"), PseudoClass.getPseudoClass("accent-primer-coral"));
}
}

@ -1,146 +0,0 @@
package atlantafx.sampler.theme;
import javafx.scene.paint.Color;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import static atlantafx.sampler.util.JColorUtils.opaqueColor;
// Theoretically, one should use different accent shades for each color theme
// and for both light and dark mode. But since creating color palettes is
// pretty time-consuming, dynamic color calculation based on opacity should
// suit demo purposes.
public class ColorMap {
private static final String MUTED_COLOR_NAME = "-color-accent-muted";
private static final String SUBTLE_COLOR_NAME = "-color-accent-subtle";
private final Map<String, Color> allColors = new HashMap<>();
private final Map<String, Color> dynamicColors = new HashMap<>();
private final Color primaryColor;
private ColorMap(Color primaryColor) {
this.primaryColor = primaryColor;
}
public Color getPrimaryColor() {
return primaryColor;
}
public Map<String, Color> getAll() {
return new HashMap<>(allColors);
}
private void setColor(String colorName, Color colorValue) {
allColors.put(Objects.requireNonNull(colorName), colorValue);
}
private void setDynamicColor(String colorName, Color colorValue) {
dynamicColors.put(Objects.requireNonNull(colorName), colorValue);
}
void update(Color background) {
allColors.put(MUTED_COLOR_NAME, opaqueColor(background, dynamicColors.get(MUTED_COLOR_NAME), 0.5));
allColors.put(SUBTLE_COLOR_NAME, opaqueColor(background, dynamicColors.get(SUBTLE_COLOR_NAME), 0.4));
}
///////////////////////////////////////////////////////////////////////////
public static ColorMap primerPurple() {
var map = new ColorMap(Color.web("#8250df"));
map.setColor("-color-accent-0", Color.web("#fbefff"));
map.setColor("-color-accent-1", Color.web("#ecd8ff"));
map.setColor("-color-accent-2", Color.web("#d8b9ff"));
map.setColor("-color-accent-3", Color.web("#c297ff"));
map.setColor("-color-accent-4", Color.web("#a475f9"));
map.setColor("-color-accent-5", Color.web("#8250df"));
map.setColor("-color-accent-6", Color.web("#6639ba"));
map.setColor("-color-accent-7", Color.web("#512a97"));
map.setColor("-color-accent-8", Color.web("#3e1f79"));
map.setColor("-color-accent-9", Color.web("#2e1461"));
map.setColor("-color-accent-fg", Color.web("#8250df"));
map.setColor("-color-accent-emphasis", Color.web("#8250df"));
map.setColor(MUTED_COLOR_NAME, Color.web("#d8b9ff"));
map.setColor(SUBTLE_COLOR_NAME, Color.web("#fbefff"));
map.setDynamicColor(MUTED_COLOR_NAME, Color.web("#d8b9ff"));
map.setDynamicColor(SUBTLE_COLOR_NAME, Color.web("#fbefff"));
return map;
}
public static ColorMap primerPink() {
var map = new ColorMap(Color.web("#bf3989"));
map.setColor("-color-accent-0", Color.web("#ffeff7"));
map.setColor("-color-accent-1", Color.web("#ffd3eb"));
map.setColor("-color-accent-2", Color.web("#ffadda"));
map.setColor("-color-accent-3", Color.web("#ff80c8"));
map.setColor("-color-accent-4", Color.web("#e85aad"));
map.setColor("-color-accent-5", Color.web("#bf3989"));
map.setColor("-color-accent-6", Color.web("#99286e"));
map.setColor("-color-accent-7", Color.web("#772057"));
map.setColor("-color-accent-8", Color.web("#611347"));
map.setColor("-color-accent-9", Color.web("#4d0336"));
map.setColor("-color-accent-fg", Color.web("#bf3989"));
map.setColor("-color-accent-emphasis", Color.web("#bf3989"));
map.setColor(MUTED_COLOR_NAME, Color.web("#ffadda"));
map.setColor(SUBTLE_COLOR_NAME, Color.web("#ffeff7"));
map.setDynamicColor(MUTED_COLOR_NAME, Color.web("#ffadda"));
map.setDynamicColor(SUBTLE_COLOR_NAME, Color.web("#ffeff7"));
return map;
}
public static ColorMap primerCoral() {
var map = new ColorMap(Color.web("#c4432b"));
map.setColor("-color-accent-0", Color.web("#fff0eb"));
map.setColor("-color-accent-1", Color.web("#ffd6cc"));
map.setColor("-color-accent-2", Color.web("#ffb4a1"));
map.setColor("-color-accent-3", Color.web("#fd8c73"));
map.setColor("-color-accent-4", Color.web("#ec6547"));
map.setColor("-color-accent-5", Color.web("#c4432b"));
map.setColor("-color-accent-6", Color.web("#9e2f1c"));
map.setColor("-color-accent-7", Color.web("#801f0f"));
map.setColor("-color-accent-8", Color.web("#691105"));
map.setColor("-color-accent-9", Color.web("#510901"));
map.setColor("-color-accent-fg", Color.web("#c4432b"));
map.setColor("-color-accent-emphasis", Color.web("#c4432b"));
map.setColor(MUTED_COLOR_NAME, Color.web("#ffb4a1"));
map.setColor(SUBTLE_COLOR_NAME, Color.web("#fff0eb"));
map.setDynamicColor(MUTED_COLOR_NAME, Color.web("#ffb4a1"));
map.setDynamicColor(SUBTLE_COLOR_NAME, Color.web("#fff0eb"));
return map;
}
// empty map contains only color names and used to reset
// accent color scale to its initial state
static ColorMap empty() {
var map = new ColorMap(Color.web("#bf3989"));
map.setColor("-color-accent-0", null);
map.setColor("-color-accent-1", null);
map.setColor("-color-accent-2", null);
map.setColor("-color-accent-3", null);
map.setColor("-color-accent-4", null);
map.setColor("-color-accent-5", null);
map.setColor("-color-accent-6", null);
map.setColor("-color-accent-7", null);
map.setColor("-color-accent-8", null);
map.setColor("-color-accent-9", null);
map.setColor("-color-accent-fg", null);
map.setColor("-color-accent-emphasis", null);
map.setColor(MUTED_COLOR_NAME, null);
map.setColor(SUBTLE_COLOR_NAME, null);
return map;
}
}

@ -12,6 +12,7 @@ import atlantafx.sampler.util.JColor;
import javafx.application.Application;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import java.net.URI;
@ -25,6 +26,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
public final class ThemeManager {
private static final String DUMMY_STYLESHEET = getResource("assets/styles/empty.css").toString();
private static final PseudoClass DARK = PseudoClass.getPseudoClass("dark");
private static final PseudoClass USER_CUSTOM = PseudoClass.getPseudoClass("user-custom");
private static final EventBus EVENT_BUS = DefaultEventBus.getInstance();
@ -39,6 +41,8 @@ public final class ThemeManager {
private final Map<String, String> customCSSRules = new LinkedHashMap<>(); // .foo | -fx-property: value;
private Scene scene;
private Pane body;
private Theme currentTheme = null;
private String fontFamily = DEFAULT_FONT_FAMILY_NAME;
private int fontSize = DEFAULT_FONT_SIZE;
@ -91,10 +95,14 @@ public final class ThemeManager {
Application.setUserAgentStylesheet(Objects.requireNonNull(theme.getUserAgentStylesheet()));
if (currentTheme != null) {
scene.getStylesheets().removeIf(url -> currentTheme.getStylesheets().contains(URI.create(url)));
getScene().getStylesheets().removeIf(url -> currentTheme.getStylesheets().contains(URI.create(url)));
}
theme.getStylesheets().forEach(uri -> scene.getStylesheets().add(uri.toString()));
theme.getStylesheets().forEach(uri -> getScene().getStylesheets().add(uri.toString()));
getScene().getRoot().pseudoClassStateChanged(DARK, theme.isDarkMode());
// remove user CSS customizations and reset accent on theme change
resetAccentColor();
resetCustomCSS();
currentTheme = theme;
@ -126,9 +134,9 @@ public final class ThemeManager {
public void setFontSize(int size) {
if (!SUPPORTED_FONT_SIZE.contains(size)) {
throw new IllegalArgumentException(String.format("Font size must in the range %d-%dpx. Actual value is %d.",
SUPPORTED_FONT_SIZE.get(0),
SUPPORTED_FONT_SIZE.get(SUPPORTED_FONT_SIZE.size() - 1),
size
SUPPORTED_FONT_SIZE.get(0),
SUPPORTED_FONT_SIZE.get(SUPPORTED_FONT_SIZE.size() - 1),
size
));
}
@ -157,8 +165,8 @@ public final class ThemeManager {
public void setZoom(int zoom) {
if (!SUPPORTED_ZOOM.contains(zoom)) {
throw new IllegalArgumentException(String.format("Zoom value must one of %s. Actual value is %d.",
SUPPORTED_ZOOM,
zoom
SUPPORTED_ZOOM,
zoom
));
}
@ -173,28 +181,22 @@ public final class ThemeManager {
public void setAccentColor(AccentColor color) {
Objects.requireNonNull(color);
var colorMap = color.getColorMap();
// adapt color map to the current theme
if (!getTheme().isDarkMode()) {
colorMap.update(Color.WHITE);
} else {
colorMap.update(Color.BLACK);
if (accentColor != null) {
getScene().getRoot().pseudoClassStateChanged(accentColor.pseudoClass(), false);
}
applyColorMap(colorMap);
getScene().getRoot().pseudoClassStateChanged(color.pseudoClass(), true);
this.accentColor = color;
reloadCustomCSS();
EVENT_BUS.publish(new ThemeEvent(EventType.COLOR_CHANGE));
}
public void resetAccentColor() {
applyColorMap(ColorMap.empty());
this.accentColor = null;
if (accentColor != null) {
getScene().getRoot().pseudoClassStateChanged(accentColor.pseudoClass(), false);
accentColor = null;
}
reloadCustomCSS();
EVENT_BUS.publish(new ThemeEvent(EventType.COLOR_CHANGE));
}
@ -256,10 +258,6 @@ public final class ThemeManager {
}
}
private void applyColorMap(ColorMap colorMap) {
colorMap.getAll().forEach(this::setOrRemoveColor);
}
private void reloadCustomCSS() {
Objects.requireNonNull(scene);
StringBuilder css = new StringBuilder();
@ -277,7 +275,9 @@ public final class ThemeManager {
css.append("}\n");
customCSSRules.forEach((k, v) -> {
css.append(".root:");
// custom CSS is applied to the body,
// thus it has a preference over accent color
css.append(".body:");
css.append(USER_CUSTOM.getPseudoClassName());
css.append(" ");
css.append(k);
@ -286,17 +286,17 @@ public final class ThemeManager {
css.append("}\n");
});
scene.getStylesheets().removeIf(uri -> uri.startsWith("data:text/css"));
scene.getStylesheets().add(
getScene().getRoot().getStylesheets().removeIf(uri -> uri.startsWith("data:text/css"));
getScene().getRoot().getStylesheets().add(
"data:text/css;base64," + Base64.getEncoder().encodeToString(css.toString().getBytes(UTF_8))
);
scene.getRoot().pseudoClassStateChanged(USER_CUSTOM, true);
getScene().getRoot().pseudoClassStateChanged(USER_CUSTOM, true);
}
public void resetCustomCSS() {
customCSSDeclarations.clear();
customCSSRules.clear();
scene.getRoot().pseudoClassStateChanged(USER_CUSTOM, false);
getScene().getRoot().pseudoClassStateChanged(USER_CUSTOM, false);
}
@SafeVarargs

@ -0,0 +1,103 @@
@use "sass:color";
@mixin primerPurpleLight() {
-color-accent-0: #fbefff;
-color-accent-1: #ecd8ff;
-color-accent-2: #d8b9ff;
-color-accent-3: #c297ff;
-color-accent-4: #a475f9;
-color-accent-5: #8250df;
-color-accent-6: #6639ba;
-color-accent-7: #512a97;
-color-accent-8: #3e1f79;
-color-accent-9: #2e1461;
-color-accent-fg: #8250df;
-color-accent-emphasis: #8250df;
-color-accent-muted: color.change(#c297ff, $alpha: 0.4);
-color-accent-subtle: #fbefff;
}
@mixin primerPinkLight() {
-color-accent-0: #ffeff7;
-color-accent-1: #ffd3eb;
-color-accent-2: #ffadda;
-color-accent-3: #ff80c8;
-color-accent-4: #e85aad;
-color-accent-5: #bf3989;
-color-accent-6: #99286e;
-color-accent-7: #772057;
-color-accent-8: #611347;
-color-accent-9: #4d0336;
-color-accent-fg: #bf3989;
-color-accent-emphasis: #bf3989;
-color-accent-muted: color.change(#ff80c8, $alpha: 0.4);
-color-accent-subtle: #ffeff7;
}
@mixin primerCoralLight() {
-color-accent-0: #fff0eb;
-color-accent-1: #ffd6cc;
-color-accent-2: #ffb4a1;
-color-accent-3: #fd8c73;
-color-accent-4: #ec6547;
-color-accent-5: #c4432b;
-color-accent-6: #9e2f1c;
-color-accent-7: #801f0f;
-color-accent-8: #691105;
-color-accent-9: #510901;
-color-accent-fg: #c4432b;
-color-accent-emphasis: #c4432b;
-color-accent-muted: color.change(#fd8c73, $alpha: 0.4);
-color-accent-subtle: #fff0eb;
}
@mixin primerPurpleDark() {
-color-accent-0: #eddeff;
-color-accent-1: #e2c5ff;
-color-accent-2: #d2a8ff;
-color-accent-3: #bc8cff;
-color-accent-4: #a371f7;
-color-accent-5: #8957e5;
-color-accent-6: #6e40c9;
-color-accent-7: #553098;
-color-accent-8: #3c1e70;
-color-accent-9: #271052;
-color-accent-fg: #bc8cff;
-color-accent-emphasis: #8957e5;
-color-accent-muted: color.change(#a371f7, $alpha: 0.4);
-color-accent-subtle: color.change(#a371f7, $alpha: 0.15);
}
@mixin primerPinkDark() {
-color-accent-0: #ffdaec;
-color-accent-1: #ffbedd;
-color-accent-2: #ff9bce;
-color-accent-3: #f778ba;
-color-accent-4: #db61a2;
-color-accent-5: #bf4b8a;
-color-accent-6: #9e3670;
-color-accent-7: #7d2457;
-color-accent-8: #5e103e;
-color-accent-9: #42062a;
-color-accent-fg: #f778ba;
-color-accent-emphasis: #bf4b8a;
-color-accent-muted: color.change(#db61a2, $alpha: 0.4);
-color-accent-subtle: color.change(#db61a2, $alpha: 0.15);
}
@mixin primerCoralDark() {
-color-accent-0: #ffddd2;
-color-accent-1: #ffc2b2;
-color-accent-2: #ffa28b;
-color-accent-3: #f78166;
-color-accent-4: #ea6045;
-color-accent-5: #cf462d;
-color-accent-6: #ac3220;
-color-accent-7: #872012;
-color-accent-8: #640d04;
-color-accent-9: #460701;
-color-accent-fg: #f78166;
-color-accent-emphasis: #cf462d;
-color-accent-muted: color.change(#ea6045, $alpha: 0.4);
-color-accent-subtle: color.change(#ea6045, $alpha: 0.15);
}

@ -1,5 +1,7 @@
// SPDX-License-Identifier: MIT
@use "accent-colors" as ac;
@mixin hide() {
-fx-min-width: 0;
-fx-pref-width: 0;
@ -35,4 +37,31 @@
}
}
}
// accent colors
&:accent-primer-purple {
@include ac.primerPurpleLight();
}
&:accent-primer-pink {
@include ac.primerPinkLight();
}
&:accent-primer-coral {
@include ac.primerCoralLight();
}
&:dark {
&:accent-primer-purple {
@include ac.primerPurpleDark();
}
&:accent-primer-pink {
@include ac.primerPinkDark();
}
&:accent-primer-coral {
@include ac.primerCoralDark();
}
}
}