Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 81 additions & 23 deletions CodenameOne/src/com/codename1/ui/css/CSSThemeCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ public void compile(String css, MutableResource resources, String themeName) {
theme = new Hashtable();
}

compileConstants(css, theme);
Rule[] rules = parseRules(css);
String strippedCss = stripComments(css);
compileConstants(strippedCss, theme);
Rule[] rules = parseRulesWithMedia(strippedCss);
for (Rule rule : rules) {
applyRule(theme, resources, rule);
}
Expand All @@ -81,20 +82,19 @@ private void resolveThemeConstantVars(Hashtable theme) {
}

private void compileConstants(String css, Hashtable theme) {
String stripped = stripComments(css);
int constantsStart = stripped.indexOf("@constants");
int constantsStart = css.indexOf("@constants");
if (constantsStart < 0) {
return;
}
int open = stripped.indexOf('{', constantsStart);
int open = css.indexOf('{', constantsStart);
if (open < 0) {
return;
}
int close = stripped.indexOf('}', open + 1);
int close = css.indexOf('}', open + 1);
if (close <= open) {
throw new CSSSyntaxException("Unterminated @constants block");
}
Declaration[] declarations = parseDeclarations(stripped.substring(open + 1, close));
Declaration[] declarations = parseDeclarations(css.substring(open + 1, close));
for (Declaration declaration : declarations) {
theme.put("@" + declaration.property, declaration.value);
}
Expand Down Expand Up @@ -500,39 +500,49 @@ private String scalar(String value) {
return out;
}

private Rule[] parseRules(String css) {
String stripped = stripComments(css);
private Rule[] parseRulesWithMedia(String css) {
ArrayList<Rule> out = new ArrayList<Rule>();
parseRulesInto(css, out, false);
return out.toArray(new Rule[out.size()]);
}

private void parseRulesInto(String css, ArrayList<Rule> out, boolean darkContext) {
int pos = 0;
while (pos < stripped.length()) {
while (pos < stripped.length() && Character.isWhitespace(stripped.charAt(pos))) {
int len = css.length();
while (pos < len) {
while (pos < len && Character.isWhitespace(css.charAt(pos))) {
pos++;
}
if (pos >= stripped.length()) {
if (pos >= len) {
break;
}
int open = stripped.indexOf('{', pos);
int open = css.indexOf('{', pos);
if (open < 0) {
throw new CSSSyntaxException("Missing '{' in CSS rule near: " + stripped.substring(pos));
throw new CSSSyntaxException("Missing '{' in CSS rule near: " + css.substring(pos));
}
int close = stripped.indexOf('}', open + 1);

String selectors = css.substring(pos, open).trim();
int close = findMatchingBrace(css, open);
if (close < 0) {
throw new CSSSyntaxException("Missing '}' for CSS rule: " + stripped.substring(pos, open).trim());
}
if (stripped.indexOf('{', open + 1) > -1 && stripped.indexOf('{', open + 1) < close) {
throw new CSSSyntaxException("Nested '{' is not supported in CSS block: " + stripped.substring(pos, open).trim());
throw new CSSSyntaxException("Missing '}' for CSS rule: " + selectors);
}

String selectors = stripped.substring(pos, open).trim();
if (selectors.startsWith("@constants")) {
pos = close + 1;
continue;
}
if (selectors.startsWith("@media")) {
String mediaQuery = selectors.substring("@media".length()).trim();
boolean nextDarkContext = darkContext || isDarkModeMediaQuery(mediaQuery);
parseRulesInto(css.substring(open + 1, close), out, nextDarkContext);
pos = close + 1;
continue;
}
if (selectors.length() == 0) {
throw new CSSSyntaxException("Missing selector before '{'");
}

String body = stripped.substring(open + 1, close).trim();
String body = css.substring(open + 1, close).trim();
Declaration[] declarations = parseDeclarations(body);
String[] selectorsList = splitOnChar(selectors, ',');
for (String selectorEntry : selectorsList) {
Expand All @@ -541,14 +551,62 @@ private Rule[] parseRules(String css) {
throw new CSSSyntaxException("Empty selector in selector list: " + selectors);
}
Rule rule = new Rule();
rule.selector = selector;
rule.selector = darkContext ? toDarkSelector(selector) : selector;
rule.declarations = declarations;
out.add(rule);
}

pos = close + 1;
}
return out.toArray(new Rule[out.size()]);
}

private int findMatchingBrace(String css, int openPos) {
int depth = 0;
int len = css.length();
for (int i = openPos; i < len; i++) {
char c = css.charAt(i);
if (c == '{') {
depth++;
} else if (c == '}') {
depth--;
if (depth == 0) {
return i;
}
}
}
return -1;
}

private boolean isDarkModeMediaQuery(String mediaQuery) {
String normalized = mediaQuery == null ? "" : mediaQuery.toLowerCase();
return normalized.indexOf("prefers-color-scheme") > -1 && normalized.indexOf("dark") > -1;
}

private String toDarkSelector(String selector) {
String trimmed = selector == null ? "" : selector.trim();
if (trimmed.length() == 0 || ":root".equals(trimmed)) {
return trimmed;
}
if (trimmed.startsWith("$Dark")) {
return trimmed;
}

int pseudoPos = trimmed.indexOf(':');
int classStatePos = trimmed.indexOf('.');
int statePos = -1;
if (pseudoPos > -1 && classStatePos > -1) {
statePos = Math.min(pseudoPos, classStatePos);
} else if (pseudoPos > -1) {
statePos = pseudoPos;
} else if (classStatePos > -1) {
statePos = classStatePos;
}
String baseSelector = statePos > -1 ? trimmed.substring(0, statePos) : trimmed;
String stateSelector = statePos > -1 ? trimmed.substring(statePos) : "";
if ("*".equals(baseSelector) || baseSelector.length() == 0) {
baseSelector = "Component";
}
return "$Dark" + baseSelector + stateSelector;
}

private String stripComments(String css) {
Expand Down
35 changes: 33 additions & 2 deletions CodenameOne/src/com/codename1/ui/plaf/UIManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -1937,13 +1937,27 @@ Style parseStyle(Resources theme, String id, String prefix, String baseStyle, bo
}

Style createStyle(String id, String prefix, boolean selected) {
return createStyle(id, prefix, selected, true);
}

private Style createStyle(String id, String prefix, boolean selected, boolean allowDarkStyle) {
Style style;
String originalId = id;
if (prefix != null && prefix.length() > 0) {
id += prefix;
}
boolean useDarkStyle = allowDarkStyle && shouldUseDarkStyle(id);
if (useDarkStyle) {
id = "$Dark" + id;
}

String baseStyle = (String) themeProps.get(id + "derive");
if (baseStyle != null) {
if (baseStyle == null && useDarkStyle) {
style = new Style(createStyle(originalId, prefix, selected, false));
} else {
style = null;
}
if (style == null && baseStyle != null) {
if (baseStyle.indexOf('.') > -1 && baseStyle.indexOf('#') < 0) {
baseStyle += "#";
}
Expand All @@ -1964,7 +1978,7 @@ Style createStyle(String id, String prefix, boolean selected) {
style = new Style(defaultStyle);
}
}
} else {
} else if (style == null) {
if (selected) {
style = new Style(defaultSelectedStyle);
} else {
Expand Down Expand Up @@ -2128,6 +2142,23 @@ Style createStyle(String id, String prefix, boolean selected) {
return style;
}

private boolean shouldUseDarkStyle(String id) {
if (themeProps == null || id == null || id.length() == 0 || id.startsWith("$Dark")) {
return false;
}
Boolean darkMode = CN.isDarkMode();
return darkMode != null && darkMode.booleanValue() && hasStyleDefinition("$Dark" + id);
}

private boolean hasStyleDefinition(String styleId) {
for (String key : themeProps.keySet()) {
if (key.startsWith(styleId)) {
return true;
}
}
return false;
}

/// This method is used to parse the margin and the padding
///
/// #### Parameters
Expand Down
113 changes: 113 additions & 0 deletions CodenameOneDesigner/src/com/codename1/designer/css/CSSTheme.java
Original file line number Diff line number Diff line change
Expand Up @@ -6941,6 +6941,117 @@ private static List<String> getMediaPrefixes(SACMediaList l) {
}
return out;
}

private static String transformDarkModeMediaQueries(String css) {
StringBuilder out = new StringBuilder();
int len = css.length();
int pos = 0;
while (pos < len) {
int mediaPos = css.indexOf("@media", pos);
if (mediaPos < 0) {
out.append(css.substring(pos));
break;
}
out.append(css.substring(pos, mediaPos));
int open = css.indexOf('{', mediaPos);
if (open < 0) {
out.append(css.substring(mediaPos));
break;
}
String query = css.substring(mediaPos + "@media".length(), open).trim().toLowerCase();
int close = findMatchingBrace(css, open);
if (close < 0) {
out.append(css.substring(mediaPos));
break;
}
String block = css.substring(open + 1, close);
if (query.indexOf("prefers-color-scheme") > -1 && query.indexOf("dark") > -1) {
out.append(prefixSelectorsWithDark(block));
} else {
out.append(css.substring(mediaPos, close + 1));
}
pos = close + 1;
}
return out.toString();
}

private static String prefixSelectorsWithDark(String block) {
StringBuilder out = new StringBuilder();
int len = block.length();
int pos = 0;
while (pos < len) {
while (pos < len && Character.isWhitespace(block.charAt(pos))) {
out.append(block.charAt(pos));
pos++;
}
if (pos >= len) {
break;
}
int open = block.indexOf('{', pos);
if (open < 0) {
out.append(block.substring(pos));
break;
}
int close = findMatchingBrace(block, open);
if (close < 0) {
out.append(block.substring(pos));
break;
}
String selectors = block.substring(pos, open);
out.append(toDarkSelectors(selectors)).append('{').append(block.substring(open + 1, close)).append('}');
pos = close + 1;
}
return out.toString();
}

private static String toDarkSelectors(String selectors) {
StringBuilder out = new StringBuilder();
String[] parts = selectors.split(",");
for (int i = 0; i < parts.length; i++) {
if (i > 0) {
out.append(',');
}
String selector = parts[i].trim();
if (selector.length() == 0 || selector.startsWith("$Dark")) {
out.append(parts[i]);
continue;
}
int pseudoPos = selector.indexOf(':');
int classPos = selector.indexOf('.');
int statePos = -1;
if (pseudoPos > -1 && classPos > -1) {
statePos = Math.min(pseudoPos, classPos);
} else if (pseudoPos > -1) {
statePos = pseudoPos;
} else if (classPos > -1) {
statePos = classPos;
}
String base = statePos > -1 ? selector.substring(0, statePos) : selector;
String suffix = statePos > -1 ? selector.substring(statePos) : "";
if ("*".equals(base) || base.length() == 0) {
base = "Component";
}
out.append("$Dark").append(base).append(suffix);
}
return out.toString();
}

private static int findMatchingBrace(String css, int openPos) {
int depth = 0;
int len = css.length();
for (int i = openPos; i < len; i++) {
char c = css.charAt(i);
if (c == '{') {
depth++;
} else if (c == '}') {
depth--;
if (depth == 0) {
return i;
}
}
}
return -1;
}

public static CSSTheme load(URL uri) throws IOException {
try {
Expand All @@ -6949,6 +7060,8 @@ public static CSSTheme load(URL uri) throws IOException {
InputStream stream = uri.openStream();
String stringContents = Util.readToString(stream);

stringContents = transformDarkModeMediaQueries(stringContents);

// The flute parser chokes on properties beginning with -- so we need to replace these with cn1 prefix
// for CSS variable support.
stringContents = stringContents.replaceAll("([\\(\\W])(--[a-zA-Z0-9\\-]+)", "$1cn1$2");
Expand Down
Loading
Loading