From 9a59e25ec1a64b3d556e3313504c68c5519f35e0 Mon Sep 17 00:00:00 2001 From: mkpaz Date: Sat, 3 Sep 2022 21:42:59 +0400 Subject: [PATCH] Refactor changing accent color via pseudo classes --- .../sampler/layout/ApplicationWindow.java | 14 +- .../page/general/AccentColorSelector.java | 10 +- .../atlantafx/sampler/theme/AccentColor.java | 21 +-- .../atlantafx/sampler/theme/ColorMap.java | 146 ------------------ .../atlantafx/sampler/theme/ThemeManager.java | 58 +++---- .../styles/scss/general/_accent-colors.scss | 103 ++++++++++++ .../assets/styles/scss/general/_root.scss | 29 ++++ 7 files changed, 188 insertions(+), 193 deletions(-) delete mode 100644 sampler/src/main/java/atlantafx/sampler/theme/ColorMap.java create mode 100644 sampler/src/main/resources/assets/styles/scss/general/_accent-colors.scss diff --git a/sampler/src/main/java/atlantafx/sampler/layout/ApplicationWindow.java b/sampler/src/main/java/atlantafx/sampler/layout/ApplicationWindow.java index 458988d..a9995c1 100755 --- a/sampler/src/main/java/atlantafx/sampler/layout/ApplicationWindow.java +++ b/sampler/src/main/java/atlantafx/sampler/layout/ApplicationWindow.java @@ -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); } } diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/AccentColorSelector.java b/sampler/src/main/java/atlantafx/sampler/page/general/AccentColorSelector.java index 6e03377..96289cc 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/general/AccentColorSelector.java +++ b/sampler/src/main/java/atlantafx/sampler/page/general/AccentColorSelector.java @@ -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())); diff --git a/sampler/src/main/java/atlantafx/sampler/theme/AccentColor.java b/sampler/src/main/java/atlantafx/sampler/theme/AccentColor.java index 6c8ed30..f4dd5b2 100644 --- a/sampler/src/main/java/atlantafx/sampler/theme/AccentColor.java +++ b/sampler/src/main/java/atlantafx/sampler/theme/AccentColor.java @@ -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")); } } diff --git a/sampler/src/main/java/atlantafx/sampler/theme/ColorMap.java b/sampler/src/main/java/atlantafx/sampler/theme/ColorMap.java deleted file mode 100644 index f0aee14..0000000 --- a/sampler/src/main/java/atlantafx/sampler/theme/ColorMap.java +++ /dev/null @@ -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 allColors = new HashMap<>(); - private final Map dynamicColors = new HashMap<>(); - - private final Color primaryColor; - - private ColorMap(Color primaryColor) { - this.primaryColor = primaryColor; - } - - public Color getPrimaryColor() { - return primaryColor; - } - - public Map 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; - } -} diff --git a/sampler/src/main/java/atlantafx/sampler/theme/ThemeManager.java b/sampler/src/main/java/atlantafx/sampler/theme/ThemeManager.java index b96cad3..950a65b 100644 --- a/sampler/src/main/java/atlantafx/sampler/theme/ThemeManager.java +++ b/sampler/src/main/java/atlantafx/sampler/theme/ThemeManager.java @@ -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 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 diff --git a/sampler/src/main/resources/assets/styles/scss/general/_accent-colors.scss b/sampler/src/main/resources/assets/styles/scss/general/_accent-colors.scss new file mode 100644 index 0000000..ef2be10 --- /dev/null +++ b/sampler/src/main/resources/assets/styles/scss/general/_accent-colors.scss @@ -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); +} \ No newline at end of file diff --git a/sampler/src/main/resources/assets/styles/scss/general/_root.scss b/sampler/src/main/resources/assets/styles/scss/general/_root.scss index 3400f00..496f59c 100644 --- a/sampler/src/main/resources/assets/styles/scss/general/_root.scss +++ b/sampler/src/main/resources/assets/styles/scss/general/_root.scss @@ -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(); + } + } } \ No newline at end of file