From 954a3259b703f880ab7e4419db0d719b3894bfc3 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:25:06 +0300 Subject: [PATCH 01/10] Add opt-in OS settings inheritance for native themes --- .../impl/CodenameOneImplementation.java | 13 + .../codename1/impl/NativeThemeSettings.java | 123 ++++++++ CodenameOne/src/com/codename1/ui/Display.java | 28 ++ CodenameOne/src/com/codename1/ui/Font.java | 23 ++ .../src/com/codename1/ui/plaf/UIManager.java | 244 +++++++++++++-- Ports/Android/src/AndroidMaterialTheme.res | Bin 178731 -> 199579 bytes .../impl/android/AndroidImplementation.java | 57 ++++ .../impl/android/CodenameOneActivity.java | 7 + .../com/codename1/impl/javase/JavaSEPort.java | 94 ++++++ Ports/LinuxPort/nativeSources/cn1_linux.h | 3 +- .../nativeSources/cn1_linux_window.c | 64 ++++ .../impl/linux/LinuxImplementation.java | 10 + .../com/codename1/impl/linux/LinuxNative.java | 2 + Ports/WindowsPort/nativeSources/cn1_windows.h | 3 +- .../nativeSources/cn1_windows_window.cpp | 6 + .../nativeSources/cn1_windows_winrt.cpp | 71 +++++ .../impl/windows/WindowsImplementation.java | 10 + .../codename1/impl/windows/WindowsNative.java | 2 + .../CodenameOne_GLViewController.m | 8 + Ports/iOSPort/nativeSources/IOSNative.m | 83 ++++++ .../nativeSources/iOSModern27Theme.res | Bin 208184 -> 213386 bytes .../iOSPort/nativeSources/iOSModernTheme.res | Bin 207421 -> 212623 bytes .../codename1/impl/ios/IOSImplementation.java | 11 + .../src/com/codename1/impl/ios/IOSNative.java | 1 + Themes/AndroidMaterialTheme.res | Bin 178731 -> 199579 bytes Themes/GnomeAdwaitaTheme.res | Bin 187721 -> 187753 bytes Themes/MacOSAquaTheme.res | Bin 169518 -> 169550 bytes Themes/WindowsFluentTheme.res | Bin 191465 -> 191497 bytes Themes/iOSModern27Theme.res | Bin 208184 -> 213386 bytes Themes/iOSModernTheme.res | Bin 207421 -> 212623 bytes docs/developer-guide/Native-Themes.asciidoc | 69 +++++ .../TestCodenameOneImplementation.java | 8 + .../ui/plaf/UIManagerNativeSettingsTest.java | 277 ++++++++++++++++++ .../NativeThemeSettingsSimulationTest.java | 60 ++++ native-themes/README.md | 14 + native-themes/android-material/theme.css | 242 +++++++-------- native-themes/gnome-adwaita/theme.css | 2 + native-themes/ios-modern/common.css | 48 +-- native-themes/macos-aqua/theme.css | 2 + native-themes/windows-fluent/theme.css | 2 + scripts/test_native_theme_settings.py | 151 ++++++++++ 41 files changed, 1575 insertions(+), 163 deletions(-) create mode 100644 CodenameOne/src/com/codename1/impl/NativeThemeSettings.java create mode 100644 maven/core-unittests/src/test/java/com/codename1/ui/plaf/UIManagerNativeSettingsTest.java create mode 100644 maven/javase/src/test/java/com/codename1/impl/javase/NativeThemeSettingsSimulationTest.java create mode 100644 scripts/test_native_theme_settings.py diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java index 5188948a974..ffec2d464df 100644 --- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java +++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java @@ -13182,6 +13182,19 @@ public boolean isAccessibilityTreeUpdateRequired() { return isAccessibilityTreeSupported(); } + /// Internal port hook. Return a fresh snapshot; unsupported ports keep bundled defaults. + public NativeThemeSettings getNativeThemeSettings() { + return new NativeThemeSettings(); + } + + /// Loads the selected OS family using the original native alias's weight/style. + /// A family of "native:" means the platform's existing native aliases. + public Object loadNativeThemeFont(String family, String template, float size, int style) { + String name = "native:".equals(family) ? template : family; + Object font = loadTrueTypeFont(name, null); + return font == null ? null : deriveTrueTypeFont(font, size, style); + } + /// Returns true if the user has selected larger type fonts in the system settings. /// Default implementation returns false. /// diff --git a/CodenameOne/src/com/codename1/impl/NativeThemeSettings.java b/CodenameOne/src/com/codename1/impl/NativeThemeSettings.java new file mode 100644 index 00000000000..cfa07611805 --- /dev/null +++ b/CodenameOne/src/com/codename1/impl/NativeThemeSettings.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl; + +import java.util.HashMap; +import java.util.Map; +import com.codename1.util.StringUtil; + +/// Internal port snapshot of OS theme defaults. Font size is in CN1 device pixels, +/// before accessibility scaling. Missing values mean unsupported, never a guessed value. +public final class NativeThemeSettings { + private final Map colors = new HashMap(); + private String fontFamily; + private float fontSize; + + public NativeThemeSettings color(String token, int rgb) { + colors.put(token, Integer.toHexString(0x1000000 | (rgb & 0xffffff)).substring(1)); + return this; + } + + /// Compares only the categories the application opted into. + public NativeThemeSettings forInheritance(boolean inheritColors, boolean inheritFonts) { + NativeThemeSettings result = new NativeThemeSettings(); + if (inheritColors) { result.colors.putAll(colors); } + if (inheritFonts) { result.font(fontFamily, fontSize); } + return result; + } + + public NativeThemeSettings copy() { + NativeThemeSettings result = new NativeThemeSettings(); + result.colors.putAll(colors); + result.fontFamily = fontFamily; + result.fontSize = fontSize; + return result; + } + + public String getColor(String token) { + return colors.get(token); + } + + public NativeThemeSettings font(String family, float pixels) { + if (family != null && family.length() > 0 && pixels > 0 && pixels < 10000) { + fontFamily = family; + fontSize = pixels; + } + return this; + } + + public String getFontFamily() { + return fontFamily; + } + + public float getFontSize() { + return fontSize; + } + + /// Parses the native bridge's newline-separated key=value snapshot. Unknown + /// and malformed entries are ignored so partially supported settings still work. + public static NativeThemeSettings parse(String encoded) { + NativeThemeSettings result = new NativeThemeSettings(); + if (encoded == null) { + return result; + } + String family = null; + float size = 0; + for (String line : StringUtil.tokenize(encoded, '\n')) { + int split = line.indexOf('='); + if (split <= 0) { + continue; + } + String key = line.substring(0, split); + String value = line.substring(split + 1); + try { + if ("fontFamily".equals(key)) { + family = value; + } else if ("fontSize".equals(key)) { + size = Float.parseFloat(value); + } else if (value.length() == 6) { + result.color(key, Integer.parseInt(value, 16)); + } + } catch (IllegalArgumentException ignored) { + // An unavailable or invalid setting keeps the bundled fallback. + } + } + return result.font(family, size); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof NativeThemeSettings)) { + return false; + } + NativeThemeSettings that = (NativeThemeSettings) other; + return colors.equals(that.colors) && Float.floatToIntBits(fontSize) == Float.floatToIntBits(that.fontSize) + && (fontFamily == null ? that.fontFamily == null : fontFamily.equals(that.fontFamily)); + } + + @Override + public int hashCode() { + return colors.hashCode() * 31 + (fontFamily == null ? 0 : fontFamily.hashCode()) + + Float.floatToIntBits(fontSize); + } +} diff --git a/CodenameOne/src/com/codename1/ui/Display.java b/CodenameOne/src/com/codename1/ui/Display.java index 6a86336ed94..8e49cb4f104 100644 --- a/CodenameOne/src/com/codename1/ui/Display.java +++ b/CodenameOne/src/com/codename1/ui/Display.java @@ -1025,6 +1025,34 @@ public void setDarkMode(Boolean darkMode) { this.darkMode = darkMode; } + /// Internal platform snapshot used by native-theme inheritance. + public com.codename1.impl.NativeThemeSettings getNativeThemeSettings() { + return impl.getNativeThemeSettings(); + } + + private boolean nativeThemeRefreshPending; + + /// Port notification for OS theme changes and resume. Coalesces onto the CN1 EDT. + public void nativeThemeSettingsChanged() { + if (!isInitialized()) { + return; + } + synchronized (this) { + if (nativeThemeRefreshPending) { + return; + } + nativeThemeRefreshPending = true; + } + callSerially(new Runnable() { + public void run() { + synchronized (Display.this) { + nativeThemeRefreshPending = false; + } + UIManager.getInstance().refreshNativeThemeSettings(); + } + }); + } + /// Returns true if the user has selected larger type fonts in the system settings. /// /// #### Returns diff --git a/CodenameOne/src/com/codename1/ui/Font.java b/CodenameOne/src/com/codename1/ui/Font.java index a9fcfbe75b7..e1edbae3219 100644 --- a/CodenameOne/src/com/codename1/ui/Font.java +++ b/CodenameOne/src/com/codename1/ui/Font.java @@ -154,6 +154,29 @@ public class Font extends CN { private static final Hashtable bitmapCache = new Hashtable(); private static final HashMap derivedFontCache = new HashMap(); + /// Internal theme hook: inherit only native UI fonts, never icon or bundled fonts. + public Font deriveNativeThemeFont(String family, float size) { + if (fontUniqueId == null || !fontUniqueId.startsWith("native:") || family == null) { + return this; + } + int style = getStyle(); + if (fontUniqueId.indexOf("Bold") >= 0 || fontUniqueId.indexOf("Black") >= 0) { + style |= STYLE_BOLD; + } + if (fontUniqueId.indexOf("Italic") >= 0) { + style |= STYLE_ITALIC; + } + Object nativeFont = Display.impl.loadNativeThemeFont(family, fontUniqueId, size, style); + if (nativeFont == null) { + return this; + } + Font result = new Font(nativeFont); + result.ttf = true; + result.pixelSize = size; + result.fontUniqueId = "os:" + family + ":" + fontUniqueId; + return result; + } + /// Clears the cache of derived TrueType fonts. Called when the theme changes so /// that fonts whose platform rendering depends on theme constants (e.g. a native /// theme's text letter spacing) are re-derived against the freshly-installed diff --git a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java index e1bb400dd76..a09c5f79e65 100644 --- a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java +++ b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java @@ -170,6 +170,167 @@ public static int getThemeGeneration() { private Style defaultStyle = new Style(); private Style defaultSelectedStyle = new Style(); private boolean useLargerTextScale; + private boolean useNativeColors; + private boolean useNativeFonts; + private boolean refreshingTheme; + private final java.util.Set nativeFontKeys = new java.util.HashSet(); + private final java.util.Set nativeColorKeys = new java.util.HashSet(); + private final java.util.Set nativeBorderKeys = new java.util.HashSet(); + private final java.util.Set applicationConstants = new java.util.HashSet(); + private final Map nativeOriginals = new HashMap(); + private final Map nativeDerived = new HashMap(); + private float nativeBaseFontSize; + private com.codename1.impl.NativeThemeSettings nativeSettings; + private float nativeSettingsTextScale = 1f; + private Boolean nativeSettingsDark; + + /// Whether native-theme colors inherit available OS settings. Defaults to false. + public boolean isUseNativeColors() { return useNativeColors; } + + /// Opts into OS colors. Call refreshTheme() to apply to an already loaded theme; + /// an explicit useNativeColorsBool theme constant takes precedence on rebuild. + public void setUseNativeColors(boolean enabled) { useNativeColors = enabled; } + + /// Whether native-theme typography inherits the OS family and normal base size. + public boolean isUseNativeFonts() { return useNativeFonts; } + + /// Opts into OS typography, independently of larger-text accessibility scaling. + /// Call refreshTheme() to apply; useNativeFontsBool takes precedence on rebuild. + public void setUseNativeFonts(boolean enabled) { useNativeFonts = enabled; } + + /// Internal EDT entry point for coalesced platform settings notifications. + public void refreshNativeThemeSettings() { + if (!useNativeColors && !useNativeFonts && !useLargerTextScale) { + return; + } + Display display = Display.getInstance(); + com.codename1.impl.NativeThemeSettings next = display.getNativeThemeSettings().forInheritance(useNativeColors, useNativeFonts); + float scale = getEffectiveLargerTextScale(); + Boolean dark = display.isDarkMode(); + if (next.equals(nativeSettings) + && Float.floatToIntBits(scale) == Float.floatToIntBits(nativeSettingsTextScale) + && (dark == null ? nativeSettingsDark == null : dark.equals(nativeSettingsDark))) { + return; + } + refreshTheme(); + com.codename1.ui.Form form = display.getCurrent(); + if (form != null) { + form.refreshTheme(true); + revalidateNativeThemeTree(form); + form.repaint(); + } + for (com.codename1.ui.Window window : com.codename1.ui.Desktop.getInstance().getWindows()) { + window.refreshTheme(true); + revalidateNativeThemeTree(window); + window.repaint(); + } + } + + private void revalidateNativeThemeTree(com.codename1.ui.Container container) { + for (int i = 0; i < container.getComponentCount(); i++) { + Component child = container.getComponentAt(i); + if (child instanceof com.codename1.ui.Container) { + revalidateNativeThemeTree((com.codename1.ui.Container) child); + } + } + container.revalidate(); + } + + private void restoreNativeThemeValues() { + for (Map.Entry entry : nativeOriginals.entrySet()) { + if (themeProps.get(entry.getKey()) == nativeDerived.get(entry.getKey())) { + themeProps.put(entry.getKey(), entry.getValue()); + } + } + nativeOriginals.clear(); + nativeDerived.clear(); + } + + private void recordNativeThemeOwnership(Hashtable incoming) { + if (refreshingTheme) { + return; + } + boolean nativeLayer = "true".equals(incoming.get("@nativeThemeDefaultsBool")); + if (nativeLayer) { + Object base = incoming.get("Label.font"); + if (!(base instanceof Font)) { base = incoming.get("font"); } + if (base instanceof Font) { nativeBaseFontSize = nativeFontSize((Font) base); } + } + for (Object item : incoming.keySet()) { + String key = (String) item; + if (key.startsWith("@")) { + if (!nativeLayer) { + applicationConstants.add(key.substring(1)); + } + } else if (nativeLayer) { + if (incoming.get(key) instanceof Font) { + nativeFontKeys.add(key); + } + if (key.endsWith("border")) { nativeBorderKeys.add(key); } + if (incoming.containsKey("@cn1-bind:" + key)) { + nativeColorKeys.add(key); + } + } else { + nativeFontKeys.remove(key); + nativeColorKeys.remove(key); + nativeBorderKeys.remove(key); + } + } + } + + private void applyNativeThemeSettings() { + Boolean colors = isThemeConstant("useNativeColorsBool"); + Boolean fonts = isThemeConstant("useNativeFontsBool"); + if (colors != null) { useNativeColors = colors.booleanValue(); } + if (fonts != null) { useNativeFonts = fonts.booleanValue(); } + if (!useNativeColors && !useNativeFonts && !useLargerTextScale) { + return; + } + Display display = Display.getInstance(); + nativeSettings = display.getNativeThemeSettings().forInheritance(useNativeColors, useNativeFonts); + nativeSettingsTextScale = getEffectiveLargerTextScale(); + nativeSettingsDark = display.isDarkMode(); + if (useNativeColors) { + for (String key : nativeColorKeys) { + Object binding = themeConstants.get("cn1-bind:" + key); + if (!(binding instanceof String) || applicationConstants.contains((String) binding)) { + continue; + } + String value = nativeSettings.getColor((String) binding); + if (value != null && themeProps.containsKey(key)) { + rememberNativeValue(key, value); + syncBoundRoundBorderColor(key, value); + } + } + } + if (useNativeFonts && nativeBaseFontSize > 0 && nativeSettings.getFontFamily() != null) { + for (String key : nativeFontKeys) { + Object value = themeProps.get(key); + if (!(value instanceof Font)) { continue; } + Font original = (Font) value; + float size = nativeFontSize(original); + if (size <= 0) { continue; } + try { + Font inherited = original.deriveNativeThemeFont(nativeSettings.getFontFamily(), + size * nativeSettings.getFontSize() / nativeBaseFontSize); + if (inherited != original) { rememberNativeValue(key, inherited); } + } catch (RuntimeException ex) { + Log.e(ex); + } + } + } + } + + private float nativeFontSize(Font font) { + return font.getPixelSize() > 0 ? font.getPixelSize() : font.getHeight(); + } + + private void rememberNativeValue(String key, Object value) { + nativeOriginals.put(key, themeProps.get(key)); + nativeDerived.put(key, value); + themeProps.put(key, value); + } + /// Tracks the original (unscaled) Font we replaced in themeProps when /// [#applyLargerTextScaleToThemeFonts] last ran. Without this, each scale /// change derives from the previously-scaled font and compounds, so going @@ -1779,6 +1940,8 @@ private void resetThemeProps(Hashtable installedTheme) { /// - `themeProps`: the properties of the given theme public void addThemeProps(Hashtable themeProps) { if (accessible) { + restoreLargerTextFonts(); + restoreNativeThemeValues(); dropSupersededBindings(themeProps); buildTheme(themeProps); styles.clear(); @@ -1935,6 +2098,8 @@ public void refreshTheme() { if (!accessible || themeProps == null) { return; } + restoreLargerTextFonts(); + restoreNativeThemeValues(); Hashtable props = new Hashtable(); for (Map.Entry e : themeProps.entrySet()) { props.put(e.getKey(), e.getValue()); @@ -1949,7 +2114,12 @@ public void refreshTheme() { props.put("@" + e.getKey(), e.getValue()); } } - setThemePropsImpl(props); + refreshingTheme = true; + try { + setThemePropsImpl(props); + } finally { + refreshingTheme = false; + } } /// Returns a theme constant defined in the resource editor @@ -2070,6 +2240,15 @@ public Object getThemeMaskConstant(String constantName) { } void setThemePropsImpl(Hashtable themeProps) { + if (!refreshingTheme && buildThemeDepth == 0) { + nativeFontKeys.clear(); + nativeColorKeys.clear(); + nativeBorderKeys.clear(); + applicationConstants.clear(); + nativeOriginals.clear(); + nativeDerived.clear(); + nativeBaseFontSize = 0; + } resetThemeProps(themeProps); styles.clear(); themeConstants.clear(); @@ -2146,12 +2325,18 @@ private void buildThemeImpl(Hashtable themeProps) { // returning a paint derived under the previous (or no) theme. Font.clearDerivedFontCache(); String con = (String) themeProps.get("@includeNativeBool"); - if (con != null && "true".equalsIgnoreCase(con) && Display.getInstance().hasNativeTheme()) { + if (!refreshingTheme && con != null && "true".equalsIgnoreCase(con) && Display.getInstance().hasNativeTheme()) { boolean a = accessible; accessible = true; Display.getInstance().installNativeTheme(); accessible = a; } + recordNativeThemeOwnership(themeProps); + // A literal supplied by the app supersedes a native palette binding, including + // on initial @includeNativeBool composition (not only on addThemeProps). + if (!refreshingTheme && !"true".equals(themeProps.get("@nativeThemeDefaultsBool"))) { + dropSupersededBindings(themeProps); + } Enumeration e = themeProps.keys(); while (e.hasMoreElements()) { String key = (String) e.nextElement(); @@ -2164,6 +2349,27 @@ private void buildThemeImpl(Hashtable themeProps) { this.themeProps.put(key, themeProps.get(key)); } + String overlayThemes = (String) themeProps.get("@OverlayThemes"); + if (overlayThemes != null) { + java.util.List overlayThemesArr = StringUtil.tokenize(overlayThemes, ','); + for (String th : overlayThemesArr) { + th = th.trim(); + if (th.length() == 0) { + continue; + } + try { + Resources res = Resources.openLayered("/" + th); + boolean a = accessible; + accessible = true; + addThemeProps(res.getTheme(res.getThemeResourceNames()[0])); + accessible = a; + } catch (Exception ex) { + System.err.println("Failed to load overlay theme file specified by @overlayThemes theme constant: " + th); + Log.e(ex); + } + } + } + applyThemeBindings(); updateLargerTextScaleSettingFromTheme(); @@ -2194,6 +2400,8 @@ private void buildThemeImpl(Hashtable themeProps) { } if (buildThemeDepth == 1) { + restoreLargerTextFonts(); + applyNativeThemeSettings(); applyLargerTextScaleToThemeFonts(); } @@ -2208,27 +2416,6 @@ private void buildThemeImpl(Hashtable themeProps) { applyLargerTextScaleToDefaultStyles(); } - String overlayThemes = (String) themeProps.get("@OverlayThemes"); - if (overlayThemes != null) { - java.util.List overlayThemesArr = StringUtil.tokenize(overlayThemes, ','); - for (String th : overlayThemesArr) { - th = th.trim(); - if (th.length() == 0) { - continue; - } - try { - Resources res = Resources.openLayered("/" + th); - boolean a = accessible; - accessible = true; - addThemeProps(res.getTheme(res.getThemeResourceNames()[0])); - accessible = a; - } catch (Exception ex) { - System.err.println("Failed to load overlay theme file specified by @overlayThemes theme constant: " + th); - Log.e(ex); - } - } - } - // Everything above merged into themeProps, so the style-definition index // and the $Dark key list it carries describe the theme as it was BEFORE // this call and have to go. @@ -2315,6 +2502,11 @@ private void syncBoundRoundBorderColor(String themeKey, String colorValue) { return; } String borderKey = themeKey.substring(0, themeKey.length() - suffix.length()) + "border"; + // An app may replace a native border without replacing its background-color + // property. The inherited fill must not recolor that explicitly supplied border. + if (nativeColorKeys.contains(themeKey) && !nativeBorderKeys.contains(borderKey)) { + return; + } Object border = themeProps.get(borderKey); if (border instanceof RoundBorder) { ((RoundBorder) border).color(Integer.parseInt(colorValue, 16)); @@ -2403,7 +2595,7 @@ private Font scaleFontForLargerText(Font font, float scale) { /// Only [#buildTheme] at depth 1 may call this: the bookkeeping below spans /// the whole merged theme, and a nested `@includeNativeBool` install sees /// only the native theme's half of it. See [#buildThemeDepth]. - private void applyLargerTextScaleToThemeFonts() { + private void restoreLargerTextFonts() { // Roll back any prior scaling we applied so this pass always derives // from the original installed font. Without the rollback, repeated // refreshes compound (each scale multiplies the previously-derived @@ -2424,6 +2616,10 @@ private void applyLargerTextScaleToThemeFonts() { scaledFontDerived.clear(); } + } + + private void applyLargerTextScaleToThemeFonts() { + restoreLargerTextFonts(); float scale = getEffectiveLargerTextScale(); if (scale <= 1f) { return; diff --git a/Ports/Android/src/AndroidMaterialTheme.res b/Ports/Android/src/AndroidMaterialTheme.res index de3c0252c13345a6f6d13b04ebf591165cafe872..b500d0eee2126e80781f5bef08e806705799d476 100644 GIT binary patch delta 12454 zcma)Ce{j^*wa@o!vRSgb*-iFGvdQKb2?+!fLyOfQU?3rq5)40LQCo&=k|jnryD?dW z4#)6&v#${->esieV12s2lwUuc|QbBxZuYAEZCbt|NAN(c7A2e zb;d%QVl`dSaCD%iJ2>!2S~YyJ-_J-2*7r2B+E=A#u#Q^G94WekSGMuxclRsh;v1qv5a`hJ%Kw!0k4BJ8GN(C*41 zrLzPJTy{-Uwq$xTR4t@KUCy+5DQjT|>8}>*dSr2QQ?1tb(43nmMbys^6hZTWneem2 zF0i3qRLKFt^P6nBjVIQO$|yNhue6p{Z^y!{l=`z>HieRC~ub7QTV239^po;<|6JGHIrq=G5r%*bi zY+n&ndcB%M)_YxKs86tFDX9ZPhXb%WFZcPVdYNU#=Qa6=a+qGC?x2PI$6CQ59Wp@= zp(_wBpO*AfIeb>`l>WH+Hs8&uW#OjtZQa34@=#E4X+0sGl35t-{&6ZGw611>O8Mcv z8Cj&iOVE3o#Xlt|VM6ff#)2F$R7Fwp!28fP=HeH6wN~~OGb6jNb#k>K-m3B~x{{GlD8Q0l1d)k?cTUip#1drwJSn zChqlqPZz4%xND2`H}&@PDxkr$fa*)G98cbvMn+YNZqh`khKW%Rw6)3CW+uKengJWy z+>%k|jZO#B<~M2=DIjCa+~wS zxw^^?XI9DaMx*0-izr72jZy!qV0UkH@rGC|8d1ejjMvP+uE|x$YQ@-Wu)pDgzuxa9 zH+%;DNBGSE-7ME>KXiz#dx@)1)Hfz9zrAbS z0|AnBCw%=A{mEO$W)gd~sA?8iSFmo(m21*QuN!*;_N>Fez{=&iF*{samkA{oGGXVw za_R4mbaNq;ydxdjE?8mySOt9ls0}uCBJ5~6YKNnpau9M~v>d&i_l7M;b4l?IF$EyS zoH^WO*SA*HN~wWkUDK5+#~Vpscm6E>9Wjj8srdcm_w%WZpPjEZ7ke@Np*x@KT%cMX zP85yoJbI6y0_#T~#$dg=KNvBne%Hr};j4%9^zQ`(`@E_s{5m|~7CRWuCec>Wz%WP> z>k|(^Yd?zhkw~IO?NaBU;?fd#IMf&l zYq}W*z|U{UAqzUhOudn@EB^MFT!0zNbDLanYLgx#w~htMsolb%rJ-PVXds1J+9r}^ zrFRQ`M{U;2U@3lJOMYC#gRQ4+r|6M`2EM{9+Oo!3jQuFFfhFOC)muGlzE!u$#ThqI zSPN{>vLQEoz7_e8=SAgEHe|=~bC=nQuFk}frK{Rn!CT@}MTfD85|Vqu zB7S~U4&@pJq@-xU)Gb9j9R)2K2nAE&k!mTnZ4}Xq`_U)7srIqROPQK`L*Z^U7mG&U zlP>6g(%{0_Dz7~0hQL!v(5_DuLFw`IlG;hF8UWXE5BzYu{JLa)W6ALhPp#BuC-YS* z=dwijY`a6>y;N{Po1e~6?!%7H(OZGthn@ldc-n0u#><(ss0ysNEIIBVtdKacI;fA4+Ao~^Q9jJF|l84|lPVb2;Xw4Nw}&uAARtvTV$JEgEYkpnd+ zZ1DE5L%sm>HKXWw&O&bfyO=bq>%?#1o>7UJZVkVv8BH-I8C&TuGT`x-l3cq_yv7?B zd56@!DyGA(Un*@8<;l??{O%Joq5qw@%$EaydtENjeTiaNv)hI0DFr3y-;GBRbS1w% z8Vz>_2Xp}dRKRR`Pvv2=Jifa7Tg*j-J<9Wixb}-4{GwnAuHTb^PZ4GSh{e3{DG+P6l!qW~3sQpL*>N{8UnC5p1bK&v#@I@PbvAFtX_6O#u=o zop7IBnTsZc`||i$R}C(yBmTwVR}?$zaPxVoq}WLPdxr#}0IrX_Q_-0e>Va>@Gu>*S zkoNv_{B~IWt_^D41!W#?rPK^j3bC|VN+&{AE)eqhG|_~%N}+`xhy2IWe_V9L*wi^jhJQ<}Sh%=~0T?Hte=M(w z#kVyTdE-PAY@MLrh3`;zAZ1Jx8}a7qY$fAL6hRZ8`kH~N!3-FS!Kq3|!e&RX z39;!O9T=Mc0g4)+^J5b#tOK1Un^2((^VzZnR3Lt~82+1%$uCcQ0@ucEK&Q%gDG`_EO9($fO{0`R_IWj!lwJ!OM$&b}bbfZ#bV+}D<) z3ivXFGopDw$!umnuz z=@00)YC51+r&hxU6?`={aF(0hZ6A~;$IL3nV`tT^@|#)5aNx`=39KuY=gnq<+4y-e zHah}JqhT~AMFkTQYYHTjK776&oF{FN^MEvxHVLs?71ZXWLw-8TWS`JY=<%Z_=YcX4 zDpWM#+K&n!&;+i&)mWYUmaiUs8uL{uIuoHv^z!+3l`$>SL^=bm|HSLm(8BxYi=}zD zf<#brp_ueli}oz_8azTr+AP3JX_s^5{M~fuZN(&CpT93Pp;2+($@`*kXZ~&2cgW4F zd%Hulq3#PtltBv*pS2g3Pyn?-{{?Bpt%@N8?a4>Bn`$1EEUVGNj9MZ_{$(ml4$}uCFG9@Wo?lk0VAH-IV_Wt(ByEqx|Q3Xr}kZ zOekQ;%8=-unfz6p;Y}Upit$+)Uo7`4~e#x zDaEqj*9U;C12+7pTMgzU$V9{Og5|o?&_2_PO&aKLB-Amn3T&Xkg?mUw4VWR#N%#|eHBpQ80rRrek3q^jxhMrPxX>8iXsv^ zo-|LX?5BJxOJR`Uzh*IW7J7@C&lzy)acM?XBCdxgPHP6uC`|at{ELB^n%euQzDYOZx*bbSwZ{t{*Rt!|N7gbJw4%&=I0MgAKJ-21$R%_ zED{B5t9Gl}`f?ckI3RtM;OfJpAMX;X*dqNNQDPL8xVH|!rRY0N*1eE-|rNgG4Sj7BHZ zPZ~t17Tx-`5J47+j)(@sSVX}-#8eoUKkZ?+ie}GjjJvNK5eIZ^j`ll;Wp4f4 z+vhf?6&P;IA{rMp&TV4C(HLzuk338BlIA;_o2hiP#dc)QIdkWHXU?5J-F)@X=BvWq G5C0z@DrJBG delta 721 zcmX|7T}V>_6y{u~-FffyE^S(y6^+>)n9D_kls_-^q-ane^rMdwg6k?>k=$%lDQN8@CDO@T0Ve z$5;(0H+mCMYK%v+QH1kp23jMVkn!dz61~MwrU*mt6?ptA%0DS637W7w_!jm3R`2|) z?}EO7Bd<+Z8%RLkz#m*1Jc?^C6oiLmR1_OHt3wdIb;Cx2?Oi4}nn^}hFa>I#g7Q%d zo{x5}kG~v~xhhp7!$CV{CS~+Y)^KqeiSSGv!0Mu?5h-{urXcWPD;K0|#GOGaO#KS# zLz4GID34GbGUL|FS=coBo{t9DXLB(gv}(RHP;uUXH?u=pZN~kG%=wOWBX)F0OlXKS zq3A0x?N?AZC&LqMK<&H@73~QqxE+r`yNK@j-2eUboT~9Hn!1F(n0)=5%&lfh;vox3 z-qW#+I9&d@&71w}GhuQ~MsztDOEC*dR;=7pM+Ppa)uMGRmk~QnB;m`7ovQ?r*hOUG zHXm)Ci#kYZ8ltP*CXl7+F75CY3gi(m@>}A8E%c4c)3v1$Bf0s9Olhok2r?JT6ygaJ zh54XDc3qcUX0o&AkYMo5B-2HKPo`21S6ax5{fi<8EL5f^oujABVFx9lE++B5bnR@j zjbz`m?W7QE(h^^=Q7Lz3P(F`4$g*MDy_5VKRgQ!FO$*vsjjFxi%A`s@yjSy3)GV^1 zQo7z9V^yU+dZIdXM(@(N+DY*!>=Lo7D?x`{|BwVM%!{1oB&)ACi)zVtJewBcu0-AB Ki@Jq}lYarElOeMJ diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java index fd5fb5b399b..5f2fa550db5 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java @@ -2038,6 +2038,63 @@ public Boolean isDarkMode() { } @Override + public com.codename1.impl.NativeThemeSettings getNativeThemeSettings() { + com.codename1.impl.NativeThemeSettings result = new com.codename1.impl.NativeThemeSettings(); + android.content.Context context = getContext(); + if (context == null) { return result; } + // Resolve normal-size platform text appearance in an unscaled configuration. + // Dividing scaledDensity is not sufficient with Android's nonlinear font scaling. + if (android.os.Build.VERSION.SDK_INT >= 17) { + Configuration config = new Configuration(context.getResources().getConfiguration()); + config.fontScale = 1f; + android.content.Context normal = context.createConfigurationContext(config); + android.content.res.TypedArray text = normal.obtainStyledAttributes( + android.R.style.TextAppearance_DeviceDefault, + new int[] {android.R.attr.textSize}); + try { + float size = text.getDimension(0, 0); + result.font("native:", size); + } finally { + text.recycle(); + } + } + // Public dynamic-color resources are available on Android 12+. Resolve by + // name so old SDK builds and vendors with partial support retain fallbacks. + if (android.os.Build.VERSION.SDK_INT >= 31) { + String[][] colors = { + {"accent-color", "system_accent1_600"}, + {"accent-color-dark", "system_accent1_200"}, + {"accent-on-color", "system_accent1_0"}, + {"accent-on-color-dark", "system_accent1_800"}, + {"accent-container-color", "system_accent1_100"}, + {"accent-container-color-dark", "system_accent1_700"}, + {"accent-on-container-color", "system_accent1_900"}, + {"accent-on-container-color-dark", "system_accent1_100"}, + {"accent-pressed-color", "system_accent1_200"}, + {"accent-pressed-color-dark", "system_accent1_700"}, + {"selection-color", "system_accent1_200"}, + {"selection-color-dark", "system_accent1_700"}, + {"window-bg-color", "system_neutral1_10"}, + {"window-bg-color-dark", "system_neutral1_900"}, + {"view-bg-color", "system_neutral1_10"}, + {"view-bg-color-dark", "system_neutral1_900"}, + {"text-color", "system_neutral1_900"}, + {"text-color-dark", "system_neutral1_100"}, + {"text-secondary-color", "system_neutral2_700"}, + {"text-secondary-color-dark", "system_neutral2_200"}, + {"separator-color", "system_neutral2_500"}, + {"separator-color-dark", "system_neutral2_600"} + }; + android.content.res.Resources resources = context.getResources(); + for (String[] color : colors) { + int id = resources.getIdentifier(color[1], "color", "android"); + if (id != 0) { result.color(color[0], resources.getColor(id)); } + } + } + return result; + } + + @Override public boolean isLargerTextEnabled() { return getLargerTextScale() > 1.0f; } diff --git a/Ports/Android/src/com/codename1/impl/android/CodenameOneActivity.java b/Ports/Android/src/com/codename1/impl/android/CodenameOneActivity.java index 9bda03998df..fb9530b303d 100644 --- a/Ports/Android/src/com/codename1/impl/android/CodenameOneActivity.java +++ b/Ports/Android/src/com/codename1/impl/android/CodenameOneActivity.java @@ -127,11 +127,18 @@ private IBillingSupport getBillingSupport() { } + @Override + public void onConfigurationChanged(android.content.res.Configuration config) { + super.onConfigurationChanged(config); + com.codename1.ui.Display.getInstance().nativeThemeSettingsChanged(); + } + @Override protected void onResume() { super.onResume(); AndroidImplementation.setActivity(this); AndroidNativeUtil.onResume(); + com.codename1.ui.Display.getInstance().nativeThemeSettingsChanged(); if (isBillingEnabled() && getBillingSupport() != null) { billingSupport.consumeAndAcknowlegePurchases(); } diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java index 110aa3657d9..560bbffd318 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java @@ -8551,6 +8551,7 @@ public void actionPerformed(ActionEvent e) { }); final JMenu largerTextMenu = installLargerTextMenu(simulateMenu, pref, frm); + simulateMenu.add(installNativeThemeSettingsMenu()); final JMenu accessibilityPreferencesMenu = installAccessibilityPreferencesMenu(pref); final JMenu notificationBackgroundMenu = installNotificationBackgroundSimulationMenu(simulateMenu); @@ -9648,6 +9649,42 @@ private float zoomLevel() { return Math.min(h1, w1); } + private JMenu installNativeThemeSettingsMenu() { + JMenu menu = new JMenu("OS Theme Settings"); + JMenuItem reset = new JMenuItem("Reset simulated settings"); + reset.addActionListener(e -> setSimulatorNativeThemeSettings(null)); + menu.add(reset); + JMenuItem accent = new JMenuItem("Accent color..."); + accent.addActionListener(e -> { + java.awt.Color color = javax.swing.JColorChooser.showDialog(canvas, "Simulated OS accent", java.awt.Color.BLUE); + if (color != null) { + com.codename1.impl.NativeThemeSettings settings = simulatorNativeThemeSettings == null + ? new com.codename1.impl.NativeThemeSettings() : simulatorNativeThemeSettings.copy(); + settings.color("accent-color", color.getRGB()).color("accent-color-dark", color.getRGB()); + setSimulatorNativeThemeSettings(settings); + } + }); + menu.add(accent); + JMenuItem font = new JMenuItem("UI font..."); + font.addActionListener(e -> { + String family = javax.swing.JOptionPane.showInputDialog(canvas, "OS UI font family", "Dialog"); + if (family == null || family.trim().length() == 0) { return; } + String size = javax.swing.JOptionPane.showInputDialog(canvas, "Normal size in CN1 pixels", "16"); + if (size == null) { return; } + try { + float pixels = Float.parseFloat(size); + if (!(pixels > 0 && pixels < 10000)) { throw new NumberFormatException(); } + com.codename1.impl.NativeThemeSettings settings = simulatorNativeThemeSettings == null + ? new com.codename1.impl.NativeThemeSettings() : simulatorNativeThemeSettings.copy(); + setSimulatorNativeThemeSettings(settings.font(family.trim(), pixels)); + } catch (NumberFormatException invalid) { + javax.swing.JOptionPane.showMessageDialog(canvas, "Enter a positive font size below 10000."); + } + }); + menu.add(font); + return menu; + } + private JMenu installLargerTextMenu(JMenu parent, final Preferences pref, final JFrame frm) { // Standard iOS Dynamic Type stops with their actual body-text point sizes. // The simulator returns ratio = bodyPt / 17pt, matching what iOS reports. @@ -10752,6 +10789,62 @@ private static String toHex(byte[] in) { return sb.toString().toUpperCase(); } + @Override + public com.codename1.impl.NativeThemeSettings getNativeThemeSettings() { + if (simulatorNativeThemeSettings != null) { return simulatorNativeThemeSettings.copy(); } + com.codename1.impl.NativeThemeSettings result = new com.codename1.impl.NativeThemeSettings(); + if (isSimulator()) { return result; } + final java.awt.Toolkit toolkit = java.awt.Toolkit.getDefaultToolkit(); + if (!nativeThemeDesktopListenersInstalled) { + nativeThemeDesktopListenersInstalled = true; + final String[] properties = {"win.messagebox.font", "win.itemHighlightColor", + "win.text.textColor", "win.frame.backgroundColor", "gnome.Gtk/FontName"}; + final java.beans.PropertyChangeListener listener = new java.beans.PropertyChangeListener() { + public void propertyChange(java.beans.PropertyChangeEvent event) { + Display.getInstance().nativeThemeSettingsChanged(); + } + }; + for (String property : properties) { toolkit.addPropertyChangeListener(property, listener); } + addDeinitializeHook(new Runnable() { + public void run() { + for (String property : properties) { toolkit.removePropertyChangeListener(property, listener); } + nativeThemeDesktopListenersInstalled = false; + } + }); + } + Object font = toolkit.getDesktopProperty("win.messagebox.font"); + if (font instanceof java.awt.Font) { + java.awt.Font uiFont = (java.awt.Font) font; + result.font(uiFont.getFamily(), uiFont.getSize2D()); + } + String[][] properties = {{"selection-color", "win.itemHighlightColor"}, + {"text-color", "win.text.textColor"}, {"window-bg-color", "win.frame.backgroundColor"}}; + for (String[] pair : properties) { + Object color = toolkit.getDesktopProperty(pair[1]); + if (color instanceof java.awt.Color) { + result.color(pair[0], ((java.awt.Color) color).getRGB()); + } + } + return result; + } + + private boolean nativeThemeDesktopListenersInstalled; + private com.codename1.impl.NativeThemeSettings simulatorNativeThemeSettings; + + /// Deterministic OS settings for simulator previews/tests; null restores host defaults. + public void setSimulatorNativeThemeSettings(com.codename1.impl.NativeThemeSettings settings) { + simulatorNativeThemeSettings = settings == null ? null : settings.copy(); + Display.getInstance().nativeThemeSettingsChanged(); + } + + @Override + public Object loadNativeThemeFont(String family, String template, float size, int style) { + if ("native:".equals(family)) { + return super.loadNativeThemeFont(family, template, size, style); + } + return new java.awt.Font(family, style, 1).deriveFont(size); + } + @Override public boolean isLargerTextEnabled() { return largerTextEnabled; @@ -11494,6 +11587,7 @@ public void windowDeiconified(WindowEvent e) { } public void windowActivated(WindowEvent e) { + Display.getInstance().nativeThemeSettingsChanged(); mainSurfaceFocusChanged(true); } diff --git a/Ports/LinuxPort/nativeSources/cn1_linux.h b/Ports/LinuxPort/nativeSources/cn1_linux.h index dad60492a33..b52049ecdec 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux.h +++ b/Ports/LinuxPort/nativeSources/cn1_linux.h @@ -100,7 +100,8 @@ typedef enum { * Java side handed out in setNativeCommands. Queued like every other input so the * command runs on the EDT rather than on the GTK thread. Same number as the Windows * port's, so the two desktop wire protocols do not drift apart. */ - CN1_EVENT_MENU_COMMAND = 23 + CN1_EVENT_MENU_COMMAND = 23, + CN1_EVENT_THEME_SETTINGS_CHANGED = 24 } CN1EventType; /* Fixed-point scale for the gesture keyCode field (see CN1_EVENT_PINCH). */ diff --git a/Ports/LinuxPort/nativeSources/cn1_linux_window.c b/Ports/LinuxPort/nativeSources/cn1_linux_window.c index 4d5120409e5..d43f4eaf259 100644 --- a/Ports/LinuxPort/nativeSources/cn1_linux_window.c +++ b/Ports/LinuxPort/nativeSources/cn1_linux_window.c @@ -1867,3 +1867,67 @@ JAVA_INT com_codename1_impl_linux_LinuxNative_systemColorScheme___R_int(CODENAME g_settings_schema_unref(schema); return result; } + +/* Settings and GTK style contexts must only be read on the GTK main thread. */ +static void cn1ThemeSettingsNotify(GObject* object, GParamSpec* property, gpointer data) { + (void)object; (void)property; (void)data; + cn1LinuxPushEvent(CN1_EVENT_THEME_SETTINGS_CHANGED, 0, 0, 0); +} + +static void cn1ReadThemeSettings(void* out) { + GString* result = (GString*)out; + GtkSettings* settings = gtk_settings_get_default(); + if (!settings || !cn1Window) return; + static GtkSettings* observed; + if (observed != settings) { + if (observed) { + g_signal_handlers_disconnect_by_func(observed, G_CALLBACK(cn1ThemeSettingsNotify), NULL); + g_object_unref(observed); + } + observed = (GtkSettings*)g_object_ref(settings); + g_signal_connect(settings, "notify::gtk-font-name", G_CALLBACK(cn1ThemeSettingsNotify), NULL); + g_signal_connect(settings, "notify::gtk-xft-dpi", G_CALLBACK(cn1ThemeSettingsNotify), NULL); + g_signal_connect(settings, "notify::gtk-theme-name", G_CALLBACK(cn1ThemeSettingsNotify), NULL); + g_signal_connect(settings, "notify::gtk-application-prefer-dark-theme", G_CALLBACK(cn1ThemeSettingsNotify), NULL); + } + gchar* fontName = NULL; + g_object_get(settings, "gtk-font-name", &fontName, NULL); + if (fontName) { + PangoFontDescription* font = pango_font_description_from_string(fontName); + const char* family = pango_font_description_get_family(font); + double size = (double)pango_font_description_get_size(font) / PANGO_SCALE; + if (!pango_font_description_get_size_is_absolute(font)) { + double dpi = gdk_screen_get_resolution(gtk_widget_get_screen(cn1Window)); + size *= (dpi > 0 ? dpi : 96) / 72.0; + } + if (family && size > 0) g_string_append_printf(result, "fontFamily=%s\nfontSize=%g\n", family, size); + pango_font_description_free(font); + g_free(fontName); + } + GtkStyleContext* context = gtk_widget_get_style_context(cn1Window); + const char* tokens[] = {"accent-color", "accent-fg-color", "selection-color", "window-bg-color", + "text-color", "view-bg-color", "text-secondary-color", "separator-color"}; + const char* names[] = {"theme_selected_bg_color", "theme_selected_fg_color", "theme_selected_bg_color", + "theme_bg_color", "theme_fg_color", "theme_base_color", "insensitive_fg_color", "borders"}; + /* GTK exposes the active palette only. Supply it to the corresponding appearance, + never overwrite the other appearance with an invented light/dark variant. */ + GdkRGBA bg; + gboolean dark = gtk_style_context_lookup_color(context, "theme_bg_color", &bg) + && (bg.red + bg.green + bg.blue) < 1.5; + for (unsigned i = 0; i < sizeof(tokens) / sizeof(tokens[0]); ++i) { + GdkRGBA color; + if (gtk_style_context_lookup_color(context, names[i], &color) && color.alpha >= 0.999) { + g_string_append_printf(result, "%s%s=%02x%02x%02x\n", tokens[i], dark ? "-dark" : "", + (unsigned)(color.red * 255 + 0.5), (unsigned)(color.green * 255 + 0.5), + (unsigned)(color.blue * 255 + 0.5)); + } + } +} + +JAVA_OBJECT com_codename1_impl_linux_LinuxNative_nativeThemeSettings___R_java_lang_String(CODENAME_ONE_THREAD_STATE) { + GString* buffer = g_string_new(NULL); + cn1LinuxRunOnMainAndWait(cn1ReadThemeSettings, buffer); + JAVA_OBJECT result = newStringFromCString(threadStateData, buffer->str); + g_string_free(buffer, TRUE); + return result; +} diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java index 701b5a935b4..6847c31ffe0 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java @@ -128,6 +128,7 @@ public boolean isScreenReaderEnabled() { /// which shares its number with the Windows port's so the two desktop wire protocols /// do not drift apart. private static final int EVENT_MENU_COMMAND = 23; + private static final int EVENT_THEME_SETTINGS_CHANGED = 24; private static final int EVENT_ROTATE = 11; private static final int EVENT_ACCESSIBILITY_ACTION = 12; // Additional desktop windows. These always carry a non-zero window id. @@ -833,7 +834,11 @@ private void drainInput() { case EVENT_WINDOW_CLOSE: Desktop.getInstance().windowCloseRequested(windowId); break; + case EVENT_THEME_SETTINGS_CHANGED: + Display.getInstance().nativeThemeSettingsChanged(); + break; case EVENT_WINDOW_FOCUS: + if (key != 0) { Display.getInstance().nativeThemeSettingsChanged(); } Desktop.getInstance().windowFocusChanged(windowId, key != 0); break; case EVENT_WINDOW_MONITOR: @@ -3460,6 +3465,11 @@ public String[] getPlatformOverrides() { /// platform does not know" (null) from "light" (FALSE) -- UIManager's dark-mode /// resolution tests for null explicitly -- and on Linux that distinction is real: /// a session with no desktop settings daemon has no answer to give. + @Override + public com.codename1.impl.NativeThemeSettings getNativeThemeSettings() { + return com.codename1.impl.NativeThemeSettings.parse(LinuxNative.nativeThemeSettings()); + } + @Override public Boolean isDarkMode() { int v = LinuxNative.systemColorScheme(); diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java index e956290d9b4..51098d6289d 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxNative.java @@ -62,6 +62,8 @@ private LinuxNative() { /// event carries that. public static native int currentModifiers(); + public static native String nativeThemeSettings(); + public static native void nativeLog(String message); /** * The desktop's colour scheme: 1 dark, 0 light, -1 unknown. diff --git a/Ports/WindowsPort/nativeSources/cn1_windows.h b/Ports/WindowsPort/nativeSources/cn1_windows.h index 8502ba81bf5..d372576130a 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows.h +++ b/Ports/WindowsPort/nativeSources/cn1_windows.h @@ -126,7 +126,8 @@ typedef enum { * Java side handed out in setNativeCommands; x, y and windowId are unused. Routed * through the same queue as every other event so the command runs on the EDT rather * than on the pump thread, which is the rule the whole bridge is built on. */ - CN1_EVENT_MENU_COMMAND = 23 + CN1_EVENT_MENU_COMMAND = 23, + CN1_EVENT_THEME_SETTINGS_CHANGED = 24 } CN1EventType; /* Fixed-point scale for the gesture keyCode field (see CN1_EVENT_PINCH). */ diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp index a6dd05b8555..683674d1339 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp @@ -679,6 +679,12 @@ int cn1WinHandleGesture(HWND hwnd, int windowId, LPARAM lParam) { LRESULT CALLBACK cn1WinWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { + case WM_SETTINGCHANGE: + case WM_THEMECHANGED: + case WM_SYSCOLORCHANGE: + case WM_DWMCOLORIZATIONCOLORCHANGED: + cn1WinPushEvent(CN1_EVENT_THEME_SETTINGS_CHANGED, 0, 0, 0); + break; case WM_GETOBJECT: return cn1WinAccessibilityObject(hwnd, wParam, lParam); case WM_LBUTTONDOWN: diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_winrt.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_winrt.cpp index 1c157d45cab..fffff8ea0dd 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_winrt.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_winrt.cpp @@ -41,6 +41,8 @@ #include "cn1_windows.h" #include +#include +#include #include #include #include @@ -458,6 +460,75 @@ JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_shareText___java_lang_Stri return JAVA_TRUE; } + +/* UISettings provides the user's actual accent palette. Keep initialization balanced: + this query runs repeatedly on the long-lived EDT, unlike short-lived worker bridges. */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_nativeThemeSettings___R_java_lang_String( + CODENAME_ONE_THREAD_STATE) { + using namespace ABI::Windows::UI::ViewManagement; + CN1Buf buffer; + cn1BufInit(&buffer); + NONCLIENTMETRICSW metrics = {}; + metrics.cbSize = sizeof(metrics); + if (SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(metrics), &metrics, 0)) { + char family[256]; + int count = WideCharToMultiByte(CP_UTF8, 0, metrics.lfMessageFont.lfFaceName, -1, + family, sizeof(family), NULL, NULL); + HDC dc = GetDC(NULL); + int dpi = dc ? GetDeviceCaps(dc, LOGPIXELSY) : 96; + if (dc) ReleaseDC(NULL, dc); + float size = (float)abs(metrics.lfMessageFont.lfHeight) * 96.0f / (dpi > 0 ? dpi : 96) + * (cn1Win.dpiScale > 0 ? cn1Win.dpiScale : 1); + if (count > 0 && size > 0) { + char line[320]; + snprintf(line, sizeof(line), "fontFamily=%s\nfontSize=%g\n", family, size); + cn1BufAppendBytes(&buffer, line, strlen(line)); + } + } + HRESULT initialized = RoInitialize(RO_INIT_MULTITHREADED); + { + ComPtr instance; + ComPtr settings; + if (SUCCEEDED(RoActivateInstance(HStringReference(RuntimeClass_Windows_UI_ViewManagement_UISettings).Get(), + &instance)) && SUCCEEDED(instance.As(&settings))) { + const char* tokens[] = {"accent-color", "accent-color-dark", "selection-color", "selection-color-dark"}; + UIColorType types[] = {UIColorType_Accent, UIColorType_AccentLight2, UIColorType_Accent, UIColorType_AccentLight2}; + for (int i = 0; i < 4; ++i) { + ABI::Windows::UI::Color color; + if (SUCCEEDED(settings->GetColorValue(types[i], &color))) { + char line[96]; + snprintf(line, sizeof(line), "%s=%02x%02x%02x\n", tokens[i], color.R, color.G, color.B); + cn1BufAppendBytes(&buffer, line, strlen(line)); + } + } + } + } + if (SUCCEEDED(initialized)) RoUninitialize(); + /* GetSysColor describes the classic/high-contrast palette, not Fluent dark surfaces. + Use these semantic colors only for high contrast; normal Fluent keeps its surfaces. */ + HIGHCONTRASTW contrast = {}; + contrast.cbSize = sizeof(contrast); + if (SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(contrast), &contrast, 0) + && (contrast.dwFlags & HCF_HIGHCONTRASTON)) { + const char* tokens[] = {"window-bg-color", "view-bg-color", "control-bg-color", "text-color", + "text-secondary-color", "selection-color", "accent-color", "accent-fg-color"}; + int indices[] = {COLOR_WINDOW, COLOR_WINDOW, COLOR_BTNFACE, COLOR_WINDOWTEXT, + COLOR_GRAYTEXT, COLOR_HIGHLIGHT, COLOR_HIGHLIGHT, COLOR_HIGHLIGHTTEXT}; + for (int i = 0; i < 8; ++i) { + COLORREF color = GetSysColor(indices[i]); + char line[128]; + for (int dark = 0; dark < 2; ++dark) { + snprintf(line, sizeof(line), "%s%s=%02x%02x%02x\n", tokens[i], dark ? "-dark" : "", + GetRValue(color), GetGValue(color), GetBValue(color)); + cn1BufAppendBytes(&buffer, line, strlen(line)); + } + } + } + JAVA_OBJECT result = newStringFromCString(threadStateData, buffer.data ? buffer.data : ""); + free(buffer.data); + return result; +} + } /* extern "C" */ #endif /* _WIN32 */ diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java index ad21a288c61..bbfde989e0e 100644 --- a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java +++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java @@ -115,6 +115,7 @@ public boolean isScreenReaderEnabled() { /// A native menu bar item was chosen; the key field carries the command id /// setNativeCommands handed out. Must match CN1_EVENT_MENU_COMMAND in cn1_windows.h. private static final int EVENT_MENU_COMMAND = 23; + private static final int EVENT_THEME_SETTINGS_CHANGED = 24; private static final int EVENT_PINCH_BEGIN = 20; private static final int EVENT_PINCH_END = 21; private static final int EVENT_ROTATE = 11; @@ -855,7 +856,11 @@ private void drainInput() { case EVENT_WINDOW_CLOSE: Desktop.getInstance().windowCloseRequested(windowId); break; + case EVENT_THEME_SETTINGS_CHANGED: + Display.getInstance().nativeThemeSettingsChanged(); + break; case EVENT_WINDOW_FOCUS: + if (key != 0) { Display.getInstance().nativeThemeSettingsChanged(); } Desktop.getInstance().windowFocusChanged(windowId, key != 0); break; case EVENT_WINDOW_MONITOR: @@ -3469,6 +3474,11 @@ public String[] getPlatformOverrides() { /// Returns Boolean rather than boolean because the contract distinguishes "the /// platform does not know" (null) from "light" (FALSE), and callers such as /// UIManager's dark-mode resolution treat the two differently. + @Override + public com.codename1.impl.NativeThemeSettings getNativeThemeSettings() { + return com.codename1.impl.NativeThemeSettings.parse(WindowsNative.nativeThemeSettings()); + } + @Override public Boolean isDarkMode() { return WindowsNative.systemUsesDarkTheme() ? Boolean.TRUE : Boolean.FALSE; diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java index 59239492f41..d810068c0d6 100644 --- a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java +++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java @@ -40,6 +40,8 @@ private WindowsNative() { /* ---------------------------------------------------------- lifecycle */ /** Writes a line to the native debug log (OutputDebugString + stderr). */ + public static native String nativeThemeSettings(); + public static native void nativeLog(String message); /** diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m index 6dbfd98149d..9ad3e02314f 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m @@ -3258,6 +3258,14 @@ @interface CodenameOne_GLViewController () #if !TARGET_OS_OSX @implementation CodenameOne_GLViewController +#if !TARGET_OS_OSX && !TARGET_OS_WATCH +- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { + [super traitCollectionDidChange:previousTraitCollection]; + com_codename1_impl_ios_IOSImplementation_nativeThemeSettingsChanged__(CN1_THREAD_GET_STATE_PASS_SINGLE_ARG); +} +#endif + + @synthesize displayLink, currentMutableImage, animating; static CodenameOne_GLViewController *sharedSingleton; +(BOOL)isDrawTextureSupported { diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index 4b257c37897..8e955f9882b 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -8541,6 +8541,89 @@ void com_codename1_impl_ios_IOSNative_bonjourPublishStop___long(CN1_THREAD_STATE #endif } +/* OS theme snapshots are captured on the Apple UI thread; Java receives values only. */ +static void cn1NativeThemeDidChange(NSNotification* notification) { + (void)notification; + com_codename1_impl_ios_IOSImplementation_nativeThemeSettingsChanged__(CN1_THREAD_GET_STATE_PASS_SINGLE_ARG); +} + +JAVA_OBJECT com_codename1_impl_ios_IOSNative_nativeThemeSettings___R_java_lang_String( + CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) { + POOL_BEGIN(); + __block NSString* snapshot = nil; + void (^readSettings)(void) = ^{ + NSMutableString* values = [NSMutableString string]; +#if TARGET_OS_OSX + static dispatch_once_t observing; + dispatch_once(&observing, ^{ + [[NSNotificationCenter defaultCenter] addObserverForName:NSSystemColorsDidChangeNotification + object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification* n) { cn1NativeThemeDidChange(n); }]; + }); + [values appendFormat:@"fontFamily=native:\nfontSize=%g\n", [NSFont systemFontSize] * (scaleValue > 0 ? scaleValue : 1)]; + if (@available(macOS 10.14, *)) { + for (int dark = 0; dark < 2; ++dark) { + NSAppearance* appearance = [NSAppearance appearanceNamed:dark ? NSAppearanceNameDarkAqua : NSAppearanceNameAqua]; + [appearance performAsCurrentDrawingAppearance:^{ + NSArray* tokens = @[@"accent-color", @"selection-color", @"window-bg-color", @"control-bg-color", + @"view-bg-color", @"text-color", @"text-secondary-color", @"separator-color"]; + NSArray* colors = @[[NSColor controlAccentColor], [NSColor selectedTextBackgroundColor], + [NSColor windowBackgroundColor], [NSColor controlBackgroundColor], [NSColor textBackgroundColor], + [NSColor labelColor], [NSColor secondaryLabelColor], [NSColor separatorColor]]; + for (NSUInteger i = 0; i < tokens.count; ++i) { + NSColor* color = [colors[i] colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; + if (!color) continue; + // Flatten semantic translucent colors onto the matching window surface. + NSColor* background = [[NSColor windowBackgroundColor] colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; + CGFloat a = color.alphaComponent; + unsigned r = (unsigned)lround(255 * (color.redComponent*a + background.redComponent*(1-a))); + unsigned g = (unsigned)lround(255 * (color.greenComponent*a + background.greenComponent*(1-a))); + unsigned b = (unsigned)lround(255 * (color.blueComponent*a + background.blueComponent*(1-a))); + [values appendFormat:@"%@%@=%02x%02x%02x\n", tokens[i], dark ? @"-dark" : @"", r, g, b]; + } + }]; + } + } +#elif !TARGET_OS_WATCH && !TARGET_OS_TV + static dispatch_once_t observing; + dispatch_once(&observing, ^{ + [[NSNotificationCenter defaultCenter] addObserverForName:UIContentSizeCategoryDidChangeNotification + object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification* n) { cn1NativeThemeDidChange(n); }]; + }); + // systemFontSize is the unscaled baseline also used by getLargerTextScale. + [values appendFormat:@"fontFamily=native:\nfontSize=%g\n", [UIFont systemFontSize] * (scaleValue > 0 ? scaleValue : 1)]; + if (@available(iOS 13.0, *)) { + for (int dark = 0; dark < 2; ++dark) { + UITraitCollection* traits = [UITraitCollection traitCollectionWithUserInterfaceStyle: + dark ? UIUserInterfaceStyleDark : UIUserInterfaceStyleLight]; + NSArray* tokens = @[@"accent-color", @"window-bg-color", @"view-bg-color", @"control-bg-color", + @"text-color", @"text-secondary-color", @"separator-color"]; + NSArray* colors = @[[UIColor systemBlueColor], [UIColor systemBackgroundColor], + [UIColor secondarySystemBackgroundColor], [UIColor secondarySystemBackgroundColor], + [UIColor labelColor], [UIColor secondaryLabelColor], [UIColor separatorColor]]; + UIColor* background = [[UIColor systemBackgroundColor] resolvedColorWithTraitCollection:traits]; + CGFloat br=0, bg=0, bb=0, ba=0; + [background getRed:&br green:&bg blue:&bb alpha:&ba]; + for (NSUInteger i = 0; i < tokens.count; ++i) { + UIColor* color = [colors[i] resolvedColorWithTraitCollection:traits]; + CGFloat r=0, g=0, b=0, a=0; + if (![color getRed:&r green:&g blue:&b alpha:&a]) continue; + [values appendFormat:@"%@%@=%02x%02x%02x\n", tokens[i], dark ? @"-dark" : @"", + (unsigned)lround(255*(r*a+br*(1-a))), (unsigned)lround(255*(g*a+bg*(1-a))), + (unsigned)lround(255*(b*a+bb*(1-a)))]; + } + } + } +#endif + snapshot = [values copy]; + }; + if ([NSThread isMainThread]) readSettings(); + else dispatch_sync(dispatch_get_main_queue(), readSettings); + JAVA_OBJECT result = fromNSString(CN1_THREAD_STATE_PASS_ARG snapshot); + [snapshot release]; + POOL_END(); + return result; +} + JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isLargerTextEnabled___R_boolean(CN1_THREAD_STATE_MULTI_ARG JAVA_OBJECT instanceObject) { #if !TARGET_OS_WATCH && !TARGET_OS_TV if (@available(iOS 7.0, *)) { diff --git a/Ports/iOSPort/nativeSources/iOSModern27Theme.res b/Ports/iOSPort/nativeSources/iOSModern27Theme.res index 3b00f7e7cc7a0c2a437b96507ed0f9bb45db3bd7..be993f1d39d374e03447af9babcb65b846cf0a56 100644 GIT binary patch delta 3698 zcma)9UrbwN6z{pTrK1-L{db|X{Fe+C5M;#3{DTx&%pigzi!SO?;0_u}TiOnUES6}Z z(Zsm;v3+}3GSR4sG0tVlYNBQ=J}pLFVtko#d-TBeHSg?wFTAn&xh=(DTfuqsCAT|0zm^&0rKzpDa-8p4TqhIyQb#ATC8usN?C^f?2Xe5@HQ?p`Hk~=9{gRFtuJvunn*30BnQ`|(M-j>VX<0ipsJ_G zAEl9zL$dX%KWiR*X{(GyrsdfF-4!f}L{q_?Rn&|v8k~M0z=7i$R=fV58s>LWrV*>K z_6<=HKqO;pB3%IR(57;zRz>s^0S@KZ;h$w>D_>16Zg|Kq8xB{UIw@&+CJ|>Ov&xtu z$j)5Sj@{Dp0d~H8!ME2ry;;sWof)9NW(Cn;{c0gd7i{c;+ZV0$XD>L=Efslg&<3|{ z^nN8c$)?9cKJ+-O70MWb6GHAOHq_la7gNL;ddJD!1-u>&)yq)yy<?F^n?Qe@ zV7=?&to)n^emXMA^0Gb&<@kgd^V5H(poYe#7~hoYOr8V@3%?ZM_iTWdK0F`E6tOcm zp!Kh%!32yO-k^Z$zH7ax)3X+Xj+qn8+Xe-_9TjfeaWc-fLS#-(?@q8msa@~kdrokW zM6fPsKpf=3(&wWfY2?mfO6~l_6ROK{nVu&!sGO)VzpKU9!H9V{xTs5XAR-P9WZ9%g zSHUS^lgPiy{m1YjqGPi~U%vUY9pws>iu?iYV=Mj-!mY^>8eI*<(%li49i*^N9)60e? zOa<;cFz6O4xM*1LtYp;(D@&>IeFp_~nsF)M7Lj;t#jAjWvMCE{B(o*Ju_=qeGp2qF zM6)~=O=n(`$74O!wiwaDZQodat^;i{sa_>{AXflv}dVd@0Y^t#vHSiYD zOEnOr>r;Y_OstEVp`U7?O%t74*O^>!>mu6pvS1hSpwjaSeBzW)(V#!a;n2`c>DKFl z4^#0o9th`d(-S)y$5J4n!A{mMA-%+RmI2h+nf55OHuAtplgxX=8LQ|J}wGyVK*FFyX zuOZ`h)>Hl%O3Kn_g16D^-q_pe&4AiIxx5eFgenvg^FdEf0j~WQOA^=`YX{?zOmtbE znwJ;lVfjqtY%G%=N+e O1=vr>^|X{Zoc{p|mfkb~ delta 242 zcmeBb=G}3NXM!k`$dQfWslTUNGcyWIzVM4>vhIJL=@S|ld8VJ^XB3`1_uua6dzcty zCu>=NMU zuukt3W)z(!%E-FiT!e8h7%g5cX`=ZW6EK-?bgb8#`@Ul_IC~Cn^d;YU?-%simQ5L2YTlNj9m$u~XYk zs1%gIp@<);r7y`yR7m00QUpQ>j8q}u#4SMxZb(HO5C>Y7BTZU(yKDPBd)M*Gmp9*h zGjC?zw{O4wMY{4(x+=HHK3lkVFkN^JpN~P{i@k&Ec*y}CGWuN#DBZyAJzP)3u>{rtvO3X0~OV*738|82O>`?lF!UQ{(cZA1B#2x zMWrTbB(yX%ckG@32mYo)D_CH=ZeOo2XRGT~c4itOJxFv-37`W4)VP-r>fxGtk25hJ2?)WQMOnq&dwUu0KPH-Awz^2O!p;zrPI7q z?j$n>r_COhK4tujnmGN z>dUac8`ge1So6rSC8ft2#a#>W&YaNYZh&Eb0G6D*stkW91m~MHb!0l7tIv%Ga$>-M z_4frH&IM&~;{u@=_j?}AOVQbt$RKaCVlus;s~zMo7k5{pbtA|JdHLfAH*OgjT=>WV z64Iu2;EJCPo4yv1VGdV);M}T39LlKK@rLgUcA8NMLJUvEDa*Y8M9Ql&d?d!g2XpCC zW=1`6UY$}0)rs`QY^iuSpU>LFQsJUnel-Griu2Bc`t%rb;CMp+**x5$@!jPZ(tkGD zurv;vV_$M#a{oT6Cswmqp-3H09UkhZZRU6_whVv(XzW>X)__p0QdpW5x56`)sVZ8k_pn3}9w58yvAHv3uUjeTw zynD1T3!jz^CTOFiEY%c()}m}z3@ir*MpMsWCjPwEG_R27eTRb)YL-RY%|TO`fgj%v z48I0wfo`$_?n@}pX(05k9__)>Nfw9Hslw-8h+~iD(lYEsLjE5YFvfQg)<91Q85Sj` xo62!igj19+<2=zVbt@9 callbacks; synchronized(instance.onActiveListeners) { diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java index 32491a582b7..44e1946e02f 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSNative.java @@ -245,6 +245,7 @@ native void fillGradient(int kind, int stopCount, float[] positions, float[] pre native boolean isLargerTextEnabled(); native float getLargerTextScale(); + native String nativeThemeSettings(); native boolean isHighContrastEnabled(); native boolean isDifferentiateWithoutColorEnabled(); native boolean isReduceMotionEnabled(); diff --git a/Themes/AndroidMaterialTheme.res b/Themes/AndroidMaterialTheme.res index de3c0252c13345a6f6d13b04ebf591165cafe872..b500d0eee2126e80781f5bef08e806705799d476 100644 GIT binary patch delta 12454 zcma)Ce{j^*wa@o!vRSgb*-iFGvdQKb2?+!fLyOfQU?3rq5)40LQCo&=k|jnryD?dW z4#)6&v#${->esieV12s2lwUuc|QbBxZuYAEZCbt|NAN(c7A2e zb;d%QVl`dSaCD%iJ2>!2S~YyJ-_J-2*7r2B+E=A#u#Q^G94WekSGMuxclRsh;v1qv5a`hJ%Kw!0k4BJ8GN(C*41 zrLzPJTy{-Uwq$xTR4t@KUCy+5DQjT|>8}>*dSr2QQ?1tb(43nmMbys^6hZTWneem2 zF0i3qRLKFt^P6nBjVIQO$|yNhue6p{Z^y!{l=`z>HieRC~ub7QTV239^po;<|6JGHIrq=G5r%*bi zY+n&ndcB%M)_YxKs86tFDX9ZPhXb%WFZcPVdYNU#=Qa6=a+qGC?x2PI$6CQ59Wp@= zp(_wBpO*AfIeb>`l>WH+Hs8&uW#OjtZQa34@=#E4X+0sGl35t-{&6ZGw611>O8Mcv z8Cj&iOVE3o#Xlt|VM6ff#)2F$R7Fwp!28fP=HeH6wN~~OGb6jNb#k>K-m3B~x{{GlD8Q0l1d)k?cTUip#1drwJSn zChqlqPZz4%xND2`H}&@PDxkr$fa*)G98cbvMn+YNZqh`khKW%Rw6)3CW+uKengJWy z+>%k|jZO#B<~M2=DIjCa+~wS zxw^^?XI9DaMx*0-izr72jZy!qV0UkH@rGC|8d1ejjMvP+uE|x$YQ@-Wu)pDgzuxa9 zH+%;DNBGSE-7ME>KXiz#dx@)1)Hfz9zrAbS z0|AnBCw%=A{mEO$W)gd~sA?8iSFmo(m21*QuN!*;_N>Fez{=&iF*{samkA{oGGXVw za_R4mbaNq;ydxdjE?8mySOt9ls0}uCBJ5~6YKNnpau9M~v>d&i_l7M;b4l?IF$EyS zoH^WO*SA*HN~wWkUDK5+#~Vpscm6E>9Wjj8srdcm_w%WZpPjEZ7ke@Np*x@KT%cMX zP85yoJbI6y0_#T~#$dg=KNvBne%Hr};j4%9^zQ`(`@E_s{5m|~7CRWuCec>Wz%WP> z>k|(^Yd?zhkw~IO?NaBU;?fd#IMf&l zYq}W*z|U{UAqzUhOudn@EB^MFT!0zNbDLanYLgx#w~htMsolb%rJ-PVXds1J+9r}^ zrFRQ`M{U;2U@3lJOMYC#gRQ4+r|6M`2EM{9+Oo!3jQuFFfhFOC)muGlzE!u$#ThqI zSPN{>vLQEoz7_e8=SAgEHe|=~bC=nQuFk}frK{Rn!CT@}MTfD85|Vqu zB7S~U4&@pJq@-xU)Gb9j9R)2K2nAE&k!mTnZ4}Xq`_U)7srIqROPQK`L*Z^U7mG&U zlP>6g(%{0_Dz7~0hQL!v(5_DuLFw`IlG;hF8UWXE5BzYu{JLa)W6ALhPp#BuC-YS* z=dwijY`a6>y;N{Po1e~6?!%7H(OZGthn@ldc-n0u#><(ss0ysNEIIBVtdKacI;fA4+Ao~^Q9jJF|l84|lPVb2;Xw4Nw}&uAARtvTV$JEgEYkpnd+ zZ1DE5L%sm>HKXWw&O&bfyO=bq>%?#1o>7UJZVkVv8BH-I8C&TuGT`x-l3cq_yv7?B zd56@!DyGA(Un*@8<;l??{O%Joq5qw@%$EaydtENjeTiaNv)hI0DFr3y-;GBRbS1w% z8Vz>_2Xp}dRKRR`Pvv2=Jifa7Tg*j-J<9Wixb}-4{GwnAuHTb^PZ4GSh{e3{DG+P6l!qW~3sQpL*>N{8UnC5p1bK&v#@I@PbvAFtX_6O#u=o zop7IBnTsZc`||i$R}C(yBmTwVR}?$zaPxVoq}WLPdxr#}0IrX_Q_-0e>Va>@Gu>*S zkoNv_{B~IWt_^D41!W#?rPK^j3bC|VN+&{AE)eqhG|_~%N}+`xhy2IWe_V9L*wi^jhJQ<}Sh%=~0T?Hte=M(w z#kVyTdE-PAY@MLrh3`;zAZ1Jx8}a7qY$fAL6hRZ8`kH~N!3-FS!Kq3|!e&RX z39;!O9T=Mc0g4)+^J5b#tOK1Un^2((^VzZnR3Lt~82+1%$uCcQ0@ucEK&Q%gDG`_EO9($fO{0`R_IWj!lwJ!OM$&b}bbfZ#bV+}D<) z3ivXFGopDw$!umnuz z=@00)YC51+r&hxU6?`={aF(0hZ6A~;$IL3nV`tT^@|#)5aNx`=39KuY=gnq<+4y-e zHah}JqhT~AMFkTQYYHTjK776&oF{FN^MEvxHVLs?71ZXWLw-8TWS`JY=<%Z_=YcX4 zDpWM#+K&n!&;+i&)mWYUmaiUs8uL{uIuoHv^z!+3l`$>SL^=bm|HSLm(8BxYi=}zD zf<#brp_ueli}oz_8azTr+AP3JX_s^5{M~fuZN(&CpT93Pp;2+($@`*kXZ~&2cgW4F zd%Hulq3#PtltBv*pS2g3Pyn?-{{?Bpt%@N8?a4>Bn`$1EEUVGNj9MZ_{$(ml4$}uCFG9@Wo?lk0VAH-IV_Wt(ByEqx|Q3Xr}kZ zOekQ;%8=-unfz6p;Y}Upit$+)Uo7`4~e#x zDaEqj*9U;C12+7pTMgzU$V9{Og5|o?&_2_PO&aKLB-Amn3T&Xkg?mUw4VWR#N%#|eHBpQ80rRrek3q^jxhMrPxX>8iXsv^ zo-|LX?5BJxOJR`Uzh*IW7J7@C&lzy)acM?XBCdxgPHP6uC`|at{ELB^n%euQzDYOZx*bbSwZ{t{*Rt!|N7gbJw4%&=I0MgAKJ-21$R%_ zED{B5t9Gl}`f?ckI3RtM;OfJpAMX;X*dqNNQDPL8xVH|!rRY0N*1eE-|rNgG4Sj7BHZ zPZ~t17Tx-`5J47+j)(@sSVX}-#8eoUKkZ?+ie}GjjJvNK5eIZ^j`ll;Wp4f4 z+vhf?6&P;IA{rMp&TV4C(HLzuk338BlIA;_o2hiP#dc)QIdkWHXU?5J-F)@X=BvWq G5C0z@DrJBG delta 721 zcmX|7T}V>_6y{u~-FffyE^S(y6^+>)n9D_kls_-^q-ane^rMdwg6k?>k=$%lDQN8@CDO@T0Ve z$5;(0H+mCMYK%v+QH1kp23jMVkn!dz61~MwrU*mt6?ptA%0DS637W7w_!jm3R`2|) z?}EO7Bd<+Z8%RLkz#m*1Jc?^C6oiLmR1_OHt3wdIb;Cx2?Oi4}nn^}hFa>I#g7Q%d zo{x5}kG~v~xhhp7!$CV{CS~+Y)^KqeiSSGv!0Mu?5h-{urXcWPD;K0|#GOGaO#KS# zLz4GID34GbGUL|FS=coBo{t9DXLB(gv}(RHP;uUXH?u=pZN~kG%=wOWBX)F0OlXKS zq3A0x?N?AZC&LqMK<&H@73~QqxE+r`yNK@j-2eUboT~9Hn!1F(n0)=5%&lfh;vox3 z-qW#+I9&d@&71w}GhuQ~MsztDOEC*dR;=7pM+Ppa)uMGRmk~QnB;m`7ovQ?r*hOUG zHXm)Ci#kYZ8ltP*CXl7+F75CY3gi(m@>}A8E%c4c)3v1$Bf0s9Olhok2r?JT6ygaJ zh54XDc3qcUX0o&AkYMo5B-2HKPo`21S6ax5{fi<8EL5f^oujABVFx9lE++B5bnR@j zjbz`m?W7QE(h^^=Q7Lz3P(F`4$g*MDy_5VKRgQ!FO$*vsjjFxi%A`s@yjSy3)GV^1 zQo7z9V^yU+dZIdXM(@(N+DY*!>=Lo7D?x`{|BwVM%!{1oB&)ACi)zVtJewBcu0-AB Ki@Jq}lYarElOeMJ diff --git a/Themes/GnomeAdwaitaTheme.res b/Themes/GnomeAdwaitaTheme.res index 7ef22acc1d65ace41dcbe8a66dbb34137ca93612..0908a366e7ac37d217ff2fdfeb0d4ee565159975 100644 GIT binary patch delta 55 zcmX@Pi~Hp+?g?VdyBTCAJ1~hh>TT6yOimP)aL7w6$t+6^$wTT6yOipayn8>(&V>&43z)? diff --git a/Themes/MacOSAquaTheme.res b/Themes/MacOSAquaTheme.res index e3d8448a92d3f67a2bf4c953edc15e268d90ba1a..12611ef55fc40fd7dc6bf9c35b679634b730b504 100644 GIT binary patch delta 55 zcmZ3thU?rKt_fny`V2CY8(Bpg^|tCUKDH5+aL7w6$t+6^$w`%X3Qqt4 diff --git a/Themes/WindowsFluentTheme.res b/Themes/WindowsFluentTheme.res index 3cdfb38bd7146cfad2a262d3f4db9249dc6130e7..d278b9086cd2f8948ff5021d5df07273ab64a52c 100644 GIT binary patch delta 55 zcmaEPoxAe}_XIKKa||+*9hgKL^|tCU?#&UEaL7w6$t+6^$w^Rf diff --git a/Themes/iOSModern27Theme.res b/Themes/iOSModern27Theme.res index 3b00f7e7cc7a0c2a437b96507ed0f9bb45db3bd7..be993f1d39d374e03447af9babcb65b846cf0a56 100644 GIT binary patch delta 3698 zcma)9UrbwN6z{pTrK1-L{db|X{Fe+C5M;#3{DTx&%pigzi!SO?;0_u}TiOnUES6}Z z(Zsm;v3+}3GSR4sG0tVlYNBQ=J}pLFVtko#d-TBeHSg?wFTAn&xh=(DTfuqsCAT|0zm^&0rKzpDa-8p4TqhIyQb#ATC8usN?C^f?2Xe5@HQ?p`Hk~=9{gRFtuJvunn*30BnQ`|(M-j>VX<0ipsJ_G zAEl9zL$dX%KWiR*X{(GyrsdfF-4!f}L{q_?Rn&|v8k~M0z=7i$R=fV58s>LWrV*>K z_6<=HKqO;pB3%IR(57;zRz>s^0S@KZ;h$w>D_>16Zg|Kq8xB{UIw@&+CJ|>Ov&xtu z$j)5Sj@{Dp0d~H8!ME2ry;;sWof)9NW(Cn;{c0gd7i{c;+ZV0$XD>L=Efslg&<3|{ z^nN8c$)?9cKJ+-O70MWb6GHAOHq_la7gNL;ddJD!1-u>&)yq)yy<?F^n?Qe@ zV7=?&to)n^emXMA^0Gb&<@kgd^V5H(poYe#7~hoYOr8V@3%?ZM_iTWdK0F`E6tOcm zp!Kh%!32yO-k^Z$zH7ax)3X+Xj+qn8+Xe-_9TjfeaWc-fLS#-(?@q8msa@~kdrokW zM6fPsKpf=3(&wWfY2?mfO6~l_6ROK{nVu&!sGO)VzpKU9!H9V{xTs5XAR-P9WZ9%g zSHUS^lgPiy{m1YjqGPi~U%vUY9pws>iu?iYV=Mj-!mY^>8eI*<(%li49i*^N9)60e? zOa<;cFz6O4xM*1LtYp;(D@&>IeFp_~nsF)M7Lj;t#jAjWvMCE{B(o*Ju_=qeGp2qF zM6)~=O=n(`$74O!wiwaDZQodat^;i{sa_>{AXflv}dVd@0Y^t#vHSiYD zOEnOr>r;Y_OstEVp`U7?O%t74*O^>!>mu6pvS1hSpwjaSeBzW)(V#!a;n2`c>DKFl z4^#0o9th`d(-S)y$5J4n!A{mMA-%+RmI2h+nf55OHuAtplgxX=8LQ|J}wGyVK*FFyX zuOZ`h)>Hl%O3Kn_g16D^-q_pe&4AiIxx5eFgenvg^FdEf0j~WQOA^=`YX{?zOmtbE znwJ;lVfjqtY%G%=N+e O1=vr>^|X{Zoc{p|mfkb~ delta 242 zcmeBb=G}3NXM!k`$dQfWslTUNGcyWIzVM4>vhIJL=@S|ld8VJ^XB3`1_uua6dzcty zCu>=NMU zuukt3W)z(!%E-FiT!e8h7%g5cX`=ZW6EK-?bgb8#`@Ul_IC~Cn^d;YU?-%simQ5L2YTlNj9m$u~XYk zs1%gIp@<);r7y`yR7m00QUpQ>j8q}u#4SMxZb(HO5C>Y7BTZU(yKDPBd)M*Gmp9*h zGjC?zw{O4wMY{4(x+=HHK3lkVFkN^JpN~P{i@k&Ec*y}CGWuN#DBZyAJzP)3u>{rtvO3X0~OV*738|82O>`?lF!UQ{(cZA1B#2x zMWrTbB(yX%ckG@32mYo)D_CH=ZeOo2XRGT~c4itOJxFv-37`W4)VP-r>fxGtk25hJ2?)WQMOnq&dwUu0KPH-Awz^2O!p;zrPI7q z?j$n>r_COhK4tujnmGN z>dUac8`ge1So6rSC8ft2#a#>W&YaNYZh&Eb0G6D*stkW91m~MHb!0l7tIv%Ga$>-M z_4frH&IM&~;{u@=_j?}AOVQbt$RKaCVlus;s~zMo7k5{pbtA|JdHLfAH*OgjT=>WV z64Iu2;EJCPo4yv1VGdV);M}T39LlKK@rLgUcA8NMLJUvEDa*Y8M9Ql&d?d!g2XpCC zW=1`6UY$}0)rs`QY^iuSpU>LFQsJUnel-Griu2Bc`t%rb;CMp+**x5$@!jPZ(tkGD zurv;vV_$M#a{oT6Cswmqp-3H09UkhZZRU6_whVv(XzW>X)__p0QdpW5x56`)sVZ8k_pn3}9w58yvAHv3uUjeTw zynD1T3!jz^CTOFiEY%c()}m}z3@ir*MpMsWCjPwEG_R27eTRb)YL-RY%|TO`fgj%v z48I0wfo`$_?n@}pX(05k9__)>Nfw9Hslw-8h+~iD(lYEsLjE5YFvfQg)<91Q85Sj` xo62!igj19+<2=zVbt@9 OS Theme Settings* to choose an accent and UI +font or reset the simulated settings. These controls exercise the app's opt-ins; +they do not enable inheritance for the app. Tests can supply a deterministic +snapshot with `JavaSEPort.setSimulatorNativeThemeSettings(...)`. + === Selecting a theme Three build hints control the platform native theme. None of them diff --git a/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java b/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java index cff76a5f0a3..d7306bffdd8 100644 --- a/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java +++ b/maven/core-unittests/src/test/java/com/codename1/testing/TestCodenameOneImplementation.java @@ -1650,6 +1650,7 @@ public void reset() { largerTextEnabled = false; largerTextScale = 1f; nativeTheme = null; + nativeThemeSettings = new com.codename1.impl.NativeThemeSettings(); cameraImpl = null; arImpl = null; visionImplCreationHook = null; @@ -3209,6 +3210,13 @@ public boolean isLargerTextEnabled() { return largerTextEnabled; } + @Override + public com.codename1.impl.NativeThemeSettings getNativeThemeSettings() { + return nativeThemeSettings.copy(); + } + + public com.codename1.impl.NativeThemeSettings nativeThemeSettings = new com.codename1.impl.NativeThemeSettings(); + @Override public float getLargerTextScale() { return largerTextScale; diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/plaf/UIManagerNativeSettingsTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/plaf/UIManagerNativeSettingsTest.java new file mode 100644 index 00000000000..21eb80a67ca --- /dev/null +++ b/maven/core-unittests/src/test/java/com/codename1/ui/plaf/UIManagerNativeSettingsTest.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.ui.plaf; + +import com.codename1.impl.NativeThemeSettings; +import com.codename1.junit.UITestBase; +import com.codename1.ui.Font; +import java.util.Hashtable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class UIManagerNativeSettingsTest extends UITestBase { + private UIManager manager; + + @BeforeEach + void resetSettings() { + manager = UIManager.getInstance(); + manager.setUseNativeColors(false); + manager.setUseNativeFonts(false); + manager.setUseLargerTextScale(false); + implementation.setLargerTextEnabled(false); + implementation.nativeThemeSettings = new NativeThemeSettings().color("accent-color", 0x00aa44) + .font("native:", 24); + } + + private Hashtable nativeTheme() { + Hashtable theme = new Hashtable(); + Font font = Font.createTrueTypeFont(Font.NATIVE_MAIN_REGULAR); + Font.clearDerivedFontCache(); + font = font.derive(16f, Font.STYLE_PLAIN); + theme.put("@nativeThemeDefaultsBool", "true"); + theme.put("@accent-color", "112233"); + theme.put("@cn1-bind:Button.fgColor", "accent-color"); + theme.put("Button.fgColor", "112233"); + assertEquals(16f, font.getPixelSize(), "fixture"); + theme.put("font", font); + theme.put("Button.font", font); + theme.put("Heading.font", font.derive(32f, Font.STYLE_BOLD)); + return theme; + } + + private float size(String uiid) { return manager.getComponentStyle(uiid).getFont().getPixelSize(); } + private int color() { return manager.getComponentStyle("Button").getFgColor(); } + + @Test + void optInsAreIndependentAndOffByDefault() { + manager.setThemeProps(nativeTheme()); + assertEquals(0x112233, color()); + assertEquals(16, size("Button")); + manager.setUseNativeColors(true); + manager.refreshTheme(); + assertEquals(0x00aa44, color()); + assertEquals(16, size("Button")); + manager.setUseNativeFonts(true); + manager.refreshTheme(); + assertEquals(24, size("Button")); + assertEquals(48, size("Heading")); + } + + @Test + void refreshAndDisablingRestoreOriginalsWithoutCompounding() { + manager.setUseNativeColors(true); + manager.setUseNativeFonts(true); + manager.setUseLargerTextScale(true); + implementation.setLargerTextEnabled(true); + implementation.setLargerTextScale(1.5f); + manager.setThemeProps(nativeTheme()); + for (int i = 0; i < 3; i++) { + manager.refreshTheme(); + assertEquals(36, size("Button")); + assertEquals(72, size("Heading")); + } + implementation.nativeThemeSettings = new NativeThemeSettings().color("accent-color", 0xcc6600) + .font("native:", 20); + manager.refreshNativeThemeSettings(); + assertEquals(30, size("Button")); + assertEquals(0xcc6600, color()); + manager.setUseNativeFonts(false); + manager.setUseNativeColors(false); + manager.refreshTheme(); + assertEquals(24, size("Button")); + assertEquals(0x112233, color()); + manager.setUseLargerTextScale(false); + manager.refreshTheme(); + assertEquals(16, size("Button")); + } + + @Test + void appLiteralsAndFontsWinAcrossSettingsChanges() { + manager.setUseNativeColors(true); + manager.setUseNativeFonts(true); + manager.setThemeProps(nativeTheme()); + Hashtable app = new Hashtable(); + app.put("Button.fgColor", "abcdef"); + app.put("Button.font", Font.createTrueTypeFont(Font.NATIVE_MAIN_REGULAR).derive(19f, Font.STYLE_PLAIN)); + manager.addThemeProps(app); + manager.refreshTheme(); + assertEquals(0xabcdef, color()); + assertEquals(19, size("Button")); + assertEquals(48, size("Heading")); + } + + @Test + void appPaletteOverrideAndExplicitFlagsWin() { + manager.setUseNativeColors(true); + manager.setUseNativeFonts(true); + manager.setThemeProps(nativeTheme()); + Hashtable app = new Hashtable(); + app.put("@accent-color", "abcdef"); + app.put("@useNativeFontsBool", "false"); + manager.addThemeProps(app); + manager.refreshTheme(); + assertEquals(0xabcdef, color()); + assertEquals(16, size("Button")); + assertFalse(manager.isUseNativeFonts()); + } + + @Test + void missingSettingsAndThemeReplacementUseFallbacks() { + manager.setUseNativeColors(true); + manager.setUseNativeFonts(true); + manager.setThemeProps(nativeTheme()); + implementation.nativeThemeSettings = new NativeThemeSettings(); + manager.refreshNativeThemeSettings(); + assertEquals(0x112233, color()); + assertEquals(16, size("Button")); + Hashtable app = new Hashtable(); + app.put("Button.fgColor", "998877"); + manager.setThemeProps(app); + implementation.nativeThemeSettings = new NativeThemeSettings().color("accent-color", 0xff0000); + manager.refreshNativeThemeSettings(); + assertEquals(0x998877, color()); + } + + @Test + void unchangedSnapshotDoesNotRebuildTheme() { + manager.setUseNativeColors(true); + manager.setThemeProps(nativeTheme()); + int generation = UIManager.getThemeGeneration(); + manager.refreshNativeThemeSettings(); + assertEquals(generation, UIManager.getThemeGeneration()); + } + + @Test + void nestedNativeThemePreservesInitialAppOverridesOnRefresh() { + implementation.setNativeTheme(nativeTheme()); + manager.setUseNativeColors(true); + manager.setUseNativeFonts(true); + Hashtable app = new Hashtable(); + app.put("@includeNativeBool", "true"); + app.put("Button.fgColor", "778899"); + app.put("Heading.font", Font.createTrueTypeFont(Font.NATIVE_MAIN_REGULAR).derive(27f, Font.STYLE_PLAIN)); + manager.setThemeProps(app); + for (int i = 0; i < 3; i++) { + manager.refreshTheme(); + assertEquals(0x778899, color()); + assertEquals(27, size("Heading")); + assertEquals(24, size("Button")); + } + } + + @Test + void themeConstantsCanEnableEachOptionAndBundledFontsStayUntouched() { + Hashtable theme = nativeTheme(); + theme.put("@useNativeColorsBool", "true"); + theme.put("@useNativeFontsBool", "true"); + Font icon = Font.createTrueTypeFont("Icons", "icons.ttf").derive(23f, Font.STYLE_PLAIN); + theme.put("Icon.font", icon); + manager.setThemeProps(theme); + assertEquals(0x00aa44, color()); + assertEquals(24, size("Button")); + assertSame(icon, manager.getComponentStyle("Icon").getFont()); + } + + @com.codename1.junit.FormTest + void borderAndVisibleComponentTrackSettingsAndRestore() { + manager.setUseNativeColors(true); + Hashtable theme = nativeTheme(); + theme.put("Button.bgColor", "112233"); + theme.put("@cn1-bind:Button.bgColor", "accent-color"); + theme.put("Button.border", RoundBorder.create().color(0x112233)); + manager.setThemeProps(theme); + com.codename1.ui.Form form = display.getCurrent(); + com.codename1.ui.Button button = new com.codename1.ui.Button("Preview"); + form.add(button); + form.revalidate(); + implementation.nativeThemeSettings = new NativeThemeSettings().color("accent-color", 0x556677); + manager.refreshNativeThemeSettings(); + assertEquals(0x556677, button.getUnselectedStyle().getBgColor()); + assertEquals(0x556677, ((RoundBorder) button.getUnselectedStyle().getBorder()).getColor()); + implementation.nativeThemeSettings = new NativeThemeSettings(); + manager.refreshNativeThemeSettings(); + assertEquals(0x112233, button.getUnselectedStyle().getBgColor()); + assertEquals(0x112233, ((RoundBorder) button.getUnselectedStyle().getBorder()).getColor()); + } + + @Test + void explicitAppBorderIsNotRetintedByNativeBackground() { + manager.setUseNativeColors(true); + Hashtable theme = nativeTheme(); + theme.put("Button.bgColor", "112233"); + theme.put("@cn1-bind:Button.bgColor", "accent-color"); + theme.put("Button.border", RoundBorder.create().color(0x112233)); + manager.setThemeProps(theme); + RoundBorder appBorder = RoundBorder.create().color(0xff00ff); + Hashtable app = new Hashtable(); + app.put("Button.border", appBorder); + manager.addThemeProps(app); + manager.refreshTheme(); + assertEquals(0xff00ff, appBorder.getColor()); + } + + @Test + void disabledCategoryChangesDoNotRebuildTheme() { + manager.setUseNativeColors(true); + manager.setThemeProps(nativeTheme()); + int generation = UIManager.getThemeGeneration(); + implementation.nativeThemeSettings = new NativeThemeSettings().color("accent-color", 0x00aa44) + .font("Different", 40); + manager.refreshNativeThemeSettings(); + assertEquals(generation, UIManager.getThemeGeneration()); + } + + @Test + void shippedThemesExposeInheritableBodyFonts() throws Exception { + String[] files = {"iOSModernTheme.res", "iOSModern27Theme.res", "AndroidMaterialTheme.res", + "WindowsFluentTheme.res", "MacOSAquaTheme.res", "GnomeAdwaitaTheme.res"}; + java.io.File root = new java.io.File(System.getProperty("user.dir")); + while (root != null && !new java.io.File(root, "native-themes").isDirectory()) { root = root.getParentFile(); } + assertNotNull(root); + for (String name : files) { + java.io.File file = new java.io.File(new java.io.File(root, "Themes"), name); + if (!file.isFile()) { continue; } // Core-only builds need no theme artifacts. + com.codename1.ui.util.Resources resource; + try (java.io.InputStream input = new java.io.FileInputStream(file)) { + resource = com.codename1.ui.util.Resources.open(input); + } + Hashtable theme = resource.getTheme(resource.getThemeResourceNames()[0]); + assertEquals("true", theme.get("@nativeThemeDefaultsBool"), name); + manager.setUseNativeFonts(true); + manager.setThemeProps(theme); + assertEquals(24f, size("Label"), 0.01f, name); + manager.refreshTheme(); + assertEquals(24f, size("Label"), 0.01f, name + " refreshed"); + } + } + + @Test + void snapshotRejectsInvalidFontSizesAndMalformedColors() { + NativeThemeSettings settings = NativeThemeSettings.parse( + "fontFamily=Sans\nfontSize=NaN\naccent-color=zzzzzz\ntext-color=112233\nunknown"); + assertNull(settings.getFontFamily()); + assertNull(settings.getColor("accent-color")); + assertEquals("112233", settings.getColor("text-color")); + } +} diff --git a/maven/javase/src/test/java/com/codename1/impl/javase/NativeThemeSettingsSimulationTest.java b/maven/javase/src/test/java/com/codename1/impl/javase/NativeThemeSettingsSimulationTest.java new file mode 100644 index 00000000000..64b7affda9f --- /dev/null +++ b/maven/javase/src/test/java/com/codename1/impl/javase/NativeThemeSettingsSimulationTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.javase; + +import com.codename1.impl.NativeThemeSettings; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class NativeThemeSettingsSimulationTest { + private JavaSEPort previous; + + @org.junit.jupiter.api.BeforeEach + void savePort() { previous = JavaSEPort.instance; } + + @org.junit.jupiter.api.AfterEach + void restorePort() { JavaSEPort.instance = previous; } + + @Test + void simulatorSnapshotsAreDefensiveAndDeterministic() { + JavaSEPort port = new JavaSEPort(); + NativeThemeSettings input = new NativeThemeSettings().color("accent-color", 0x123456).font("Dialog", 19); + port.setSimulatorNativeThemeSettings(input); + input.color("accent-color", 0xffffff); + assertEquals("123456", port.getNativeThemeSettings().getColor("accent-color")); + port.getNativeThemeSettings().color("accent-color", 0); + assertEquals("123456", port.getNativeThemeSettings().getColor("accent-color")); + assertEquals(19f, port.getNativeThemeSettings().getFontSize()); + } + + @Test + void selectedUiFontPreservesRequestedSizeAndStyle() { + JavaSEPort port = new JavaSEPort(); + java.awt.Font font = (java.awt.Font) port.loadNativeThemeFont("Serif", "native:MainBold", 23, + java.awt.Font.BOLD | java.awt.Font.ITALIC); + assertEquals("Serif", font.getFamily()); + assertEquals(23f, font.getSize2D()); + assertTrue(font.isBold()); + assertTrue(font.isItalic()); + } +} diff --git a/native-themes/README.md b/native-themes/README.md index 47ee328634e..c91c9f8839f 100644 --- a/native-themes/README.md +++ b/native-themes/README.md @@ -5,6 +5,20 @@ They are compiled by `scripts/build-native-themes.sh` (which invokes the thin `maven/css-compiler` jar with `strictNoCef=true`) into `.res` files under the repo's `Themes/` directory, alongside the legacy hand-authored themes. +## Optional OS defaults + +Bundled themes declare `nativeThemeDefaultsBool: true`. This internal marker lets +UIManager distinguish their defaults from application overrides during live OS +settings updates. Keep it in bundled theme sources; do not add it to application +themes. `Label.font` is the body-size reference for proportional font inheritance +(the default `font` entry is the fallback for a theme without `Label.font`). + +Applications opt in independently with `useNativeColorsBool` and +`useNativeFontsBool`; both default to false. Larger-text scaling retains its +existing, separate control. Semantic colors must use `var(--token, fallback)` +bindings to participate. Keep every fallback identical when adding a binding, and +preserve intentional differences between iOS generation layers. + ## Layout ``` diff --git a/native-themes/android-material/theme.css b/native-themes/android-material/theme.css index 197434171c9..cb9c8a621d8 100644 --- a/native-themes/android-material/theme.css +++ b/native-themes/android-material/theme.css @@ -70,6 +70,14 @@ */ #Constants { + /* Marks bundled defaults eligible for optional OS settings inheritance. */ + nativeThemeDefaultsBool: true; + --text-color: #1d1b20; + --text-color-dark: #e6e0e9; + --text-secondary-color: #49454f; + --text-secondary-color-dark: #cac4d0; + --window-bg-color: #fef7ff; + --window-bg-color-dark: #141218; /* Material 3 primary accent palette overridable at runtime via addThemeProps. Each var(--name, fallback) embeds the fallback as the baked-in default and emits a @cn1-bind constant the runtime @@ -174,14 +182,14 @@ } Component { - color: #1d1b20; - background-color: #fef7ff; + color: var(--text-color, #1d1b20); + background-color: var(--window-bg-color, #fef7ff); padding: 0; margin: 0; } -Form { background-color: #fef7ff; padding: 0; margin: 0; } -ContentPane { background-color: #fef7ff; padding: 0; margin: 0; } +Form { background-color: var(--window-bg-color, #fef7ff); padding: 0; margin: 0; } +ContentPane { background-color: var(--window-bg-color, #fef7ff); padding: 0; margin: 0; } /* Container is layout-only by default - transparent so wrappers like SpanLabel (which uses UIID="Container") inside a coloured parent (Dialog, Tab pill) let that parent's surface show @@ -192,7 +200,7 @@ Container { background-color: transparent; padding: 0; margin: 0; } SpanLabel's internal TextArea uses UIID="Label" so this also fixes the visible block behind dialog body text. */ Label { - color: #1d1b20; + color: var(--text-color, #1d1b20); background-color: transparent; padding: 0.5mm 1.5mm 0.5mm 1.5mm; margin: 0; @@ -200,7 +208,7 @@ Label { font-size: 2.5mm; } -SecondaryLabel { cn1-derive: Label; color: #49454f; } +SecondaryLabel { cn1-derive: Label; color: var(--text-secondary-color, #49454f); } TertiaryLabel { cn1-derive: Label; color: #79747e; } /* SpanLabel + SpanLabelText transparent so they sit cleanly over a translucent / coloured parent (Dialog, PopupContent) without @@ -309,7 +317,7 @@ FlatButton.pressed { background-color: transparent; color: var(--accent-color, # reason given at the top of this file: the per-state Style merge otherwise drifts the text by a pixel as the selection changes. */ ToggleButton { - color: #1d1b20; + color: var(--text-color, #1d1b20); background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #79747e; @@ -351,7 +359,7 @@ ToggleButton { and those conditions drawn some other way -- rather than collapsing three dimensions onto one style lookup. That reaches every focusable component, so it is not a line in a theme. */ -ToggleButton.selected { color: #1d1b20; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid var(--accent-color, #6750a4); padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } +ToggleButton.selected { color: var(--text-color, #1d1b20); background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid var(--accent-color, #6750a4); padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } ToggleButton.pressed { color: var(--accent-on-container-color, #21005d); background-color: var(--accent-container-color, #eaddff); cn1-background-type: cn1-pill-border; border: 0.25mm solid #79747e; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } ToggleButton.disabled { color: #a5a0ab; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #cac4d0; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } @@ -364,22 +372,22 @@ ToggleButton.disabled { color: #a5a0ab; background-color: transparent; cn1-backg and every state is spelled out because cn1-derive is only read at base level. */ ToggleButtonFirst { cn1-derive: ToggleButton; } -ToggleButtonFirst.selected { color: #1d1b20; background-color: transparent; cn1-background-type: cn1-pill-border; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; cn1-derive: ToggleButton; border: 0.25mm solid var(--accent-color, #6750a4); } +ToggleButtonFirst.selected { color: var(--text-color, #1d1b20); background-color: transparent; cn1-background-type: cn1-pill-border; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; cn1-derive: ToggleButton; border: 0.25mm solid var(--accent-color, #6750a4); } ToggleButtonFirst.pressed { color: var(--accent-on-container-color, #21005d); background-color: var(--accent-container-color, #eaddff); cn1-background-type: cn1-pill-border; border: 0.25mm solid #79747e; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } ToggleButtonFirst.disabled { color: #a5a0ab; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #cac4d0; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } ToggleButtonLast { cn1-derive: ToggleButton; } -ToggleButtonLast.selected { color: #1d1b20; background-color: transparent; cn1-background-type: cn1-pill-border; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; cn1-derive: ToggleButton; border: 0.25mm solid var(--accent-color, #6750a4); } +ToggleButtonLast.selected { color: var(--text-color, #1d1b20); background-color: transparent; cn1-background-type: cn1-pill-border; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; cn1-derive: ToggleButton; border: 0.25mm solid var(--accent-color, #6750a4); } ToggleButtonLast.pressed { color: var(--accent-on-container-color, #21005d); background-color: var(--accent-container-color, #eaddff); cn1-background-type: cn1-pill-border; border: 0.25mm solid #79747e; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } ToggleButtonLast.disabled { color: #a5a0ab; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #cac4d0; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } ToggleButtonOnly { cn1-derive: ToggleButton; } -ToggleButtonOnly.selected { color: #1d1b20; background-color: transparent; cn1-background-type: cn1-pill-border; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; cn1-derive: ToggleButton; border: 0.25mm solid var(--accent-color, #6750a4); } +ToggleButtonOnly.selected { color: var(--text-color, #1d1b20); background-color: transparent; cn1-background-type: cn1-pill-border; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; cn1-derive: ToggleButton; border: 0.25mm solid var(--accent-color, #6750a4); } ToggleButtonOnly.pressed { color: var(--accent-on-container-color, #21005d); background-color: var(--accent-container-color, #eaddff); cn1-background-type: cn1-pill-border; border: 0.25mm solid #79747e; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } ToggleButtonOnly.disabled { color: #a5a0ab; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #cac4d0; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } /* Material 3 OUTLINED text field: no fill, a thin outline, taller (more vertical padding), tight margins. A line border is CEF-free. */ TextField { - color: #1d1b20; + color: var(--text-color, #1d1b20); background-color: transparent; padding: 2.7mm 2.5mm 2.63mm 2.5mm; margin: 0.4mm 0 0.4mm 0; @@ -393,7 +401,7 @@ TextField.pressed { background-color: transparent; border: 0.3mm solid var(--acc TextField.disabled { color: #a5a0ab; background-color: transparent; border: 0.2mm solid #a9a8aa; border-radius: 0.55mm; } TextArea { - color: #1d1b20; + color: var(--text-color, #1d1b20); background-color: #f3edf7; padding: 1.5mm 2.5mm 1.5mm 2.5mm; margin: 0.8mm 1.5mm 0.8mm 1.5mm; @@ -411,7 +419,7 @@ TextHint { } CheckBox { - color: #1d1b20; + color: var(--text-color, #1d1b20); background-color: transparent; padding: 1.1mm 1mm 1.1mm 0.6mm; margin: 0; @@ -425,7 +433,7 @@ CheckBox.selected { color: var(--accent-color, #6750a4); } CheckBox.disabled { color: #777679; background-color: transparent; } RadioButton { - color: #1d1b20; + color: var(--text-color, #1d1b20); background-color: transparent; padding: 1.1mm 1mm 1.1mm 0.6mm; margin: 0; @@ -440,9 +448,9 @@ RadioButton.disabled { color: #777679; background-color: transparent; } from the on-surface label. Read by the framework via *UncheckedColorUIID. The .disabled variant greys the box outline (38% on-surface) independently of the darker disabled label text. */ -CheckBoxUnchecked { cn1-derive: CheckBox; color: #49454f; } +CheckBoxUnchecked { cn1-derive: CheckBox; color: var(--text-secondary-color, #49454f); } CheckBoxUnchecked.disabled { color: #a9a8aa; } -RadioButtonUnchecked { cn1-derive: RadioButton; color: #49454f; } +RadioButtonUnchecked { cn1-derive: RadioButton; color: var(--text-secondary-color, #49454f); } RadioButtonUnchecked.disabled { color: #a9a8aa; } /* Material 3 slider/progress: thin rounded track + accent-filled portion. border-radius without a border is CEF-free. */ @@ -493,7 +501,7 @@ Switch { text-align: left; } Switch.selected { color: var(--accent-on-color, #ffffff); background-color: var(--accent-color, #6750a4); } -Switch.disabled { color: #a5a0ab; background-color: #fef7ff; } +Switch.disabled { color: #a5a0ab; background-color: var(--window-bg-color, #fef7ff); } /* Subtle near-surface ring colour for the disabled track (distinct from the more visible disabled thumb). Read via switchDisabledOutlineColorUIID. */ SwitchDisabledTrack { color: #e1dfe1; } @@ -512,8 +520,8 @@ Toolbar { a transparent bar only looks right when whatever is behind it happens to be the surface colour -- the dark fidelity tile showed the bar vanishing entirely into a black backdrop (57% fidelity). */ - background-color: #fef7ff; - color: #1d1b20; + background-color: var(--window-bg-color, #fef7ff); + color: var(--text-color, #1d1b20); padding: 2mm 1mm 2mm 1mm; margin: 0; } @@ -522,13 +530,13 @@ Toolbar { different purposes and keeping inheritance simple is safer. */ TitleArea { background-color: transparent; - color: #1d1b20; + color: var(--text-color, #1d1b20); padding: 1mm 2mm 1mm 2mm; margin: 0; } Title { - color: #1d1b20; + color: var(--text-color, #1d1b20); background-color: transparent; padding: 0.8mm 1.5mm 0.8mm 1.5mm; font-family: "native:MainRegular"; @@ -598,11 +606,11 @@ DesktopHorizontalScrollThumb.pressed { color rather than a pill fill. No border-radius here - combining it with a visible border would drop into the 9-piece rasterization path. */ -Tabs { background-color: #fef7ff; padding: 0; margin: 0; } +Tabs { background-color: var(--window-bg-color, #fef7ff); padding: 0; margin: 0; } /* border:none on the tab UIIDs: when this theme is LAYERED over another via addThemeProps (the Playground preview, runtime theme switching) a base theme's Tab border would otherwise survive the merge and box every cell. */ -TabsContainer { border: none; background-color: #fef7ff; color: #1d1b20; padding: 0; margin: 0; } +TabsContainer { border: none; background-color: var(--window-bg-color, #fef7ff); color: var(--text-color, #1d1b20); padding: 0; margin: 0; } /* Material 3 TabLayout draws a full-width hairline divider along its bottom edge (surfaceVariant outline). The tab row is a custom Container that does not paint a border, so Tabs reads this UIID's bgColor and paints the line directly @@ -610,9 +618,9 @@ TabsContainer { border: none; background-color: #fef7ff; color: #1d1b20; padding TabsDivider { background-color: #cac4d0; } Tab { - color: #49454f; + color: var(--text-secondary-color, #49454f); border: none; - background-color: #fef7ff; + background-color: var(--window-bg-color, #fef7ff); /* Top padding tuned against the native tile: 2.2mm sat the icon+label ~6px lower than TabLayout and grew the bar by the same 6px. */ /* Bottom padding sets the bar's total height: 2.1mm left the bar edge @@ -631,29 +639,29 @@ Tab { icon-gap: 1mm; text-align: center; } -Tab.selected { color: var(--accent-color, #6750a4); border: none; background-color: #fef7ff; font-family: "native:MainBold"; font-size: 2.25mm; } -Tab.pressed { color: var(--accent-color, #6750a4); border: none; background-color: #fef7ff; } +Tab.selected { color: var(--accent-color, #6750a4); border: none; background-color: var(--window-bg-color, #fef7ff); font-family: "native:MainBold"; font-size: 2.25mm; } +Tab.pressed { color: var(--accent-color, #6750a4); border: none; background-color: var(--window-bg-color, #fef7ff); } -SelectedTab { cn1-derive: Tab; color: var(--accent-color, #6750a4); background-color: #fef7ff; font-family: "native:MainBold"; font-size: 2.25mm; } -UnselectedTab { cn1-derive: Tab; color: #49454f; } +SelectedTab { cn1-derive: Tab; color: var(--accent-color, #6750a4); background-color: var(--window-bg-color, #fef7ff); font-family: "native:MainBold"; font-size: 2.25mm; } +UnselectedTab { cn1-derive: Tab; color: var(--text-secondary-color, #49454f); } /* TabIndicator: color reference read by Tabs.paintAnimatedIndicator() for the sliding underline. The actual drawing is done in Java; this UIID only contributes the foreground colour. */ TabIndicator { color: var(--accent-color, #6750a4); background-color: transparent; padding: 0; margin: 0; } -SideNavigationPanel { background-color: #fef7ff; padding: 0; margin: 0; } +SideNavigationPanel { background-color: var(--window-bg-color, #fef7ff); padding: 0; margin: 0; } SideCommand { cn1-derive: Button; background-color: transparent; - color: #1d1b20; + color: var(--text-color, #1d1b20); padding: 2mm 3mm 2mm 3mm; margin: 0; } SideCommand.pressed { background-color: var(--accent-container-color, #eaddff); } -List { background-color: #fef7ff; padding: 0; margin: 0; } +List { background-color: var(--window-bg-color, #fef7ff); padding: 0; margin: 0; } ListRenderer { cn1-derive: Label; padding: 1.5mm 2mm 1.5mm 2mm; } /* Selection needs an explicit style: without it the row falls back to the @@ -670,8 +678,8 @@ ListRenderer.pressed { background-color: var(--accent-container-color, #eaddff); in light mode against white). Inline a clean rectangular row look instead. */ MultiButton { - background-color: #fef7ff; - color: #1d1b20; + background-color: var(--window-bg-color, #fef7ff); + color: var(--text-color, #1d1b20); padding: 2mm 3mm 2mm 3mm; margin: 0; font-family: "native:MainRegular"; @@ -680,8 +688,8 @@ MultiButton { MultiButton.pressed { background-color: var(--accent-container-color, #eaddff); } MultiButton.disabled { color: #a5a0ab; background-color: #e0dce4; } -MultiLine1 { cn1-derive: Label; color: #1d1b20; font-family: "native:MainBold"; font-size: 2.5mm; } -MultiLine2 { cn1-derive: Label; color: #49454f; } +MultiLine1 { cn1-derive: Label; color: var(--text-color, #1d1b20); font-family: "native:MainBold"; font-size: 2.5mm; } +MultiLine2 { cn1-derive: Label; color: var(--text-secondary-color, #49454f); } MultiLine3 { cn1-derive: Label; color: #79747e; } MultiLine4 { cn1-derive: Label; color: #79747e; } @@ -692,7 +700,7 @@ Dialog { /* Material 3 alert surface = surfaceContainerHigh (#ece6f0), matching the native AlertDialog. */ background-color: #ece6f0; - color: #1d1b20; + color: var(--text-color, #1d1b20); padding: 2mm; margin: 0; /* Material 3 dialog corner = 28dp (4.4mm at 160dpi); 6mm read visibly @@ -709,7 +717,7 @@ Dialog { } DialogBody { background-color: transparent; - color: #49454f; + color: var(--text-secondary-color, #49454f); padding: 0 2mm 0 2mm; margin: 0; font-family: "native:MainRegular"; @@ -729,7 +737,7 @@ DialogButton { text-align: center; } DialogTitle { - color: #1d1b20; + color: var(--text-color, #1d1b20); background-color: transparent; padding: 1.5mm 2mm 1.5mm 2mm; font-family: "native:MainRegular"; @@ -774,7 +782,7 @@ ChatBubbleUser { } ChatBubbleAssistant { background-color: #e6e0e9; - color: #1d1b20; + color: var(--text-color, #1d1b20); padding: 2mm 3mm; margin: 0.5mm 0; border-radius: 4mm; @@ -802,7 +810,7 @@ ChatVoiceButton { cn1-derive: Button; padding: 1.5mm; margin: 0.5mm; } Separator { background-color: #cac4d0; padding: 0; margin: 0; } PopupContent { background-color: #e6e0e9; - color: #1d1b20; + color: var(--text-color, #1d1b20); padding: 2mm; margin: 0; border-radius: 7mm; @@ -852,7 +860,7 @@ PhoneVerificationLink { above the field once it has content, and an error line beneath it. All three were undefined, so the control the guide recommends over a raw TextField rendered with default styling. */ -FloatingHint { cn1-derive: SecondaryLabel; color: #49454f; font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } +FloatingHint { cn1-derive: SecondaryLabel; color: var(--text-secondary-color, #49454f); font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } FloatingHint.disabled { padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; font-family: "native:MainRegular"; font-size: 1.9mm; color: #a9a8aa; background-color: transparent; } /* M3 error role; the line sits under the field and carries the message. */ ErrorLabel { cn1-derive: SecondaryLabel; color: #b3261e; font-size: 1.9mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } @@ -861,8 +869,8 @@ ErrorLabel.disabled { padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; font-family: "nat /* Table's documented cell/header UIIDs. The framework does not set these itself -- the chapter tells you to -- so defining them makes the documented pattern land on a real style instead of the default. */ -TableCell { cn1-derive: Label; color: #1d1b20; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } -TableHeader { cn1-derive: Label; color: #49454f; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableCell { cn1-derive: Label; color: var(--text-color, #1d1b20); padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableHeader { cn1-derive: Label; color: var(--text-secondary-color, #49454f); font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } /* SideMenuBar drops this icon-only button into the TitleArea's east slot, so it takes the compact title-command treatment. FlatButton would give it a @@ -875,14 +883,14 @@ TableHeader { cn1-derive: Label; color: #49454f; font-family: "native:MainBold"; Where a state is meant to differ it is declared above and left alone here. */ ErrorLabel.selected { font-family: "native:MainRegular"; color: #b3261e; font-size: 1.9mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } ErrorLabel.pressed { font-family: "native:MainRegular"; color: #b3261e; font-size: 1.9mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } -FloatingHint.selected { font-family: "native:MainRegular"; color: #49454f; font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } -FloatingHint.pressed { font-family: "native:MainRegular"; color: #49454f; font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } -TableCell.selected { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: #1d1b20; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } -TableCell.pressed { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: #1d1b20; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } -TableCell.disabled { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: #1d1b20; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } -TableHeader.selected { margin: 0; font-size: 2.5mm; color: #49454f; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } -TableHeader.pressed { margin: 0; font-size: 2.5mm; color: #49454f; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } -TableHeader.disabled { margin: 0; font-size: 2.5mm; color: #49454f; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +FloatingHint.selected { font-family: "native:MainRegular"; color: var(--text-secondary-color, #49454f); font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } +FloatingHint.pressed { font-family: "native:MainRegular"; color: var(--text-secondary-color, #49454f); font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } +TableCell.selected { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: var(--text-color, #1d1b20); padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableCell.pressed { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: var(--text-color, #1d1b20); padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableCell.disabled { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: var(--text-color, #1d1b20); padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableHeader.selected { margin: 0; font-size: 2.5mm; color: var(--text-secondary-color, #49454f); font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableHeader.pressed { margin: 0; font-size: 2.5mm; color: var(--text-secondary-color, #49454f); font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } +TableHeader.disabled { margin: 0; font-size: 2.5mm; color: var(--text-secondary-color, #49454f); font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } /* Every UIID above is completed to all four states below. A missing state is not inherited: cn1-derive is read at base level only, and a lookup of @@ -891,15 +899,15 @@ TableHeader.disabled { margin: 0; font-size: 2.5mm; color: #49454f; font-family: Where a state is meant to differ it is declared above and left alone here. */ @media (prefers-color-scheme: dark) { - Component { color: #e6e0e9; background-color: #141218; } - Form { background-color: #141218; padding: 0; margin: 0; } - ContentPane { background-color: #141218; padding: 0; margin: 0; } + Component { color: var(--text-color-dark, #e6e0e9); background-color: var(--window-bg-color-dark, #141218); } + Form { background-color: var(--window-bg-color-dark, #141218); padding: 0; margin: 0; } + ContentPane { background-color: var(--window-bg-color-dark, #141218); padding: 0; margin: 0; } - Label { color: #e6e0e9; background-color: transparent; } - SecondaryLabel { color: #cac4d0; background-color: transparent; } + Label { color: var(--text-color-dark, #e6e0e9); background-color: transparent; } + SecondaryLabel { color: var(--text-secondary-color-dark, #cac4d0); background-color: transparent; } TertiaryLabel { color: #938f99; background-color: transparent; } - SpanLabel { color: #e6e0e9; background-color: transparent; } - SpanLabelText { color: #e6e0e9; background-color: transparent; } + SpanLabel { color: var(--text-color-dark, #e6e0e9); background-color: transparent; } + SpanLabelText { color: var(--text-color-dark, #e6e0e9); background-color: transparent; } /* Re-declare cn1-background-type on each dark Button override - the compiler doesn't carry the light declaration's pill border @@ -921,23 +929,23 @@ TableHeader.disabled { margin: 0; font-size: 2.5mm; color: #49454f; font-family: FlatButton.pressed { background-color: transparent; color: var(--accent-color-dark, #d0bcff); cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; } - TextField { color: #e6e0e9; background-color: transparent; border: 0.2mm solid #938f99; border-radius: 0.55mm; } + TextField { color: var(--text-color-dark, #e6e0e9); background-color: transparent; border: 0.2mm solid #938f99; border-radius: 0.55mm; } TextField.pressed { background-color: #49454f; } TextField.disabled { color: #5c5967; background-color: transparent; border: 0.2mm solid #49454f; border-radius: 0.55mm; } - TextArea { color: #e6e0e9; background-color: #211f26; } + TextArea { color: var(--text-color-dark, #e6e0e9); background-color: #211f26; } TextArea.pressed { background-color: #49454f; } TextArea.disabled { color: #5c5967; background-color: #2b2930; } TextHint { color: #938f99; } - CheckBox { color: #e6e0e9; background-color: transparent; } + CheckBox { color: var(--text-color-dark, #e6e0e9); background-color: transparent; } CheckBox.selected { color: var(--accent-color-dark, #d0bcff); } CheckBox.disabled { color: #8a868c; background-color: transparent; } - RadioButton { color: #e6e0e9; background-color: transparent; } + RadioButton { color: var(--text-color-dark, #e6e0e9); background-color: transparent; } RadioButton.selected { color: var(--accent-color-dark, #d0bcff); } RadioButton.disabled { color: #8a868c; background-color: transparent; } - CheckBoxUnchecked { color: #cac4d0; } + CheckBoxUnchecked { color: var(--text-secondary-color-dark, #cac4d0); } CheckBoxUnchecked.disabled { color: #575559; } - RadioButtonUnchecked { color: #cac4d0; } + RadioButtonUnchecked { color: var(--text-secondary-color-dark, #cac4d0); } RadioButtonUnchecked.disabled { color: #575559; } Slider { background-color: #49454f; color: #8e8e93; border-radius: 1mm; } SliderFull { background-color: var(--accent-color-dark, #d0bcff); color: var(--accent-color-dark, #d0bcff); border-radius: 1mm; } @@ -948,16 +956,16 @@ TableHeader.disabled { margin: 0; font-size: 2.5mm; color: #49454f; font-family: Switch { color: #938f99; background-color: #36343b; } Switch.selected { color: var(--accent-on-color-dark, #381e72); background-color: var(--accent-color-dark, #d0bcff); } - Switch.disabled { color: #5c5967; background-color: #141218; } + Switch.disabled { color: #5c5967; background-color: var(--window-bg-color-dark, #141218); } SwitchDisabledTrack { color: #222022; } OnOffSwitch { color: var(--accent-on-color-dark, #381e72); background-color: #49454f; } OnOffSwitch.selected { background-color: var(--accent-color-dark, #d0bcff); color: var(--accent-on-color-dark, #381e72); } - Toolbar { background-color: #141218; color: #e6e0e9; } - TitleArea { background-color: transparent; color: #e6e0e9; } - Title { color: #e6e0e9; background-color: transparent; } - MainTitle { color: #e6e0e9; background-color: transparent; } + Toolbar { background-color: var(--window-bg-color-dark, #141218); color: var(--text-color-dark, #e6e0e9); } + TitleArea { background-color: transparent; color: var(--text-color-dark, #e6e0e9); } + Title { color: var(--text-color-dark, #e6e0e9); background-color: transparent; } + MainTitle { color: var(--text-color-dark, #e6e0e9); background-color: transparent; } BackCommand { color: var(--accent-color-dark, #d0bcff); background-color: transparent; } TitleCommand { color: var(--accent-color-dark, #d0bcff); background-color: transparent; } @@ -972,30 +980,30 @@ TableHeader.disabled { margin: 0; font-size: 2.5mm; color: #49454f; font-family: DesktopHorizontalScrollThumb.selected { background-color: #aea9b4; cn1-background-type: cn1-round-border; } DesktopHorizontalScrollThumb.pressed { background-color: #cac4d0; cn1-background-type: cn1-round-border; } - Tabs { background-color: #141218; } - TabsContainer { background-color: #141218; color: #e6e0e9; } + Tabs { background-color: var(--window-bg-color-dark, #141218); } + TabsContainer { background-color: var(--window-bg-color-dark, #141218); color: var(--text-color-dark, #e6e0e9); } TabsDivider { background-color: #49454f; } - Tab { color: #cac4d0; background-color: #141218; } - Tab.selected { color: var(--accent-color-dark, #d0bcff); background-color: #141218; font-family: "native:MainBold"; font-size: 2.25mm; } - Tab.pressed { color: var(--accent-color-dark, #d0bcff); background-color: #141218; } - SelectedTab { color: var(--accent-color-dark, #d0bcff); background-color: #141218; font-family: "native:MainBold"; font-size: 2.25mm; } - UnselectedTab { color: #cac4d0; } + Tab { color: var(--text-secondary-color-dark, #cac4d0); background-color: var(--window-bg-color-dark, #141218); } + Tab.selected { color: var(--accent-color-dark, #d0bcff); background-color: var(--window-bg-color-dark, #141218); font-family: "native:MainBold"; font-size: 2.25mm; } + Tab.pressed { color: var(--accent-color-dark, #d0bcff); background-color: var(--window-bg-color-dark, #141218); } + SelectedTab { color: var(--accent-color-dark, #d0bcff); background-color: var(--window-bg-color-dark, #141218); font-family: "native:MainBold"; font-size: 2.25mm; } + UnselectedTab { color: var(--text-secondary-color-dark, #cac4d0); } /* Animated indicator inherits the dark-mode accent. */ TabIndicator { color: var(--accent-color-dark, #d0bcff); background-color: transparent; } - SideNavigationPanel { background-color: #141218; } - SideCommand { color: #e6e0e9; background-color: transparent; } + SideNavigationPanel { background-color: var(--window-bg-color-dark, #141218); } + SideCommand { color: var(--text-color-dark, #e6e0e9); background-color: transparent; } SideCommand.pressed { background-color: var(--accent-pressed-color-dark, #4f378b); } - List { background-color: #141218; } - ListRenderer { color: #e6e0e9; background-color: transparent; } + List { background-color: var(--window-bg-color-dark, #141218); } + ListRenderer { color: var(--text-color-dark, #e6e0e9); background-color: transparent; } ListRenderer.selected { background-color: var(--accent-container-color-dark, #4a4458); color: var(--accent-on-container-color-dark, #e6e0e9); } ListRenderer.pressed { background-color: var(--accent-container-color-dark, #4a4458); color: var(--accent-on-container-color-dark, #e6e0e9); } - MultiButton { background-color: #141218; color: #e6e0e9; } + MultiButton { background-color: var(--window-bg-color-dark, #141218); color: var(--text-color-dark, #e6e0e9); } MultiButton.pressed { background-color: var(--accent-pressed-color-dark, #4f378b); } - MultiLine1 { color: #e6e0e9; } - MultiLine2 { color: #cac4d0; } + MultiLine1 { color: var(--text-color-dark, #e6e0e9); } + MultiLine2 { color: var(--text-secondary-color-dark, #cac4d0); } MultiLine3 { color: #938f99; } MultiLine4 { color: #938f99; } @@ -1005,33 +1013,33 @@ TableHeader.disabled { margin: 0; font-size: 2.5mm; color: #49454f; font-family: use surfaceContainerHigh (#2b2930) with NO outline (tonal elevation, not a stroke -- the golden-tile diff showed the old #211f26 + 0.4mm grey border as the two biggest dark-dialog gaps). */ - Dialog { background-color: #2b2930; color: #e6e0e9; border: none; border-radius: 4.4mm; } - DialogBody { background-color: transparent; color: #cac4d0; } - DialogTitle { color: #e6e0e9; background-color: transparent; } + Dialog { background-color: #2b2930; color: var(--text-color-dark, #e6e0e9); border: none; border-radius: 4.4mm; } + DialogBody { background-color: transparent; color: var(--text-secondary-color-dark, #cac4d0); } + DialogTitle { color: var(--text-color-dark, #e6e0e9); background-color: transparent; } DialogButton { color: var(--accent-color-dark, #d0bcff); background-color: transparent; padding: 1mm 2.4mm 1mm 2.4mm; margin: 0; font-family: "native:MainRegular"; font-size: 2.25mm; } DialogContentPane { background-color: transparent; } DialogCommandArea { background-color: transparent; } - PopupContent { background-color: #211f26; color: #e6e0e9; } + PopupContent { background-color: #211f26; color: var(--text-color-dark, #e6e0e9); } FloatingActionButton { color: var(--accent-on-container-color-dark, #eaddff); background-color: var(--accent-container-color-dark, #4f378b); } /* #4f378b + #eaddff@12% -- the dark pressed state layer lightens. */ FloatingActionButton.pressed { background-color: #624b99; } - ChatView { background-color: #141218; } - ChatViewMessages { background-color: #141218; } + ChatView { background-color: var(--window-bg-color-dark, #141218); } + ChatViewMessages { background-color: var(--window-bg-color-dark, #141218); } ChatBubbleUser { background-color: var(--accent-container-color-dark, #4f378b); color: var(--accent-on-container-color-dark, #eaddff); } - ChatBubbleAssistant { background-color: #2b2930; color: #e6e0e9; } + ChatBubbleAssistant { background-color: #2b2930; color: var(--text-color-dark, #e6e0e9); } ChatBubbleSystem { color: #938f99; } - ChatBubbleText { color: #e6e0e9; background-color: transparent; } + ChatBubbleText { color: var(--text-color-dark, #e6e0e9); background-color: transparent; } ChatTypingIndicator { color: #938f99; } - ChatInput { background-color: #141218; } - ChatInputField { background-color: #1d1b20; color: #e6e0e9; border-radius: 3mm; } + ChatInput { background-color: var(--window-bg-color-dark, #141218); } + ChatInputField { background-color: #1d1b20; color: var(--text-color-dark, #e6e0e9); border-radius: 3mm; } Separator { background-color: #49454f; } - OtpDigit { color: #e6e0e9; } - PhoneNumberText { color: #e6e0e9; } - PhoneVerificationText { color: #cac4d0; } + OtpDigit { color: var(--text-color-dark, #e6e0e9); } + PhoneNumberText { color: var(--text-color-dark, #e6e0e9); } + PhoneVerificationText { color: var(--text-secondary-color-dark, #cac4d0); } PhoneVerificationError { color: #f2b8b5; } PhoneVerificationLink { color: var(--accent-color-dark, #d0bcff); } @@ -1043,32 +1051,32 @@ TableHeader.disabled { margin: 0; font-size: 2.5mm; color: #49454f; font-family: ErrorLabel.selected { font-family: "native:MainRegular"; color: #f2b8b5; font-size: 1.9mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } ErrorLabel.pressed { font-family: "native:MainRegular"; color: #f2b8b5; font-size: 1.9mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; background-color: transparent; } ErrorLabel.disabled { font-size: 1.9mm; padding: 0.3mm 0.5mm 0 0.5mm; margin: 0; color: #5c5967; background-color: transparent; } - FloatingHint { cn1-derive: SecondaryLabel; color: #cac4d0; font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } - FloatingHint.selected { font-family: "native:MainRegular"; color: #cac4d0; font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } - FloatingHint.pressed { font-family: "native:MainRegular"; color: #cac4d0; font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } + FloatingHint { cn1-derive: SecondaryLabel; color: var(--text-secondary-color-dark, #cac4d0); font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } + FloatingHint.selected { font-family: "native:MainRegular"; color: var(--text-secondary-color-dark, #cac4d0); font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } + FloatingHint.pressed { font-family: "native:MainRegular"; color: var(--text-secondary-color-dark, #cac4d0); font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; background-color: transparent; } FloatingHint.disabled { font-size: 1.9mm; padding: 0 0.5mm 0.3mm 0.5mm; margin: 0; color: #5c5967; background-color: transparent; } - TableCell { cn1-derive: Label; color: #e6e0e9; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } - TableCell.selected { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: #e6e0e9; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } - TableCell.pressed { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: #e6e0e9; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } - TableCell.disabled { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: #e6e0e9; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } - TableHeader { cn1-derive: Label; color: #cac4d0; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } - TableHeader.selected { margin: 0; font-size: 2.5mm; color: #cac4d0; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } - TableHeader.pressed { margin: 0; font-size: 2.5mm; color: #cac4d0; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } - TableHeader.disabled { margin: 0; font-size: 2.5mm; color: #cac4d0; font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } - ToggleButton { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } - ToggleButton.selected { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid var(--accent-color-dark, #d0bcff); padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + TableCell { cn1-derive: Label; color: var(--text-color-dark, #e6e0e9); padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableCell.selected { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: var(--text-color-dark, #e6e0e9); padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableCell.pressed { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: var(--text-color-dark, #e6e0e9); padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableCell.disabled { margin: 0; font-family: "native:MainRegular"; font-size: 2.5mm; color: var(--text-color-dark, #e6e0e9); padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableHeader { cn1-derive: Label; color: var(--text-secondary-color-dark, #cac4d0); font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableHeader.selected { margin: 0; font-size: 2.5mm; color: var(--text-secondary-color-dark, #cac4d0); font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableHeader.pressed { margin: 0; font-size: 2.5mm; color: var(--text-secondary-color-dark, #cac4d0); font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + TableHeader.disabled { margin: 0; font-size: 2.5mm; color: var(--text-secondary-color-dark, #cac4d0); font-family: "native:MainBold"; padding: 1.5mm 2mm 1.5mm 2mm; background-color: transparent; } + ToggleButton { color: var(--text-color-dark, #e6e0e9); background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButton.selected { color: var(--text-color-dark, #e6e0e9); background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid var(--accent-color-dark, #d0bcff); padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } ToggleButton.pressed { color: var(--accent-on-container-color-dark, #eaddff); background-color: var(--accent-container-color-dark, #4f378b); cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } ToggleButton.disabled { color: #5c5967; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #49454f; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } - ToggleButtonFirst { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } - ToggleButtonLast { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } - ToggleButtonOnly { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } - ToggleButtonFirst.selected { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid var(--accent-color-dark, #d0bcff); padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonFirst { color: var(--text-color-dark, #e6e0e9); background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonLast { color: var(--text-color-dark, #e6e0e9); background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonOnly { color: var(--text-color-dark, #e6e0e9); background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonFirst.selected { color: var(--text-color-dark, #e6e0e9); background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid var(--accent-color-dark, #d0bcff); padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } ToggleButtonFirst.pressed { color: var(--accent-on-container-color-dark, #eaddff); background-color: var(--accent-container-color-dark, #4f378b); cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } ToggleButtonFirst.disabled { color: #5c5967; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #49454f; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } - ToggleButtonLast.selected { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid var(--accent-color-dark, #d0bcff); padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonLast.selected { color: var(--text-color-dark, #e6e0e9); background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid var(--accent-color-dark, #d0bcff); padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } ToggleButtonLast.pressed { color: var(--accent-on-container-color-dark, #eaddff); background-color: var(--accent-container-color-dark, #4f378b); cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } ToggleButtonLast.disabled { color: #5c5967; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #49454f; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } - ToggleButtonOnly.selected { color: #e6e0e9; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid var(--accent-color-dark, #d0bcff); padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } + ToggleButtonOnly.selected { color: var(--text-color-dark, #e6e0e9); background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid var(--accent-color-dark, #d0bcff); padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } ToggleButtonOnly.pressed { color: var(--accent-on-container-color-dark, #eaddff); background-color: var(--accent-container-color-dark, #4f378b); cn1-background-type: cn1-pill-border; border: 0.25mm solid #938f99; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } ToggleButtonOnly.disabled { color: #5c5967; background-color: transparent; cn1-background-type: cn1-pill-border; border: 0.25mm solid #49454f; padding: 1.55mm 4mm 1.55mm 4mm; margin: 0.5mm; font-family: "native:MainRegular"; font-size: 2.5mm; text-align: center; } } diff --git a/native-themes/gnome-adwaita/theme.css b/native-themes/gnome-adwaita/theme.css index 42f905708c3..1c01c181b52 100644 --- a/native-themes/gnome-adwaita/theme.css +++ b/native-themes/gnome-adwaita/theme.css @@ -47,6 +47,8 @@ */ #Constants { + /* Marks bundled defaults eligible for optional OS settings inheritance. */ + nativeThemeDefaultsBool: true; /* libadwaita named colours, light. Overridable at runtime: each var(--name, fallback) bakes the fallback in as the default and emits a @cn1-bind constant, so a port that reads the user's real accent through gtk_style_context_lookup_color can push it in diff --git a/native-themes/ios-modern/common.css b/native-themes/ios-modern/common.css index dd1843950bc..5099165868d 100644 --- a/native-themes/ios-modern/common.css +++ b/native-themes/ios-modern/common.css @@ -64,6 +64,14 @@ */ #Constants { + /* Marks bundled defaults eligible for optional OS settings inheritance. */ + nativeThemeDefaultsBool: true; + --window-bg-color: #f2f2f7; + --window-bg-color-dark: #1c1c1e; + --text-secondary-color: #3c3c43; + --text-secondary-color-dark: #ebebf5; + --text-color: #000000; + --text-color-dark: #ffffff; /* Accent palette overridable at runtime via addThemeProps. The light values feed the default rules; the -dark counterparts feed the corresponding $Dark entries the dark @media block @@ -315,14 +323,14 @@ } Component { - color: #000000; + color: var(--text-color, #000000); background-color: #ffffff; padding: 0; margin: 0; } -Form { background-color: #f2f2f7; padding: 0; margin: 0; } -ContentPane { background-color: #f2f2f7; padding: 0; margin: 0; } +Form { background-color: var(--window-bg-color, #f2f2f7); padding: 0; margin: 0; } +ContentPane { background-color: var(--window-bg-color, #f2f2f7); padding: 0; margin: 0; } /* Container has no surface of its own; specific UIIDs that need a fill (Form, Dialog, Tab, etc.) declare it explicitly. Transparent default means a SpanLabel / FlowLayout / generic wrapper inside a @@ -337,14 +345,14 @@ Container { background-color: transparent; padding: 0; margin: 0; } styled with UIID="Label" so this transparency also fixes the visible block behind dialog body text. */ Label { - color: #000000; + color: var(--text-color, #000000); background-color: transparent; padding: 1mm 2mm 1mm 2mm; margin: 0; font-family: "native:MainRegular"; } -SecondaryLabel { cn1-derive: Label; color: #3c3c43; } +SecondaryLabel { cn1-derive: Label; color: var(--text-secondary-color, #3c3c43); } TertiaryLabel { cn1-derive: Label; color: #8e8e93; } /* SpanLabel + its inner SpanLabelText render transparent so when placed on a translucent surface (Dialog, PopupContent) the @@ -494,7 +502,7 @@ TextField { border-radius: 1.4mm; backdrop-filter: blur(20px); } -TextField.pressed { background-color: #f2f2f7; border: 0.25mm solid rgba(0,0,0,0.05); border-radius: 1.4mm; } +TextField.pressed { background-color: var(--window-bg-color, #f2f2f7); border: 0.25mm solid rgba(0,0,0,0.05); border-radius: 1.4mm; } /* The native disabled field barely changes: same cell, text dims slightly. */ TextField.disabled { color: rgba(0,0,0,0.4); background-color: #ffffff; border: 0.25mm solid rgba(0,0,0,0.03); border-radius: 1.4mm; } @@ -502,7 +510,7 @@ TextField.disabled { color: rgba(0,0,0,0.4); background-color: #ffffff; border: rendered as a bare label. Give it the same contrasting-glass cell chrome as a TextField so it reads as a tappable field. */ Picker { cn1-derive: TextField; color: #000000; text-align: left; } -Picker.pressed { cn1-derive: TextField; background-color: #f2f2f7; } +Picker.pressed { cn1-derive: TextField; background-color: var(--window-bg-color, #f2f2f7); } Picker.disabled { cn1-derive: TextField; color: rgba(0,0,0,0.3); } TextArea { @@ -1007,7 +1015,7 @@ MultiLine4 { Dialog { /* iOS alert surface: an opaque light-grey card (secondary system background) so the card reads clearly, with a rounded corner. */ - background-color: #f2f2f7; + background-color: var(--window-bg-color, #f2f2f7); color: #000000; padding: 0; margin: 0; @@ -1061,8 +1069,8 @@ FloatingActionButton.pressed { background-color: var(--accent-pressed-color, #00 bubbles centre with the muted secondary text colour. The TextArea inside each bubble (ChatBubbleText) inherits via cn1-derive so the text colour matches the bubble's foreground. */ -ChatView { background-color: #f2f2f7; padding: 0; margin: 0; } -ChatViewMessages { background-color: #f2f2f7; padding: 1mm; margin: 0; } +ChatView { background-color: var(--window-bg-color, #f2f2f7); padding: 0; margin: 0; } +ChatViewMessages { background-color: var(--window-bg-color, #f2f2f7); padding: 1mm; margin: 0; } ChatBubbleRow { background-color: transparent; padding: 1mm 2mm; margin: 0; } ChatBubbleUser { background-color: var(--accent-color, #007aff); @@ -1094,7 +1102,7 @@ ChatTypingIndicator { ChatInput { background-color: #ffffff; padding: 1mm 2mm; margin: 0; } ChatInputField { cn1-derive: TextField; - background-color: #f2f2f7; + background-color: var(--window-bg-color, #f2f2f7); padding: 2mm 3mm; margin: 0.5mm; border-radius: 3mm; @@ -1188,12 +1196,12 @@ TableHeader.disabled { margin: 0; color: #6c6c70; font-family: "native:MainBold" Where a state is meant to differ it is declared above and left alone here. */ @media (prefers-color-scheme: dark) { - Component { color: #ffffff; background-color: #000000; } - Form { background-color: #1c1c1e; padding: 0; margin: 0; } - ContentPane { background-color: #1c1c1e; padding: 0; margin: 0; } + Component { color: var(--text-color-dark, #ffffff); background-color: #000000; } + Form { background-color: var(--window-bg-color-dark, #1c1c1e); padding: 0; margin: 0; } + ContentPane { background-color: var(--window-bg-color-dark, #1c1c1e); padding: 0; margin: 0; } - Label { color: #ffffff; background-color: transparent; } - SecondaryLabel { color: #ebebf5; background-color: transparent; } + Label { color: var(--text-color-dark, #ffffff); background-color: transparent; } + SecondaryLabel { color: var(--text-secondary-color-dark, #ebebf5); background-color: transparent; } TertiaryLabel { color: #8e8e93; background-color: transparent; } SpanLabel { color: #ffffff; background-color: transparent; } SpanLabelText { color: #ffffff; background-color: transparent; } @@ -1358,7 +1366,7 @@ TableHeader.disabled { margin: 0; color: #6c6c70; font-family: "native:MainBold" stripes bleed through and obscure the text. DialogBody / Title / ContentPane / CommandArea stay transparent so the Dialog surface is the single colour they all share. */ - Dialog { background-color: #1c1c1e; color: #ffffff; } + Dialog { background-color: var(--window-bg-color-dark, #1c1c1e); color: #ffffff; } DialogBody { background-color: transparent; color: #8e8e93; } DialogTitle { color: #ffffff; background-color: transparent; } DialogContentPane { background-color: transparent; } @@ -1371,14 +1379,14 @@ TableHeader.disabled { margin: 0; color: #6c6c70; font-family: "native:MainBold" FloatingActionButton { color: var(--accent-on-color, #ffffff); background-color: var(--accent-color-dark, #0a84ff); } FloatingActionButton.pressed { background-color: var(--accent-pressed-color-dark, #64b1ff); } - ChatView { background-color: #1c1c1e; } - ChatViewMessages { background-color: #1c1c1e; } + ChatView { background-color: var(--window-bg-color-dark, #1c1c1e); } + ChatViewMessages { background-color: var(--window-bg-color-dark, #1c1c1e); } ChatBubbleUser { background-color: var(--accent-color-dark, #0a84ff); color: #ffffff; } ChatBubbleAssistant { background-color: #2c2c2e; color: #ffffff; } ChatBubbleSystem { color: #8e8e93; } ChatBubbleText { color: #ffffff; background-color: transparent; } ChatTypingIndicator { color: #8e8e93; } - ChatInput { background-color: #1c1c1e; } + ChatInput { background-color: var(--window-bg-color-dark, #1c1c1e); } ChatInputField { background-color: #2c2c2e; color: #ffffff; border-radius: 3mm; } /* Attach (+) and voice (Mic) mirror the dark regular Button (blue glyph on a dark glass pill) -- and must keep the SAME capsule shape as the light diff --git a/native-themes/macos-aqua/theme.css b/native-themes/macos-aqua/theme.css index fcc840fd3b9..064e9942179 100644 --- a/native-themes/macos-aqua/theme.css +++ b/native-themes/macos-aqua/theme.css @@ -45,6 +45,8 @@ */ #Constants { + /* Marks bundled defaults eligible for optional OS settings inheritance. */ + nativeThemeDefaultsBool: true; --accent-color: #007aff; /* NSColor.controlAccentColor, default blue */ --accent-color-dark: #007aff; --accent-fg-color: #ffffff; diff --git a/native-themes/windows-fluent/theme.css b/native-themes/windows-fluent/theme.css index 50c3dbf38fe..6c419e7ff55 100644 --- a/native-themes/windows-fluent/theme.css +++ b/native-themes/windows-fluent/theme.css @@ -49,6 +49,8 @@ */ #Constants { + /* Marks bundled defaults eligible for optional OS settings inheritance. */ + nativeThemeDefaultsBool: true; /* WinUI resource names in brackets. The accent is the one thing a Windows user really does personalise, so it is bound: a port reading UISettings.GetColorValue(Accent) can push the real one in with a single addThemeProps call. #0078D4 is the system default diff --git a/scripts/test_native_theme_settings.py b/scripts/test_native_theme_settings.py new file mode 100644 index 00000000000..499a5cd6027 --- /dev/null +++ b/scripts/test_native_theme_settings.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. +# SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0 +"""Local native-theme bridge checks: AppKit runtime, UIKit and Windows compilation.""" +import pathlib +import shutil +import shlex +import subprocess +import tempfile +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] + + +class NativeThemeSettingsTest(unittest.TestCase): + def test_appkit_snapshot_and_notifications(self): + source = (ROOT / "Ports/iOSPort/nativeSources/IOSNative.m").read_text() + start = source.index("static void cn1NativeThemeDidChange(") + end = source.index("JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isLargerTextEnabled", start) + bridge = source[start:end] + harness = r''' +#import +#include +#include +#define TARGET_OS_OSX 1 +#define TARGET_OS_WATCH 0 +#define TARGET_OS_TV 0 +#define CN1_THREAD_STATE_MULTI_ARG +#define CN1_THREAD_STATE_PASS_ARG +#define CN1_THREAD_GET_STATE_PASS_SINGLE_ARG +#define JAVA_OBJECT id +#define POOL_BEGIN() +#define POOL_END() +static float scaleValue = 2; +static int notifications; +static void com_codename1_impl_ios_IOSImplementation_nativeThemeSettingsChanged__(void) { notifications++; } +static id fromNSString(NSString* value) { return [[value copy] autorelease]; } +BRIDGE +int main(void) { + @autoreleasepool { + NSString* settings = com_codename1_impl_ios_IOSNative_nativeThemeSettings___R_java_lang_String(nil); + assert([settings containsString:@"fontFamily=native:\n"]); + NSString* size = [NSString stringWithFormat:@"fontSize=%g\n", [NSFont systemFontSize] * 2]; + assert([settings containsString:size]); + assert([settings containsString:@"accent-color="]); + assert([settings containsString:@"accent-color-dark="]); + assert([settings containsString:@"text-color="]); + assert([settings containsString:@"text-color-dark="]); + [[NSNotificationCenter defaultCenter] postNotificationName:NSSystemColorsDidChangeNotification object:nil]; + assert(notifications == 1); + // Repeated snapshots must not install duplicate observers. + com_codename1_impl_ios_IOSNative_nativeThemeSettings___R_java_lang_String(nil); + [[NSNotificationCenter defaultCenter] postNotificationName:NSSystemColorsDidChangeNotification object:nil]; + assert(notifications == 2); + puts("AppKit native theme snapshot and notification checks passed"); + } + return 0; +} +'''.replace("BRIDGE", bridge) + with tempfile.TemporaryDirectory(prefix="cn1-theme-settings-") as directory: + path = pathlib.Path(directory) + source_path = path / "settings.m" + source_path.write_text(harness) + executable = path / "settings" + subprocess.run(["xcrun", "clang", "-fblocks", "-framework", "AppKit", + str(source_path), "-o", str(executable)], check=True) + subprocess.run([str(executable)], check=True) + + def test_uikit_bridge_compiles(self): + source = (ROOT / "Ports/iOSPort/nativeSources/IOSNative.m").read_text() + start = source.index("static void cn1NativeThemeDidChange(") + end = source.index("JAVA_BOOLEAN com_codename1_impl_ios_IOSNative_isLargerTextEnabled", start) + prelude = """ +#import +#include +#define CN1_THREAD_STATE_MULTI_ARG +#define CN1_THREAD_STATE_PASS_ARG +#define CN1_THREAD_GET_STATE_PASS_SINGLE_ARG +#define JAVA_OBJECT id +#define POOL_BEGIN() +#define POOL_END() +static float scaleValue = 2; +static void com_codename1_impl_ios_IOSImplementation_nativeThemeSettingsChanged__(void) {} +static id fromNSString(NSString* value) { return value; } +""" + sdk = subprocess.check_output(["xcrun", "--sdk", "iphonesimulator", "--show-sdk-path"], text=True).strip() + with tempfile.TemporaryDirectory(prefix="cn1-theme-uikit-") as directory: + path = pathlib.Path(directory) / "settings.m" + path.write_text(prelude + source[start:end]) + subprocess.run(["xcrun", "clang", "-fblocks", "-fsyntax-only", "-isysroot", sdk, + "-target", "arm64-apple-ios15.0-simulator", str(path)], check=True) + + @unittest.skipUnless(shutil.which("x86_64-w64-mingw32-g++"), "MinGW cross compiler unavailable") + def test_windows_bridge_compiles(self): + source = (ROOT / "Ports/WindowsPort/nativeSources/cn1_windows_winrt.cpp").read_text() + start = source.index("typedef struct CN1Buf") + end = source.index("static void cn1BufAppendChar", start) + helpers = source[start:end] + start = source.index("JAVA_OBJECT com_codename1_impl_windows_WindowsNative_nativeThemeSettings") + end = source.index('} /* extern "C" */', start) + prelude = """ +#include +#include +#include +#include +#include +#include +#include +#include +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +#define JAVA_OBJECT void* +#define CODENAME_ONE_THREAD_STATE void* threadStateData +extern void* newStringFromCString(void*, const char*); +struct { float dpiScale; } cn1Win; +""" + with tempfile.TemporaryDirectory(prefix="cn1-theme-win32-") as directory: + path = pathlib.Path(directory) / "settings.cpp" + path.write_text(prelude + helpers + source[start:end]) + # MinGW 14's generated header defines IReference and + # IReference with the same C++ unsigned-char specialization. + # This bridge uses neither; suppress that duplicate header definition. + subprocess.run(["x86_64-w64-mingw32-g++", "-std=c++17", + "-D____FIReference_1_boolean_INTERFACE_DEFINED__", + "-fsyntax-only", str(path)], check=True) + + def test_gtk_bridge_compiles(self): + if not shutil.which("pkg-config") or subprocess.call( + ["pkg-config", "--exists", "gtk+-3.0"]) != 0: + self.skipTest("GTK3 development headers unavailable") + source = (ROOT / "Ports/LinuxPort/nativeSources/cn1_linux_window.c").read_text() + start = source.index("static void cn1ThemeSettingsNotify(") + prelude = """ +#include +#define JAVA_OBJECT void* +#define CODENAME_ONE_THREAD_STATE void* threadStateData +#define CN1_EVENT_THEME_SETTINGS_CHANGED 24 +extern void* newStringFromCString(void*, const char*); +extern void cn1LinuxPushEvent(int, int, int, int); +extern void cn1LinuxRunOnMainAndWait(void (*fn)(void*), void* arg); +static GtkWidget* cn1Window; +""" + flags = shlex.split(subprocess.check_output(["pkg-config", "--cflags", "gtk+-3.0"], text=True)) + with tempfile.TemporaryDirectory(prefix="cn1-theme-gtk-") as directory: + path = pathlib.Path(directory) / "settings.c" + path.write_text(prelude + source[start:]) + subprocess.run(["cc", "-fsyntax-only", "-Werror=implicit-function-declaration", *flags, str(path)], check=True) + + +if __name__ == "__main__": + unittest.main() From c16112b31833038ca1fe5abd4bdbbdda34dbaaaf Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 30 Sep 2026 16:27:05 +0300 Subject: [PATCH 02/10] Apply Checkstyle formatting to native theme settings --- .../codename1/impl/NativeThemeSettings.java | 8 +++- .../src/com/codename1/ui/plaf/UIManager.java | 48 ++++++++++++++----- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/CodenameOne/src/com/codename1/impl/NativeThemeSettings.java b/CodenameOne/src/com/codename1/impl/NativeThemeSettings.java index cfa07611805..ca1c40cab8a 100644 --- a/CodenameOne/src/com/codename1/impl/NativeThemeSettings.java +++ b/CodenameOne/src/com/codename1/impl/NativeThemeSettings.java @@ -41,8 +41,12 @@ public NativeThemeSettings color(String token, int rgb) { /// Compares only the categories the application opted into. public NativeThemeSettings forInheritance(boolean inheritColors, boolean inheritFonts) { NativeThemeSettings result = new NativeThemeSettings(); - if (inheritColors) { result.colors.putAll(colors); } - if (inheritFonts) { result.font(fontFamily, fontSize); } + if (inheritColors) { + result.colors.putAll(colors); + } + if (inheritFonts) { + result.font(fontFamily, fontSize); + } return result; } diff --git a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java index a09c5f79e65..444fc508d81 100644 --- a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java +++ b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java @@ -185,18 +185,26 @@ public static int getThemeGeneration() { private Boolean nativeSettingsDark; /// Whether native-theme colors inherit available OS settings. Defaults to false. - public boolean isUseNativeColors() { return useNativeColors; } + public boolean isUseNativeColors() { + return useNativeColors; + } /// Opts into OS colors. Call refreshTheme() to apply to an already loaded theme; /// an explicit useNativeColorsBool theme constant takes precedence on rebuild. - public void setUseNativeColors(boolean enabled) { useNativeColors = enabled; } + public void setUseNativeColors(boolean enabled) { + useNativeColors = enabled; + } /// Whether native-theme typography inherits the OS family and normal base size. - public boolean isUseNativeFonts() { return useNativeFonts; } + public boolean isUseNativeFonts() { + return useNativeFonts; + } /// Opts into OS typography, independently of larger-text accessibility scaling. /// Call refreshTheme() to apply; useNativeFontsBool takes precedence on rebuild. - public void setUseNativeFonts(boolean enabled) { useNativeFonts = enabled; } + public void setUseNativeFonts(boolean enabled) { + useNativeFonts = enabled; + } /// Internal EDT entry point for coalesced platform settings notifications. public void refreshNativeThemeSettings() { @@ -253,8 +261,12 @@ private void recordNativeThemeOwnership(Hashtable incoming) { boolean nativeLayer = "true".equals(incoming.get("@nativeThemeDefaultsBool")); if (nativeLayer) { Object base = incoming.get("Label.font"); - if (!(base instanceof Font)) { base = incoming.get("font"); } - if (base instanceof Font) { nativeBaseFontSize = nativeFontSize((Font) base); } + if (!(base instanceof Font)) { + base = incoming.get("font"); + } + if (base instanceof Font) { + nativeBaseFontSize = nativeFontSize((Font) base); + } } for (Object item : incoming.keySet()) { String key = (String) item; @@ -266,7 +278,9 @@ private void recordNativeThemeOwnership(Hashtable incoming) { if (incoming.get(key) instanceof Font) { nativeFontKeys.add(key); } - if (key.endsWith("border")) { nativeBorderKeys.add(key); } + if (key.endsWith("border")) { + nativeBorderKeys.add(key); + } if (incoming.containsKey("@cn1-bind:" + key)) { nativeColorKeys.add(key); } @@ -281,8 +295,12 @@ private void recordNativeThemeOwnership(Hashtable incoming) { private void applyNativeThemeSettings() { Boolean colors = isThemeConstant("useNativeColorsBool"); Boolean fonts = isThemeConstant("useNativeFontsBool"); - if (colors != null) { useNativeColors = colors.booleanValue(); } - if (fonts != null) { useNativeFonts = fonts.booleanValue(); } + if (colors != null) { + useNativeColors = colors.booleanValue(); + } + if (fonts != null) { + useNativeFonts = fonts.booleanValue(); + } if (!useNativeColors && !useNativeFonts && !useLargerTextScale) { return; } @@ -306,14 +324,20 @@ private void applyNativeThemeSettings() { if (useNativeFonts && nativeBaseFontSize > 0 && nativeSettings.getFontFamily() != null) { for (String key : nativeFontKeys) { Object value = themeProps.get(key); - if (!(value instanceof Font)) { continue; } + if (!(value instanceof Font)) { + continue; + } Font original = (Font) value; float size = nativeFontSize(original); - if (size <= 0) { continue; } + if (size <= 0) { + continue; + } try { Font inherited = original.deriveNativeThemeFont(nativeSettings.getFontFamily(), size * nativeSettings.getFontSize() / nativeBaseFontSize); - if (inherited != original) { rememberNativeValue(key, inherited); } + if (inherited != original) { + rememberNativeValue(key, inherited); + } } catch (RuntimeException ex) { Log.e(ex); } From 4599d362efefb6567eed313c9dcb45ad15b8969e Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 30 Sep 2026 17:10:44 +0300 Subject: [PATCH 03/10] Fix native theme inheritance review findings and CI failures --- CodenameOne/src/com/codename1/ui/Display.java | 1 + .../src/com/codename1/ui/plaf/UIManager.java | 12 ++- .../com/codename1/impl/javase/JavaSEPort.java | 90 +++++++++++++------ .../nativeSources/cn1_windows_window.cpp | 2 +- Ports/iOSPort/nativeSources/IOSNative.m | 17 +++- .../src/main/css/guide-snippets-theme.css | 10 +++ .../generated/NativeThemesJava001Snippet.java | 8 ++ docs/developer-guide/Native-Themes.asciidoc | 12 +-- .../ui/plaf/UIManagerNativeSettingsTest.java | 40 +++++++++ .../NativeThemeSettingsSimulationTest.java | 25 ++++++ scripts/test_native_theme_settings.py | 16 +++- 11 files changed, 187 insertions(+), 46 deletions(-) diff --git a/CodenameOne/src/com/codename1/ui/Display.java b/CodenameOne/src/com/codename1/ui/Display.java index 8e49cb4f104..de239447359 100644 --- a/CodenameOne/src/com/codename1/ui/Display.java +++ b/CodenameOne/src/com/codename1/ui/Display.java @@ -1044,6 +1044,7 @@ public void nativeThemeSettingsChanged() { nativeThemeRefreshPending = true; } callSerially(new Runnable() { + @Override public void run() { synchronized (Display.this) { nativeThemeRefreshPending = false; diff --git a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java index 444fc508d81..9d532e9a322 100644 --- a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java +++ b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java @@ -246,7 +246,8 @@ private void revalidateNativeThemeTree(com.codename1.ui.Container container) { private void restoreNativeThemeValues() { for (Map.Entry entry : nativeOriginals.entrySet()) { - if (themeProps.get(entry.getKey()) == nativeDerived.get(entry.getKey())) { + // Ownership is by identity: an equal application replacement must survive. + if (themeProps.get(entry.getKey()) == nativeDerived.get(entry.getKey())) { //NOPMD CompareObjectsWithEquals themeProps.put(entry.getKey(), entry.getValue()); } } @@ -335,7 +336,8 @@ private void applyNativeThemeSettings() { try { Font inherited = original.deriveNativeThemeFont(nativeSettings.getFontFamily(), size * nativeSettings.getFontSize() / nativeBaseFontSize); - if (inherited != original) { + // The original instance signals that the font is not eligible for inheritance. + if (inherited != original) { //NOPMD CompareObjectsWithEquals rememberNativeValue(key, inherited); } } catch (RuntimeException ex) { @@ -2045,6 +2047,10 @@ public void zoomFonts(float factor) { if (factor == 1f) { return; } + // Zoom the bundled originals, then reapply OS and accessibility settings. + // Otherwise zoom replaces the instances tracked by the restoration maps. + restoreLargerTextFonts(); + restoreNativeThemeValues(); for (Map.Entry entry : themeProps.entrySet()) { if (!entry.getKey().endsWith(Style.FONT)) { continue; @@ -2067,6 +2073,8 @@ public void zoomFonts(float factor) { entry.setValue(scaled); } } + applyNativeThemeSettings(); + applyLargerTextScaleToThemeFonts(); Font defFont = defaultStyle.getFont(); if (defFont != null && defFont.isTTFNativeFont()) { Font scaled = scaleFontByFactor(defFont, factor); diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java index 560bbffd318..596022cd096 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java @@ -9652,33 +9652,44 @@ private float zoomLevel() { private JMenu installNativeThemeSettingsMenu() { JMenu menu = new JMenu("OS Theme Settings"); JMenuItem reset = new JMenuItem("Reset simulated settings"); - reset.addActionListener(e -> setSimulatorNativeThemeSettings(null)); + reset.addActionListener(new java.awt.event.ActionListener() { + @Override + public void actionPerformed(java.awt.event.ActionEvent event) { + setSimulatorNativeThemeSettings(null); + } + }); menu.add(reset); JMenuItem accent = new JMenuItem("Accent color..."); - accent.addActionListener(e -> { - java.awt.Color color = javax.swing.JColorChooser.showDialog(canvas, "Simulated OS accent", java.awt.Color.BLUE); - if (color != null) { - com.codename1.impl.NativeThemeSettings settings = simulatorNativeThemeSettings == null - ? new com.codename1.impl.NativeThemeSettings() : simulatorNativeThemeSettings.copy(); - settings.color("accent-color", color.getRGB()).color("accent-color-dark", color.getRGB()); - setSimulatorNativeThemeSettings(settings); + accent.addActionListener(new java.awt.event.ActionListener() { + @Override + public void actionPerformed(java.awt.event.ActionEvent event) { + java.awt.Color color = javax.swing.JColorChooser.showDialog(canvas, "Simulated OS accent", java.awt.Color.BLUE); + if (color != null) { + com.codename1.impl.NativeThemeSettings settings = simulatorNativeThemeSettings == null + ? new com.codename1.impl.NativeThemeSettings() : simulatorNativeThemeSettings.copy(); + settings.color("accent-color", color.getRGB()).color("accent-color-dark", color.getRGB()); + setSimulatorNativeThemeSettings(settings); + } } }); menu.add(accent); JMenuItem font = new JMenuItem("UI font..."); - font.addActionListener(e -> { - String family = javax.swing.JOptionPane.showInputDialog(canvas, "OS UI font family", "Dialog"); - if (family == null || family.trim().length() == 0) { return; } - String size = javax.swing.JOptionPane.showInputDialog(canvas, "Normal size in CN1 pixels", "16"); - if (size == null) { return; } - try { - float pixels = Float.parseFloat(size); - if (!(pixels > 0 && pixels < 10000)) { throw new NumberFormatException(); } - com.codename1.impl.NativeThemeSettings settings = simulatorNativeThemeSettings == null - ? new com.codename1.impl.NativeThemeSettings() : simulatorNativeThemeSettings.copy(); - setSimulatorNativeThemeSettings(settings.font(family.trim(), pixels)); - } catch (NumberFormatException invalid) { - javax.swing.JOptionPane.showMessageDialog(canvas, "Enter a positive font size below 10000."); + font.addActionListener(new java.awt.event.ActionListener() { + @Override + public void actionPerformed(java.awt.event.ActionEvent event) { + String family = javax.swing.JOptionPane.showInputDialog(canvas, "OS UI font family", "Dialog"); + if (family == null || family.trim().length() == 0) { return; } + String size = javax.swing.JOptionPane.showInputDialog(canvas, "Normal size in CN1 pixels", "16"); + if (size == null) { return; } + try { + float pixels = Float.parseFloat(size); + if (!(pixels > 0 && pixels < 10000)) { throw new NumberFormatException(); } + com.codename1.impl.NativeThemeSettings settings = simulatorNativeThemeSettings == null + ? new com.codename1.impl.NativeThemeSettings() : simulatorNativeThemeSettings.copy(); + setSimulatorNativeThemeSettings(settings.font(family.trim(), pixels)); + } catch (NumberFormatException invalid) { + javax.swing.JOptionPane.showMessageDialog(canvas, "Enter a positive font size below 10000."); + } } }); menu.add(font); @@ -10798,7 +10809,7 @@ public com.codename1.impl.NativeThemeSettings getNativeThemeSettings() { if (!nativeThemeDesktopListenersInstalled) { nativeThemeDesktopListenersInstalled = true; final String[] properties = {"win.messagebox.font", "win.itemHighlightColor", - "win.text.textColor", "win.frame.backgroundColor", "gnome.Gtk/FontName"}; + "win.text.textColor", "win.frame.backgroundColor", "gnome.Gtk/FontName", "gnome.Xft/DPI"}; final java.beans.PropertyChangeListener listener = new java.beans.PropertyChangeListener() { public void propertyChange(java.beans.PropertyChangeEvent event) { Display.getInstance().nativeThemeSettingsChanged(); @@ -10812,11 +10823,8 @@ public void run() { } }); } - Object font = toolkit.getDesktopProperty("win.messagebox.font"); - if (font instanceof java.awt.Font) { - java.awt.Font uiFont = (java.awt.Font) font; - result.font(uiFont.getFamily(), uiFont.getSize2D()); - } + readDesktopThemeFont(result, toolkit.getDesktopProperty("win.messagebox.font"), + toolkit.getDesktopProperty("gnome.Gtk/FontName"), toolkit.getDesktopProperty("gnome.Xft/DPI")); String[][] properties = {{"selection-color", "win.itemHighlightColor"}, {"text-color", "win.text.textColor"}, {"window-bg-color", "win.frame.backgroundColor"}}; for (String[] pair : properties) { @@ -10828,6 +10836,34 @@ public void run() { return result; } + // Package visibility allows deterministic tests without a running desktop session. + static void readDesktopThemeFont(com.codename1.impl.NativeThemeSettings result, + Object windowsFont, Object gtkFontName, Object gtkDpi) { + if (windowsFont instanceof java.awt.Font) { + java.awt.Font font = (java.awt.Font) windowsFont; + result.font(font.getFamily(), font.getSize2D()); + } else if (gtkFontName instanceof String) { + // Pango descriptions end in a point size (or an absolute px size). + java.util.regex.Matcher match = java.util.regex.Pattern.compile( + "^(.+?)\\s+(\\d+(?:\\.\\d+)?)(px)?$").matcher(((String) gtkFontName).trim()); + if (!match.matches()) { + return; + } + String family = match.group(1).replaceFirst( + "(?i)(?:\\s+(?:bold|italic|oblique|regular|normal|light|medium|semibold|condensed))+$", ""); + float size = Float.parseFloat(match.group(2)); + if (match.group(3) == null) { + // XSettings publishes DPI as a fixed-point integer with 10 fractional bits. + float dpi = gtkDpi instanceof Number ? ((Number) gtkDpi).floatValue() / 1024f : 96f; + if (!(dpi > 0) || Float.isInfinite(dpi)) { + dpi = 96f; + } + size *= dpi / 72f; + } + result.font(family, size); + } + } + private boolean nativeThemeDesktopListenersInstalled; private com.codename1.impl.NativeThemeSettings simulatorNativeThemeSettings; diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp index 683674d1339..a67850d9586 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp @@ -684,7 +684,7 @@ LRESULT CALLBACK cn1WinWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam case WM_SYSCOLORCHANGE: case WM_DWMCOLORIZATIONCOLORCHANGED: cn1WinPushEvent(CN1_EVENT_THEME_SETTINGS_CHANGED, 0, 0, 0); - break; + return DefWindowProcW(hwnd, msg, wParam, lParam); case WM_GETOBJECT: return cn1WinAccessibilityObject(hwnd, wParam, lParam); case WM_LBUTTONDOWN: diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index 8e955f9882b..b818dd57089 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -8563,7 +8563,7 @@ JAVA_OBJECT com_codename1_impl_ios_IOSNative_nativeThemeSettings___R_java_lang_S if (@available(macOS 10.14, *)) { for (int dark = 0; dark < 2; ++dark) { NSAppearance* appearance = [NSAppearance appearanceNamed:dark ? NSAppearanceNameDarkAqua : NSAppearanceNameAqua]; - [appearance performAsCurrentDrawingAppearance:^{ + void (^readColors)(void) = ^{ NSArray* tokens = @[@"accent-color", @"selection-color", @"window-bg-color", @"control-bg-color", @"view-bg-color", @"text-color", @"text-secondary-color", @"separator-color"]; NSArray* colors = @[[NSColor controlAccentColor], [NSColor selectedTextBackgroundColor], @@ -8580,7 +8580,20 @@ JAVA_OBJECT com_codename1_impl_ios_IOSNative_nativeThemeSettings___R_java_lang_S unsigned b = (unsigned)lround(255 * (color.blueComponent*a + background.blueComponent*(1-a))); [values appendFormat:@"%@%@=%02x%02x%02x\n", tokens[i], dark ? @"-dark" : @"", r, g, b]; } - }]; + }; + if (@available(macOS 11.0, *)) { + [appearance performAsCurrentDrawingAppearance:readColors]; + } else { + // Catalina supports semantic colors but not the block appearance API. + NSAppearance* previous = [[NSAppearance currentAppearance] retain]; + @try { + [NSAppearance setCurrentAppearance:appearance]; + readColors(); + } @finally { + [NSAppearance setCurrentAppearance:previous]; + [previous release]; + } + } } } #elif !TARGET_OS_WATCH && !TARGET_OS_TV diff --git a/docs/demos/common/src/main/css/guide-snippets-theme.css b/docs/demos/common/src/main/css/guide-snippets-theme.css index 066d1f1ceec..2123109f569 100644 --- a/docs/demos/common/src/main/css/guide-snippets-theme.css +++ b/docs/demos/common/src/main/css/guide-snippets-theme.css @@ -840,3 +840,13 @@ Button.hover { background-color: #f6f6f6; } /* end::native-themes-css-hover[] */ + +/* tag::native-theme-os-settings[] */ +#Constants { + includeNativeBool: true; + useNativeColorsBool: true; + useNativeFontsBool: true; + useLargerTextScaleBool: true; +} + +/* end::native-theme-os-settings[] */ diff --git a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/NativeThemesJava001Snippet.java b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/NativeThemesJava001Snippet.java index 5c7547dbb5f..1591ebe2669 100644 --- a/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/NativeThemesJava001Snippet.java +++ b/docs/demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/NativeThemesJava001Snippet.java @@ -107,4 +107,12 @@ void snippet() throws Exception { } // end::native-themes-java-001[] } + void inheritNativeThemeSettings() { + // tag::native-theme-os-settings[] + UIManager ui = UIManager.getInstance(); + ui.setUseNativeColors(true); + ui.setUseNativeFonts(true); + ui.setUseLargerTextScale(true); + // end::native-theme-os-settings[] + } } diff --git a/docs/developer-guide/Native-Themes.asciidoc b/docs/developer-guide/Native-Themes.asciidoc index 4939af8c843..50e6b3b38f7 100644 --- a/docs/developer-guide/Native-Themes.asciidoc +++ b/docs/developer-guide/Native-Themes.asciidoc @@ -18,22 +18,14 @@ an application theme that includes the native theme: [source,css] ---- -#Constants { - includeNativeBool: true; - useNativeColorsBool: true; - useNativeFontsBool: true; - useLargerTextScaleBool: true; -} +include::../demos/common/src/main/css/guide-snippets-theme.css[tag=native-theme-os-settings,indent=0] ---- Alternatively, configure the `UIManager` before loading the application theme: [source,java] ---- -UIManager ui = UIManager.getInstance(); -ui.setUseNativeColors(true); -ui.setUseNativeFonts(true); -ui.setUseLargerTextScale(true); +include::../demos/common/src/main/java/com/codenameone/developerguide/snippets/generated/NativeThemesJava001Snippet.java[tag=native-theme-os-settings,indent=0] ---- An explicit theme constant takes precedence over its Java setter when the theme diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/plaf/UIManagerNativeSettingsTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/plaf/UIManagerNativeSettingsTest.java index 21eb80a67ca..b48eb3c9219 100644 --- a/maven/core-unittests/src/test/java/com/codename1/ui/plaf/UIManagerNativeSettingsTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/ui/plaf/UIManagerNativeSettingsTest.java @@ -106,6 +106,46 @@ void refreshAndDisablingRestoreOriginalsWithoutCompounding() { assertEquals(16, size("Button")); } + @Test + void zoomRetainsNativeFontRestoration() { + assertZoomRetainsNativeFontRestoration(1f); + } + + @Test + void zoomRetainsNativeFontRestorationWithAccessibilityScaling() { + assertZoomRetainsNativeFontRestoration(1.5f); + } + + private void assertZoomRetainsNativeFontRestoration(float accessibilityScale) { + manager.setUseNativeFonts(true); + manager.setUseLargerTextScale(true); + implementation.setLargerTextEnabled(true); + implementation.setLargerTextScale(accessibilityScale); + manager.setThemeProps(nativeTheme()); + Hashtable app = new Hashtable(); + app.put("App.font", Font.createTrueTypeFont(Font.NATIVE_MAIN_REGULAR).derive(19f, Font.STYLE_PLAIN)); + manager.addThemeProps(app); + manager.zoomFonts(2f); + assertEquals(48f * accessibilityScale, size("Button")); + assertEquals(96f * accessibilityScale, size("Heading")); + assertEquals(38f * accessibilityScale, size("App")); + for (int i = 0; i < 2; i++) { + manager.refreshTheme(); + assertEquals(48f * accessibilityScale, size("Button")); + } + implementation.nativeThemeSettings = new NativeThemeSettings().font("native:", 20); + manager.refreshNativeThemeSettings(); + assertEquals(40f * accessibilityScale, size("Button")); + assertEquals(80f * accessibilityScale, size("Heading")); + manager.setUseNativeFonts(false); + manager.refreshTheme(); + assertEquals(32f * accessibilityScale, size("Button")); + manager.setUseLargerTextScale(false); + manager.refreshTheme(); + assertEquals(32f, size("Button")); + assertEquals(38f, size("App")); + } + @Test void appLiteralsAndFontsWinAcrossSettingsChanges() { manager.setUseNativeColors(true); diff --git a/maven/javase/src/test/java/com/codename1/impl/javase/NativeThemeSettingsSimulationTest.java b/maven/javase/src/test/java/com/codename1/impl/javase/NativeThemeSettingsSimulationTest.java index 64b7affda9f..fe36b08d938 100644 --- a/maven/javase/src/test/java/com/codename1/impl/javase/NativeThemeSettingsSimulationTest.java +++ b/maven/javase/src/test/java/com/codename1/impl/javase/NativeThemeSettingsSimulationTest.java @@ -57,4 +57,29 @@ void selectedUiFontPreservesRequestedSizeAndStyle() { assertTrue(font.isBold()); assertTrue(font.isItalic()); } + @Test + void gnomeFontUsesFamilyPointSizeAndDesktopDpi() { + NativeThemeSettings settings = new NativeThemeSettings(); + JavaSEPort.readDesktopThemeFont(settings, null, "Noto Sans Bold Italic 10.5", 144 * 1024); + assertEquals("Noto Sans", settings.getFontFamily()); + assertEquals(21f, settings.getFontSize()); + JavaSEPort.readDesktopThemeFont(settings, null, "DejaVu Sans 12", null); + assertEquals("DejaVu Sans", settings.getFontFamily()); + assertEquals(16f, settings.getFontSize()); + JavaSEPort.readDesktopThemeFont(settings, null, "Noto Sans 17px", 144 * 1024); + assertEquals(17f, settings.getFontSize()); + } + + @Test + void desktopFontFallbackIgnoresMalformedValuesAndPrefersWindowsFont() { + for (Object value : new Object[] {null, 12, "Sans", "Sans -2", "Sans NaN", "Sans 0"}) { + NativeThemeSettings settings = new NativeThemeSettings(); + JavaSEPort.readDesktopThemeFont(settings, null, value, null); + assertNull(settings.getFontFamily()); + } + NativeThemeSettings settings = new NativeThemeSettings(); + JavaSEPort.readDesktopThemeFont(settings, new java.awt.Font("Dialog", 0, 18), "Sans 12", null); + assertEquals("Dialog", settings.getFontFamily()); + assertEquals(18f, settings.getFontSize()); + } } diff --git a/scripts/test_native_theme_settings.py b/scripts/test_native_theme_settings.py index 499a5cd6027..e217702f5a5 100644 --- a/scripts/test_native_theme_settings.py +++ b/scripts/test_native_theme_settings.py @@ -13,6 +13,7 @@ class NativeThemeSettingsTest(unittest.TestCase): + @unittest.skipUnless(shutil.which("xcrun"), "Apple toolchain unavailable") def test_appkit_snapshot_and_notifications(self): source = (ROOT / "Ports/iOSPort/nativeSources/IOSNative.m").read_text() start = source.index("static void cn1NativeThemeDidChange(") @@ -38,7 +39,10 @@ def test_appkit_snapshot_and_notifications(self): BRIDGE int main(void) { @autoreleasepool { + NSAppearance* previous = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; + [NSAppearance setCurrentAppearance:previous]; NSString* settings = com_codename1_impl_ios_IOSNative_nativeThemeSettings___R_java_lang_String(nil); + assert([NSAppearance currentAppearance] == previous); assert([settings containsString:@"fontFamily=native:\n"]); NSString* size = [NSString stringWithFormat:@"fontSize=%g\n", [NSFont systemFontSize] * 2]; assert([settings containsString:size]); @@ -60,12 +64,16 @@ def test_appkit_snapshot_and_notifications(self): with tempfile.TemporaryDirectory(prefix="cn1-theme-settings-") as directory: path = pathlib.Path(directory) source_path = path / "settings.m" - source_path.write_text(harness) executable = path / "settings" - subprocess.run(["xcrun", "clang", "-fblocks", "-framework", "AppKit", - str(source_path), "-o", str(executable)], check=True) - subprocess.run([str(executable)], check=True) + # Force the Catalina fallback on the current host as well as checking availability at compile time. + for variant in (harness, harness.replace("if (@available(macOS 11.0, *))", "if (@available(macOS 99.0, *))")): + source_path.write_text(variant) + subprocess.run(["xcrun", "clang", "-fblocks", "-framework", "AppKit", + "-mmacosx-version-min=10.15", "-Werror=unguarded-availability", + str(source_path), "-o", str(executable)], check=True) + subprocess.run([str(executable)], check=True) + @unittest.skipUnless(shutil.which("xcrun"), "Apple toolchain unavailable") def test_uikit_bridge_compiles(self): source = (ROOT / "Ports/iOSPort/nativeSources/IOSNative.m").read_text() start = source.index("static void cn1NativeThemeDidChange(") From 8e4262bc81ed1277e8b8475d3b80bd13edb89b38 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:30:30 +0300 Subject: [PATCH 04/10] Fix overlay theme refresh, desktop appearance and Windows font scaling --- .../src/com/codename1/ui/plaf/UIManager.java | 6 +- .../com/codename1/impl/javase/JavaSEPort.java | 16 ++++- .../nativeSources/cn1_windows_winrt.cpp | 9 ++- docs/developer-guide/Native-Themes.asciidoc | 6 +- .../ui/plaf/UIManagerNativeSettingsTest.java | 53 +++++++++++++++++ .../NativeThemeSettingsSimulationTest.java | 21 +++++++ scripts/test_native_theme_settings.py | 59 +++++++++++++++++++ 7 files changed, 158 insertions(+), 12 deletions(-) diff --git a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java index 9d532e9a322..925941963e0 100644 --- a/CodenameOne/src/com/codename1/ui/plaf/UIManager.java +++ b/CodenameOne/src/com/codename1/ui/plaf/UIManager.java @@ -1977,7 +1977,11 @@ public void addThemeProps(Hashtable themeProps) { programmaticStyleInstalled = false; themeGeneration++; imageCache.clear(); - current.refreshTheme(false); + // Overlay resources merge inside the enclosing build. Refreshing here + // would cache control styles before OS colors/fonts have been applied. + if (buildThemeDepth == 0) { + current.refreshTheme(false); + } } } diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java index 596022cd096..2bd35902345 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java @@ -651,7 +651,7 @@ public Boolean isDarkMode() { private static boolean osDarkModeResolved; private static Boolean osDarkMode; - private static Boolean osDarkMode() { + private static synchronized Boolean osDarkMode() { if (!osDarkModeResolved) { osDarkModeResolved = true; try { @@ -672,6 +672,16 @@ private static Boolean osDarkMode() { return osDarkMode; } + // AWT settings listeners and activation run outside the CN1 EDT. Invalidate + // under the same lock as the query before queuing the EDT theme refresh. + static void desktopThemeSettingsChanged() { + synchronized (JavaSEPort.class) { + osDarkModeResolved = false; + osDarkMode = null; + } + Display.getInstance().nativeThemeSettingsChanged(); + } + /// Runs a short OS query and returns its standard output, or null when it failed or did /// not finish in time. A missing key is a non-zero exit with no output, which the parsers /// treat as the platform's default. @@ -10812,7 +10822,7 @@ public com.codename1.impl.NativeThemeSettings getNativeThemeSettings() { "win.text.textColor", "win.frame.backgroundColor", "gnome.Gtk/FontName", "gnome.Xft/DPI"}; final java.beans.PropertyChangeListener listener = new java.beans.PropertyChangeListener() { public void propertyChange(java.beans.PropertyChangeEvent event) { - Display.getInstance().nativeThemeSettingsChanged(); + desktopThemeSettingsChanged(); } }; for (String property : properties) { toolkit.addPropertyChangeListener(property, listener); } @@ -11623,7 +11633,7 @@ public void windowDeiconified(WindowEvent e) { } public void windowActivated(WindowEvent e) { - Display.getInstance().nativeThemeSettingsChanged(); + desktopThemeSettingsChanged(); mainSurfaceFocusChanged(true); } diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_winrt.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_winrt.cpp index fffff8ea0dd..858808b8cf0 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_winrt.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_winrt.cpp @@ -474,11 +474,10 @@ JAVA_OBJECT com_codename1_impl_windows_WindowsNative_nativeThemeSettings___R_jav char family[256]; int count = WideCharToMultiByte(CP_UTF8, 0, metrics.lfMessageFont.lfFaceName, -1, family, sizeof(family), NULL, NULL); - HDC dc = GetDC(NULL); - int dpi = dc ? GetDeviceCaps(dc, LOGPIXELSY) : 96; - if (dc) ReleaseDC(NULL, dc); - float size = (float)abs(metrics.lfMessageFont.lfHeight) * 96.0f / (dpi > 0 ? dpi : 96) - * (cn1Win.dpiScale > 0 ? cn1Win.dpiScale : 1); + // SPI_GETNONCLIENTMETRICS already returns lfHeight in pixels at the + // caller's DPI, the same coordinate system used by screenDpi(). Do not + // normalize it to 96 DPI: CN1 fonts consume device pixels directly. + float size = (float)abs(metrics.lfMessageFont.lfHeight); if (count > 0 && size > 0) { char line[320]; snprintf(line, sizeof(line), "fontFamily=%s\nfontSize=%g\n", family, size); diff --git a/docs/developer-guide/Native-Themes.asciidoc b/docs/developer-guide/Native-Themes.asciidoc index 50e6b3b38f7..25e50cc2341 100644 --- a/docs/developer-guide/Native-Themes.asciidoc +++ b/docs/developer-guide/Native-Themes.asciidoc @@ -38,11 +38,11 @@ Color inheritance feeds the theme's semantic palette bindings. Font inheritance uses the normal OS UI font family and size while retaining the native theme's relative heading sizes and styles. Explicit application style properties and palette constants override these inherited defaults. Bundled application fonts -and icon fonts are not replaced. Legacy hand-authored native themes do not carry +and icon fonts aren't replaced. Legacy hand-authored native themes don't carry the inheritance metadata and retain their existing appearance. The existing larger-text accessibility option remains independent. It scales -eligible fonts after OS font inheritance, so enabling both does not apply the OS +eligible fonts after OS font inheritance, so enabling both doesn't apply the OS text-size multiplier twice. Existing dark-mode selection remains independent too. Settings changes and application activation trigger a coalesced theme refresh. @@ -68,7 +68,7 @@ settings rather than the host's palette. In the simulator, use *Simulate > OS Theme Settings* to choose an accent and UI font or reset the simulated settings. These controls exercise the app's opt-ins; -they do not enable inheritance for the app. Tests can supply a deterministic +they don't enable inheritance for the app. Tests can supply a deterministic snapshot with `JavaSEPort.setSimulatorNativeThemeSettings(...)`. === Selecting a theme diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/plaf/UIManagerNativeSettingsTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/plaf/UIManagerNativeSettingsTest.java index b48eb3c9219..cbd793029f5 100644 --- a/maven/core-unittests/src/test/java/com/codename1/ui/plaf/UIManagerNativeSettingsTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/ui/plaf/UIManagerNativeSettingsTest.java @@ -146,6 +146,59 @@ private void assertZoomRetainsNativeFontRestoration(float accessibilityScale) { assertEquals(38f, size("App")); } + @Test + void overlayRefreshUsesInheritedControlStylesOnInitialLoad() throws Exception { + assertOverlayRefreshUsesInheritedControlStyles(false); + } + + @Test + void overlayRefreshUsesInheritedControlStylesWhenAddingTheme() throws Exception { + assertOverlayRefreshUsesInheritedControlStyles(true); + } + + private void assertOverlayRefreshUsesInheritedControlStyles(boolean add) throws Exception { + manager.setUseNativeColors(true); + manager.setUseNativeFonts(true); + Hashtable overlay = new Hashtable(); + overlay.put("@overlayMarker", "loaded"); + com.codename1.ui.util.EditableResources resources = new com.codename1.ui.util.EditableResources(); + resources.setTheme("overlay", overlay); + java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream(); + resources.save(bytes); + String resourceName = "native-settings-overlay-" + add; + implementation.putResource("/" + resourceName + ".res", + new java.io.ByteArrayInputStream(bytes.toByteArray())); + Hashtable theme = nativeTheme(); + theme.put("CheckBox.font", theme.get("Button.font")); + theme.put("CheckBox.fgColor", "112233"); + theme.put("@cn1-bind:CheckBox.fgColor", "accent-color"); + theme.put("@OverlayThemes", resourceName); + final java.util.List