Native theme accents: runtime var() bindings via addThemeProps#4884
Open
shai-almog wants to merge 5 commits intomasterfrom
Open
Native theme accents: runtime var() bindings via addThemeProps#4884shai-almog wants to merge 5 commits intomasterfrom
shai-almog wants to merge 5 commits intomasterfrom
Conversation
…ntime
CSS rules in the iOS Modern and Android Material 3 native themes
reference an accent palette via var(--accent-color, fallback). The
Flute compiler still inlines the fallback as the baked-in default
AND additionally emits a @cn1-bind:<UIID>.<key>=accent-color
constant alongside, so the .res file remembers which style keys
track which palette variable.
UIManager.buildTheme() gains an applyThemeBindings() pass that
overlays @<varname> overrides supplied via addThemeProps onto every
bound theme key. A user app rebrands the accent with a single
addThemeProps({"@accent-color": "ff2d95", ...}) call - no per-UIID
rule duplication, no theme recompile.
Replaces the compile-time-only var() approach reverted in #4877
(PR #4848). The same accent vocabulary works at runtime now and
the docs no longer suggest forking the shipped native theme.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks:
Unused image preview:
|
Contributor
Cloudflare Preview
|
Contributor
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
Collaborator
Author
|
Compared 90 screenshots: 90 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
Collaborator
Author
|
Compared 85 screenshots: 85 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
Collaborator
Author
|
Compared 90 screenshots: 90 matched. Benchmark Results
Build and Run Timing
Detailed Performance Metrics
|
The old per-UIID override and the new @accent-color override happen to map to the same set of visible widgets on the iOS Modern capture form (Button.fgColor, RaisedButton.bg/fg) - both produce the same magenta there, so the iOS pipeline shows zero unmatched screenshots which masks whether the new binding mechanism actually fires on iOS. Add a vivid teal override on @accent-disabled-color (iOS-only - the M3 theme hard-codes its disabled colours and has no binding for this slot) so the disabled RaisedButton on the form switches from the default iOS accent-disabled blue to teal. iOS captures now diverge from the pre-binding baseline, confirming the runtime binding pass fires on iOS too. Android's diff is already covered by the magenta @accent-container-color retuning RaisedButton's tonal fill. Add a sanity log at install time that surfaces any leak from a previous test in the suite (a stale @accent-color constant). The test runs near the tail of Cn1ssDeviceRunner and finish() reloads /theme via initFirstTheme which clears themeConstants - so the expected pre-state is "no leak". The log is the cheap signal we need if a future framework regression ever drops that cleanup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Android Material 3's RaisedButton (and other UIIDs using cn1-pill-border) wraps its fill in a RoundBorder whose color the CSS compiler bakes in at compile time. By default RoundBorder paints from that baked field via fillShape() with uiid=false, ignoring Style.bgColor at render time. The runtime binding pass updates themeProps[<UIID>.bgColor] correctly when the user pushes an @accent-color override, but the visible pill stays at the compile-time fallback because the border, not the Style, owns the visible color. When the source background-color came from a var() expansion (i.e. the binding mechanism wants this fill to be runtime-tunable), flip the RoundBorder into uiid mode so it routes through Style.getBgPainter() at paint time. Style.bgColor then drives the fill, and a runtime @accent-* override propagates all the way to the visible pixels. Legacy themes whose backgrounds are inlined hex (no var()) keep the existing baked-color path, so this is a no-op for everything that isn't already opted into the binding mechanism. Update iOS PaletteOverrideTheme_light/dark goldens (both GL and Metal) to the captures produced by the previously-pushed override-color expansion - iOS uses border-radius (RoundRectBorder) which already respects Style.bgColor, so its captures only changed because we added @accent-disabled-color to the override and the disabled RaisedButton on the form is now teal instead of accent-disabled blue. Android goldens will need a fresh CI run with this fix to capture the now-correct magenta RaisedButton; deferring those. NativeThemeBindingsTest: extended to cover the AndroidMaterialTheme .res so the binding round-trip is exercised on both shipped native-theme palettes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…constants
Lets a user app's theme.css override a native-theme palette variable
purely from CSS:
#Constants {
includeNativeBool: true;
--accent-color: #ff2d95;
--accent-color-dark: #ff2d95;
}
Previously `--name` declarations short-circuited into the parser-
internal variables map (used only for compile-time var() resolution
within the same compilation unit) and never reached the runtime, so
a user's --accent-color redeclaration was silently dropped. The
Flute compiler now ALSO emits these as @name theme constants when
they sit inside a #Constants pseudo-element, which routes them
through UIManager.themeConstants where the binding-overlay pass
already knows what to do with them - the user theme.css is loaded
after the native theme (via includeNativeBool=true), the @-constant
overwrites the native default, applyThemeBindings retunes every
bound UIID. Same end-state as runtime addThemeProps but driven from
CSS, no Java code, no Hashtable.
Adds SAC_RGBCOLOR / SAC_FUNCTION (rgb, rgba) handling to the
constants-serialization loop so hex / rgb() colors in #Constants
make it out as plain hex strings (the format runtime themeProps and
applyThemeBindings expect for color values).
Native theme captures still emit their own @accent-color etc. from
their #Constants blocks - this is by design: the constants are
already in themeProps with the native default, so a no-op overlay
runs after each native-theme-load. When the user theme then loads
on top, the user's @accent-color overwrites the native default and
the next applyThemeBindings overlays the user's value.
NativeThemeBindingsTest now also asserts @accent-color is present
in the loaded theme so the round-trip CSS -> .res -> Hashtable is
covered for both shipped native themes.
Native-Themes docs lead with the CSS-from-theme.css path; the
runtime addThemeProps path is documented as the dynamic-theming
counterpart for cases like in-app accent toggles. Test docstring
clarifies it's exercising the runtime path because screenshot
tests can't easily mutate the app's compiled theme.css.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…literal
Element.resolveBinding walked the parent chain whenever the current
Element had no `bindings` entry for the requested property, but
`bindings` only records properties whose VALUE came from a var() -
not properties the rule set with a literal. So a state rule like
`Button.disabled { background-color: #e0dce4 }` (a literal that
deliberately breaks the inheritance from base `Button { background-
color: var(--accent-color) }`) was treated by the binding walker as
"no value of its own" and the parent's accent binding was emitted
for `Button.dis#bgColor`. At runtime the @accent-color override
then stomped the disabled tone with the primary colour, visibly
shifting `Button.disabled` away from the M3 baseline.
Fix: in resolveBinding, after the local `bindings` miss, check
whether the current Element's `style` map has an entry for the
property. If yes, this rule overrode the value with a literal;
return null so the override stops at this level. Only walk to the
parent when the Element has no value of its own (the derive-only
case Button-derived RaisedButton relies on, or the implicit
unselected state inheriting from the base UIID).
Caught by Android ButtonTheme_dark/light captures shifting in the
disabled-button band on the latest CI run; the Button.dis#bgColor
binding is now correctly absent from the rebuilt
AndroidMaterialTheme.res.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
var(--accent-color, fallback)syntax in the shipped native theme CSS but additionally emits@cn1-bind:<UIID>.<key>=accent-colorconstants alongside the inlined fallback. The .res ships with every accent-bearing UIID quietly tracking the underlying palette variable.UIManager.buildTheme()gains anapplyThemeBindings()pass that overlays@<varname>constants supplied viaaddThemePropsonto every bound theme key — so a singleaddThemeProps({"@accent-color": "ff2d95", ...})call retunes every UIID at once. Light/dark variants use distinctaccent-color/accent-color-darkconstants; values can be passed as"ff2d95","#FF2D95", or"#f0a"shorthand.#Constantsdeclarations from the reverted Added theme constants for accents #4848 to bothnative-themes/ios-modern/theme.cssandnative-themes/android-material/theme.css. iOS uses 4 vars, Android adds container/on-container/on-color-dark for the M3 token model.docs/developer-guide/Native-Themes.asciidoc) replace the "Forking a theme to rebrand" section with a runtime-override section documenting the@accent-*constant vocabulary per platform.PaletteOverrideThemeScreenshotTestswapped its 12-key per-UIID override for a tighter@-prefixed constant set demonstrating the new path.native-themes/edits so theme.css changes re-run the platform builds.Why this replaces #4848
#4848 also surfaced these constants but did so at CSS-compile time —
var()resolved against the fallback and the native theme had to be forked + recompiled to rebrand. Native themes ship inside the framework build, so forking isn't actually viable for app developers. This PR keeps the same author ergonomics in the CSS source but moves resolution to runtime: the .res carries enough metadata foraddThemePropsto retune every bound UIID without recompiling anything.Test plan
mvn -pl css-compiler installbuilds the CSS compiler with the new binding tracking.scripts/build-native-themes.shregeneratesiOSModernTheme.res/AndroidMaterialTheme.res. Verified@cn1-bind:entries are present in the .res output.mvn -Dtest='*UIManager*,*Theme*,*Style*' test— 46 tests pass.UIManagerThemeBindingsTest(6 tests) covers default fallback, override, hash-prefix and 3-digit shorthand normalization, orphan-binding skip, invalid-color leaving default intact.NativeThemeBindingsTestend-to-end loads the freshly builtiOSModernTheme.resand confirms@accent-colorretunes Button.fgColor.scripts/{ios,android}/screenshots/PaletteOverrideTheme_*.png(and the matchingscreenshots-metal/) will need to be regenerated. The new override touches@accent-container-colortoo, so Android RaisedButton goes magenta where the old test left it at the M3 default tone. iOS captures should be unchanged.🤖 Generated with Claude Code