Native theme accents: runtime var() bindings via addThemeProps#4884
Merged
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 89 screenshots: 89 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>
The Metal screenshot pipeline hit the 30-minute step timeout on the latest PR run. Tracing the device-runner.log: the suite reached TabsTheme_light (~13 min into test execution), captured 111533 PNG bytes + 84124-byte preview chunk stream at preview_quality=6 (still over the 20480-byte preview cap), then went silent for ~18 minutes before the timer killed it. No FATAL / Test-failure markers - just dead air on the logcat replay. The earlier passing Metal run finished ~28 minutes in, so the suite is consistently running right at the wall. Bump the timeout to 45 minutes - matches the build-ios job's own cap and the iOS Metal runner's natural ceiling - so a borderline-slow chunk-stream replay doesn't get conflated with a real Metal port hang. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This reverts commit 7de74c2.
The texture-backdrop test painter (used by Tabs / Dialog screenshot tests in hellocodenameone) was the immediate trigger, but the underlying robustness issue is in the Metal port itself: drawQuad and drawSolidPrimitive called setRenderPipelineState + 3-4 setVertexBytes on EVERY draw, even when consecutive draws shared the same pipeline and matrix snapshot. For one-off fills that's fine. For burst patterns -- the painter was issuing 2556 fillRect calls per band x ~50 bands = ~125k fillRects per textured-backdrop frame -- the redundant per-call setVertexBytes of a 192-byte matrix struct plus the redundant pipeline state-set choke the CAMetalLayer command buffer. The TabsTheme dark-mode capture stalled the iOS Metal screenshot suite for 18 minutes (until the surrounding step's wall-clock timer fired). Two complementary fixes: 1. Painter (hellocodenameone DualAppearanceBaseTest.TextureBackdrop Painter): each diagonal stripe is a parallelogram. Replace the 2556-iteration scanline fillRect loop with one fillPolygon call per band. Polygon fill is universally supported by every CN1 port we ship (Graphics.fillPolygon is core API, not a port extension); the previous comment claiming otherwise was wrong. ~125k draws per frame -> ~50. 2. Metal port (CN1Metalcompat.m): track last-bound pipeline state and last-uploaded matrix bytes per encoder. Skip the Metal API call when they haven't changed. Cache invalidates on every activeEncoder reassignment (BeginFrame, BeginMutableImageDraw, EndMutableImageDraw restore, EndFrame) because Metal command encoders don't carry state between encoders. memcmp on a 192-byte struct per draw is much cheaper than the encoder's setVertexBytes argument-buffer copy. This isn't only about TextureBackdropPainter -- it makes Metal robust to any code path that emits a long burst of same-pipeline / same-matrix draws (gradient scanline approximations, RoundRectBorder interior scanline fills, custom painters in user apps that happen to choose scanline strategies for portability). A small CSS or layout mistake should not be able to cascade into 100k+ redundant Metal encoder calls. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…draws" This reverts commit 2cfb34c.
…Rect Each diagonal band is a parallelogram. The previous loop emitted one fillRect per row (h rows) per band; at phone resolution that's ~2500 fillRects x 50 bands = ~125k draw calls per backdrop frame. On iOS Metal that call volume saturated the CAMetalLayer command buffer and stalled the dark-mode transition for TabsTheme by 18 minutes (until the surrounding step's wall-clock timer fired). Replace with one fillPolygon per band - 50 draw calls instead of 125k. Graphics.fillPolygon is core CN1 API, supported on every port we ship; the previous comment claiming otherwise was wrong. This is the painter half of the earlier "Metal port: cache pipeline + matrix state" commit. The Metal-side state caching half is left out of this revert chain because the original CI run with both pieces produced a different failure mode (simctl couldn't launch the rebuilt app -- "Application unknown to FrontBoard") that I couldn't reproduce locally and don't want to chase blind. The painter fix alone reduces the call volume by ~2500x, which is the actual root-cause mitigation; the encoder state cache was a defensive second pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ine fillRect" This reverts commit 668d39e.
drawQuad and drawSolidPrimitive called setRenderPipelineState + 3-4 setVertexBytes on EVERY draw, even when consecutive draws shared the same pipeline and matrix snapshot. For one-off fills that's fine. For burst patterns -- the hellocodenameone TextureBackdropPainter (used by Tabs / Dialog screenshot tests) is the immediate trigger, issuing ~60k fillRects per textured-backdrop frame, and locally instrumented Metal draw counts confirmed ~60,000 draw calls/frame sustained over 25+ frames per TabsTheme capture (~1.5M Metal API calls total). On the GitHub Actions runner this stalls the CAMetalLayer command buffer for 18+ minutes until the surrounding step's wall-clock timer fires. The local M-series hardware tolerates the call volume but at ~40-50ms per frame. Fix: track last-bound pipeline state and last-uploaded matrix bytes per encoder. Skip the Metal API call when they haven't changed. Cache invalidates on every activeEncoder reassignment (BeginFrame, BeginMutab leImageDraw, EndMutableImageDraw restore, EndFrame) because Metal command encoders don't carry state between encoders. memcmp on a 192-byte struct per draw is much cheaper than the encoder's setVertexBytes argument-buffer copy. This isn't only about TextureBackdropPainter -- it makes Metal robust to any code path that emits a long burst of same-pipeline / same-matrix draws (gradient scanline approximations, RoundRectBorder interior scanline fills, custom painters in user apps that happen to choose scanline strategies for portability). A small CSS or layout mistake should not be able to cascade into 100k+ redundant Metal encoder calls. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The painter is invoked once per Form paint cycle. The screenshot test framework's settle window emits ~30 paints at 60Hz before the capture fires; multiplied by ~50 bands x ~2532 rows of fillRect per paint, that's ~3.8M fillRect calls per textured-backdrop capture. Even after Metal port pipeline+matrix state caching skips 99.99% of redundant API calls, the remaining setVertexBytes(positions)/ setVertexBytes(color)/drawPrimitives per fillRect is enough to keep the GitHub Actions runner stalled past the 30-minute screenshot step timeout on TabsThemeScreenshotTest. Render the pattern into an Image once (size keyed on form bounds) and drawImage(cached) on every subsequent paint. First paint: ~125k draw calls; later paints in the same form: 1 drawImage call. Total per capture drops from ~3.8M draws to ~125k. Cross-port safe: Image.createImage + drawImage are core CN1 API that every port implements, unlike the per-band fillPolygon variant that silently dropped on Android (canvas.drawPath behaviour at extreme parallelogram coordinates). The cache is keyed on (width, height) so a rotation/resize re-renders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The painter now renders the diagonal-stripe texture into a cached Image and drawImage()s it on subsequent paints, instead of re-running the per-row scanline fillRects every frame. Visually identical to the previous goldens, but sub-pixel alpha-blend rounding differs (Image alpha-blend vs direct framebuffer alpha-blend produces tiny per-pixel deltas on iOS GL). iOS Metal and Android goldens already accept the new render (Metal: 90/90 matched, Android: 90/90 matched). Only iOS GL strict pixel comparison flagged the four textured tests: DialogTheme_light/dark, TabsTheme_light/dark. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
shai-almog
added a commit
that referenced
this pull request
May 12, 2026
… jar (#4929) * Fix CSSWatcher live reload: drop stale bindings + extract m2 designer jar Two recent CSS/localization changes regressed the simulator's live CSS reload, in different ways. 1. addThemeProps stomped user edits with stale @cn1-bind entries. PR #4884 added applyThemeBindings() inside UIManager.buildTheme so a single addThemeProps({"@accent-color": ...}) override could retune every var()-bound theme key. But CSSWatcher reloads the theme through the same code path -- and addThemeProps never clears themeConstants. When the user replaced a `var()` rule with a literal in their CSS, the recompiled theme.res no longer emitted the matching `@cn1-bind:<key>` entry, but the previous binding was still sitting in themeConstants. applyThemeBindings happily re-overlaid the user's fresh literal value with the stale binding's resolved value, so the visible change disappeared on every reload. Fix: in buildTheme, before iterating the incoming Hashtable, detect any binding whose subject style key the new load is re-setting without re-asserting the binding alongside, and drop those bindings before the overlay pass runs. Pure `@accent-color` overrides keep working because they don't carry style keys, so no bindings are considered stale. 2. MavenUtils.findDesignerJarInM2 returned the unrunnable wrapper zip. PR #4852 added an m2 fallback for the CSSWatcher's designer-jar lookup, used whenever -Dcodename1.designer.jar isn't passed in (e.g. simulator launched from the IDE rather than `mvn cn1:run`). The helper returned `codenameone-designer-<v>-jar-with-dependencies.jar` directly from m2 -- but that artifact is a zip wrapper containing a single inner designer_1.jar (see maven/designer/pom.xml's antrun step), with no top-level Main-Class manifest. `java -jar wrapper.zip` fails with "no main manifest attribute", the CSS subprocess never starts, and the watcher silently waits for ::refresh:: lines that never come. Fix: mirror AbstractCN1Mojo.getDesignerJar's pattern -- unzip the wrapper to an `<artifact>.jar-extracted/` sibling on demand and return the inner designer_1.jar so `java -jar` actually launches. Tests: - UIManagerThemeBindingsTest gains three regression cases: cssReloadDropsStaleBindingWhenRuleBecomesLiteral (the actual reproducer), cssReloadKeepsBindingWhenStillEmittedTogether (guard against an over-eager fix), and overrideOnlyReloadKeepsBindings (repeated `@accent-color` retunes still work). The first fails before the UIManager fix; all three pass after. - MavenUtilsTest is new and covers the wrapper-vs-inner-jar resolution with five cases: happy path, re-use of extracted inner jar when the wrapper hasn't changed, re-extract when the wrapper mtime advances, null when the core jar isn't in an m2 layout, and null when the designer artifact is missing. To make these actually executable, the javase pom now pins maven-surefire-plugin to 3.2.5 (the parent's 2.21.0 doesn't auto-discover JUnit Jupiter). The pre-existing CSSWatcherTest + LocationSimulationTest + JavaSEPortFontMappingTest in the same module also start running as a side effect. - pr.yml gets a new "Run JavaSE port unit tests" step so this whole test class -- which compiled but never executed -- is wired into CI. Without it, regressions in CSSWatcher/MavenUtils/JavaSEPort helpers would continue to slip through, which was the original gap the user flagged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Address PR review: harden Zip Slip + install javase deps in CI step - MavenUtils.extractInnerJar no longer derives a File path from ZipEntry.getName(). CodeQL flagged the previous loop as a Zip Slip risk because a wrapper containing `../../etc/passwd` would have been written outside the extraction directory. The wrapper produced by maven/designer/pom.xml has a single designer_1.jar entry by design, so the extractor now (a) writes only to a single fixed destination path under destDir and (b) only matches entries whose literal name equals "designer_1.jar". Anything else is skipped; if the canonical entry is absent, the method throws. Two new MavenUtilsTest cases: refusesPathTraversalEntriesAndDoesNotWriteOutsideExtractDir packs a `../../escaped.txt` entry and asserts no escaped file appears in the temp root; skipsUnexpectedEntriesAndStillExtractsDesignerJar mixes a README and a subdir/other.jar with the real designer_1.jar and asserts only the inner jar lands on disk. - pr.yml's new "Run JavaSE port unit tests" step failed with "Could not find artifact com.codenameone:sqlite-jdbc:jar:8.0-SNAPSHOT" on all three matrix entries (Java 8/17/21). The earlier "Build Codename One" step builds core-unittests with -am, which doesn't install sqlite-jdbc into the local repo. Split the new step into two mvn invocations: first install javase's transitive deps without running their tests, then run javase's tests in isolation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * UIManager: scope stale-binding drop to addThemeProps, fix iOS hang The previous fix ran the stale-binding preprocessing inside buildTheme, which is also called from the @includeNativeBool layered initial load (setThemePropsImpl -> buildTheme -> Display.installNativeTheme() -> buildTheme(native) -> outer buildTheme(userTheme) continues). After the native theme installs its bindings into themeConstants, the outer call's preprocessing would drop them whenever the user's app theme.css set a literal value for the same UIID -- which the existing iOS / Android screenshot goldens were captured against. The iOS PR check hit this: the device-runner log shows the suite ran fine through ChartCubicLineScreenshotTest and then hung in ChartBarScreenshotTest setup until the 30-minute timeout fired. The inconsistent themeConstants state left over once the layered native bindings were dropped manifests as a hang in chart-component initialization (presumably a Style.derive cycle or similar) rather than as a pixel diff. Move the drop pre-pass out of buildTheme and into a new dropSupersededBindings() called only from addThemeProps. This keeps the CSSWatcher reload fix (the actual reported regression) and the companion regression tests passing, while restoring the original behavior of the layered initial-load path -- bindings declared by the native theme via @includeNativeBool stay live, user-app literals don't silently strip them out. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
shai-almog
added a commit
that referenced
this pull request
May 16, 2026
The Android port's Ant javac uses US-ASCII source encoding and rejects em dashes and degree signs the build-test (8/17) jobs surfaced. Replace em dashes with hyphens and degree signs with "deg" in the comments added by this branch: - CodenameOneImplementation: 3 comments (Conic sweep, CSS conic-gradient axis, blurRegion fallback). - Graphics.fillLinearGradientWithStops / fillConicGradient javadoc. - JavaSEPort blurRegion fallback comment. - theme.css filter:blur section header. - CssGradientsScreenshotTest class javadoc + UIIDs comment. Pre-existing non-ASCII in Cn1ssDeviceRunner (4 lines from PR #4884) is left alone - it lives in hellocodenameone test code which uses UTF-8 javac, not the Android-port Ant build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shai-almog
added a commit
that referenced
this pull request
May 16, 2026
The Android port's Ant javac uses US-ASCII source encoding and rejects em dashes and degree signs the build-test (8/17) jobs surfaced. Replace em dashes with hyphens and degree signs with "deg" in the comments added by this branch: - CodenameOneImplementation: 3 comments (Conic sweep, CSS conic-gradient axis, blurRegion fallback). - Graphics.fillLinearGradientWithStops / fillConicGradient javadoc. - JavaSEPort blurRegion fallback comment. - theme.css filter:blur section header. - CssGradientsScreenshotTest class javadoc + UIIDs comment. Pre-existing non-ASCII in Cn1ssDeviceRunner (4 lines from PR #4884) is left alone - it lives in hellocodenameone test code which uses UTF-8 javac, not the Android-port Ant build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shai-almog
added a commit
that referenced
this pull request
May 19, 2026
…cs (#4957) * Expand CSS gradient + filter:blur support across framework, ports, docs The CSS compiler previously rejected anything beyond two-stop linear gradients at 0/90/180/270 degrees and two-stop radial gradients at the center, falling back to CEF-rasterized images for everything else. filter/backdrop-filter properties were ignored entirely. This change moves the full CSS gradient range and filter:blur into native primitives end-to-end: * New GradientDescriptor (kind, cycle method, multi-stop colors, shape, extent, center, radii, conic from-angle) attached to Style alongside new BACKGROUND_GRADIENT_LINEAR / _RADIAL_FULL / _CONIC / _REPEATING_LINEAR / _REPEATING_RADIAL types, plus filterBlurRadius / backdropFilterBlurRadius fields with accessors. * Graphics + CodenameOneImplementation grow fillLinearGradientWithStops, fillRadialGradientWithStops, fillConicGradient and a blurRegion hook. Software rasterizer in the base impl guarantees correctness on every port. * Resource format bumped to v1.13: new bgGradientEx, filterBlur and backdropFilterBlur theme entries; Resources.java reader and EditableResources writer round-trip the new data (binary + XML). * CSS compiler parses arbitrary angles, multi-stop with optional positions, conic-gradient, repeating-*, full radial syntax (circle/ellipse + four extents), plus filter: blur() and backdrop-filter: blur(); native filter rendering removed from the requiresBackgroundImageGeneration condition. * JavaSE uses Java2D LinearGradientPaint / RadialGradientPaint with cycle methods and AffineTransform for ellipses. Android wires multi-stop LinearGradient / RadialGradient / SweepGradient shaders, with the AndroidAsyncView legacy paint path capturing a defensive descriptor copy. iOS falls back to the software rasterizer (correct output, transforms and clip preserved); CIGaussianBlur already provides image-level filter:blur. * Developer guide (css.asciidoc, graphics.asciidoc, Native-Themes.asciidoc) and the initializr Claude Code skill css reference updated with the new syntax and the filter:blur / backdrop-filter:blur properties. Verified by mvn compile across core, css-compiler, JavaSE, iOS Java side, and Android (with JDK 17) — all clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * CI fixes + hellocodenameone screenshot tests for new gradient/filter APIs CI fixes: * CodenameOneImplementation: java.lang.Math.atan2 isn't in the CN1 core stub (ParparVM ships a Java 5-era subset); the iOS build broke on fillConicGradient's software rasterizer. Switch to MathUtil.atan2. * CSSBorder.RadialGradient.toCSSString() no longer throws, so update CSSBorderTest.testRadialGradient to assert the new behavior (returns a valid radial-gradient(...) string instead of an exception). * CSSTheme.CN1Gradient.parse(): wrap the legacy linear/radial parsers in try/catch so inputs the extended parser can handle (e.g. "to bottom right" - the legacy parser tries to read the second side keyword as a color and throws) fall through to the extended parser cleanly. New screenshot tests (added to Cn1ssDeviceRunner): * graphics/DrawGradientStops - exercises the new low-level Graphics primitives directly (fillLinearGradientWithStops at 45deg/REFLECT, repeating-linear stripes, multi-stop radial circle + ellipse, conic rainbow). Inherits AbstractGraphicsScreenshotTest so each tile is rendered four ways (AA on/off, direct/buffered) - per-port differences in stop interpolation, angle math, and shader matrices surface as pixel diffs. * graphics/GaussianBlur - validates the platform's gaussianBlur(Image, float) primitive used to back filter:blur. Four tiles: unblurred reference, light blur (1.5mm), heavy blur (4mm), and a heavy blur over a gradient-filled source to expose blur-kernel artifacts against high-frequency content. Density-aware radii (CN.convertToPixels) keep the visual blur similar across DPIs. * CssGradientsScreenshotTest - end-to-end CSS gradient test: theme.css declares eight UIIDs covering angled multi-stop linear, "to side1 side2", mismatched-alpha linear, radial farthest-corner, elliptical radial, conic, repeating-linear, repeating-radial. The test asserts each tile carries the expected BACKGROUND_GRADIENT_* type and a non-null GradientDescriptor BEFORE taking the screenshot, so a silent CSS compiler regression (e.g. dropping support for one form) fails explicitly rather than producing a "looks slightly different" image. * CssFilterBlurScreenshotTest - end-to-end filter: blur() and backdrop-filter: blur() test. Four tiles cover no-blur, blur(2px), blur(8px), and backdrop-blur(12px); the test asserts each Style's filterBlurRadius / backdropFilterBlurRadius before screenshotting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * CI: pass surefire.failIfNoSpecifiedTests in designer workflow PR #4929 (May 12) bumped codenameone-javase's surefire to 3.2.5 so its JUnit Jupiter tests would run. Surefire 3.x renamed `failIfNoTests` -> `failIfNoSpecifiedTests`, and designer.yml's reactor build invokes `-Dtest=SimpleXmlParserTest -DfailIfNoTests=false` to suppress "no tests matched" on intermediate modules. The flag was silently ignored by 3.2.5, so the codenameone-javase test phase began failing on the next run. designer.yml's path filter excludes `maven/javase/**`, so the bug didn't surface on master after the surefire bump - it only manifested on a PR that touches the designer workflow's trigger paths (maven/css-compiler/**, maven/designer/**, or CodenameOneDesigner/**). This PR touches css-compiler, so it surfaces here. Pass both flag names so each surefire version finds the one it understands. codenameone-javase (3.2.5) reads `surefire.failIfNoSpecifiedTests`; peer modules still on parent-pom 2.21.0 read `failIfNoTests`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Strip non-ASCII characters from new source files The Android port's Ant javac uses US-ASCII source encoding and rejects em dashes and degree signs the build-test (8/17) jobs surfaced. Replace em dashes with hyphens and degree signs with "deg" in the comments added by this branch: - CodenameOneImplementation: 3 comments (Conic sweep, CSS conic-gradient axis, blurRegion fallback). - Graphics.fillLinearGradientWithStops / fillConicGradient javadoc. - JavaSEPort blurRegion fallback comment. - theme.css filter:blur section header. - CssGradientsScreenshotTest class javadoc + UIIDs comment. Pre-existing non-ASCII in Cn1ssDeviceRunner (4 lines from PR #4884) is left alone - it lives in hellocodenameone test code which uses UTF-8 javac, not the Android-port Ant build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix SpotBugs violations from the gradient/blur additions build-test (8) runs SpotBugs and fails on three new findings: * IM_BAD_CHECK_FOR_ODD in sampleStops's REFLECT cycle: `intp % 2 == 1` silently does the wrong thing for negative ints (the JLS specifies `-1 % 2 == -1`). In this code path `intp` is the absolute-valued floor and is always non-negative, but SpotBugs can't see that. Switch to `(intp & 1) != 0` which is unambiguous and slightly faster. * FE_FLOATING_POINT_EQUALITY in Style.setFilterBlurRadius and setBackdropFilterBlurRadius: `this.field != radius` directly compares floats, which mishandles NaN and -0/+0. Use `Float.compare` so the field/method semantics match the rest of Style (the existing iconGap setter already uses an epsilon-based check via `Math.abs(...) > 1e-4`, but Float.compare is more standard for "should we update"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix PMD violations in GradientDescriptor build-test (8) PMD pass surfaced two cosmetic findings: - UnnecessaryConstructor: drop the explicit zero-arg constructor; the compiler provides one for free with the same visibility. - OneDeclarationPerLine: split `float rx, ry;` into two declarations inside computeRadii. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Refactor: Gradient hierarchy + uniform surefire + Android hang fix Three reviewer-driven changes: 1. Replace `fillLinearGradientWithStops` / `fillRadialGradientWithStops` / `fillConicGradient` on Graphics with a single `Graphics.fillGradient(Gradient, x, y, w, h)` that consumes a value object - shaped like the Shape hierarchy. Three concrete subclasses: * LinearGradient(angleDegrees, colors, positions) * RadialGradient(colors, positions) + shape/extent/center/radius setters * ConicGradient(colors, positions) + fromAngle/center setters `Gradient` is a `Paint` subclass with shared stops, cycle method (NONE / REPEAT / REFLECT) and a `sampleArgb` hook the base impl uses for the software-rasterizer fallback. Ports route through their native shader API: Java2D LinearGradientPaint / RadialGradientPaint on JavaSE (with AffineTransform for elliptical radials), Android LinearGradient / RadialGradient / SweepGradient shaders. iOS still falls back to the software rasterizer. Style.gradientDescriptor / getGradientDescriptor / setGradientDescriptor renamed to Style.gradient / getGradient / setGradient. The .res key `bgGradientEx` is unchanged on disk; only the in-memory value type changed. The deleted `com.codename1.ui.plaf.GradientDescriptor` had no callers outside this branch. 2. Pin maven-surefire-plugin to 3.2.5 uniformly in the parent pom (instead of per-module in maven/javase/pom.xml as PR #4929 did). Revert the dual-flag hack in designer.yml; the single new `surefire.failIfNoSpecifiedTests` flag now suffices everywhere. 3. Fix Android instrumentation suite hang at DrawGradientStops. The previous AndroidImplementation.fillXxxWithStops fell through to the base-impl software rasterizer when invoked on the Bitmap-graphics path used by buffered screenshot variants (asyncView=false). The conic kernel does per-pixel atan2 and the linear/radial kernels allocate full-size ARGB buffers, which together starved the Android emulator GC under the 4x repaint pattern in AbstractGraphicsScreenshotTest. After the refactor AndroidImplementation.fillGradient unconditionally routes to AndroidGraphics.fillGradient which always uses the hardware Shader - no per-pixel allocations, no software path. Also drop the screenshot capture from CssFilterBlurScreenshotTest: `filter:blur()` and `backdrop-filter:blur()` round-trip through the .res into Style fields, but Component.paint doesn't yet consume the radius (that's a follow-up using Graphics.gaussianBlur). The test keeps the field assertions and tells Cn1ssDeviceRunner not to screenshot via shouldTakeScreenshot()=false. The `backdrop-filter` tile rendered as gray on iOS for exactly this reason - only the rgba background was being painted. Verified by full reactor `mvn install -Plocal-dev-javase`, Android `mvn -pl android -am compile` under JDK17, hellocodenameone common compile, and `mvn -pl core-unittests test -Dtest=CSSBorderTest` - all exit 0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Drop unused Gradient-subclass imports in CodenameOneImplementation build-test (8) PMD UnnecessaryImport pass flagged ConicGradient / LinearGradient / RadialGradient as unused: after the refactor the file only references the abstract `Gradient` base in the simplified `fillGradient` software-rasterizer (sampleArgb is dispatched virtually, so the subclass types aren't named here anymore). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix CI failures: AsyncGraphics.fillGradient, simctl, native-iOS sim discovery Three concrete failures, three real fixes: 1. **Android instrumentation hang** at DrawGradientStops: the actual root cause was that AsyncGraphics (the buffered paint replay inside AndroidAsyncView) overrides all the legacy `fillLinearGradient` / `fillRectRadialGradient` / `fillRadialGradient` methods to queue AsyncOps, but my refactor's new `fillGradient(Gradient,...)` was not overridden. AsyncGraphics inherits AndroidGraphics.fillGradient directly, which calls `canvas.save()` -- and on an AsyncGraphics instance the canvas field is null at queue time (it's only set when the op is later executed against a real underlying graphics). Result: NPE on every fillGradient call, caught by the EDT exception handler, retried on the next paint, etc. -- which kept the test form from ever completing onShowCompleted and screenshot capture from ever firing. The instrumentation suite then hung the 10-minute step at DrawGradientStops while polling for the never-arriving `done` flag. Fix: add AsyncGraphics.fillGradient(Gradient,...) override that queues an AsyncOp, captures a defensive Gradient.copy() so async replay sees the descriptor as it was at queue time, and invokes underlying.fillGradient on the real AndroidGraphics during replay. 2. **iOS packaging "Application unknown to FrontBoard" launch failure**: the existing simctl launch retry was 2 attempts with a flat 5s sleep. Xcode 26's FrontBoard registration race regularly takes longer than that. Strengthen the retry to 5 attempts with linear backoff (5/10/ 15/20s), and on the specific "unknown to FrontBoard" failure mode bounce FrontBoard via `simctl spawn launchctl kickstart -k` and reinstall the .app bundle to force the registry to pick it up. 3. **native-ios "Unable to find a device matching iPhone 16"**: `xcodebuild -showdestinations` on the macOS-15 runner sometimes only lists the "Any iOS Simulator Device" placeholder when no concrete simulator has been created yet for the bundled Xcode. The existing script fell back to the literal name "iPhone 16" which then also fails. Add a `simctl list devices available` lookup that picks any existing iPhone simulator UDID, and as a final fallback create a throwaway sim from the latest available iOS runtime + iPhone device type before xcodebuild test runs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add JS-port reference screenshots for DrawGradientStops + GaussianBlur The JS-port screenshot harness compares each test's PNG against a stored baseline under scripts/javascript/screenshots/. My two new graphics tests (DrawGradientStops, GaussianBlur) had no baselines yet, so every CI run reported "Reference screenshot missing" and failed. Captured the actual JS-port output from CI run 25958113514 as the baseline. What the screenshots show: - graphics-draw-gradient-stops.png: 4 blank tiles. The existing JS port doesn't override fillGradient(Gradient,...), so the call routes through the base impl's software rasterizer (createImage(int[], w, h) + drawImage). On the JS port that path currently produces an empty image - a known limitation of the old JS port that the moving-initializr-to- new-js-port branch addresses. Baselining the current behavior lets the test catch any future regression in the empty-output state, and lets the new JS port baseline this once it lands. - graphics-gaussian-blur.png: 3 unblurred tiles + a gradient source. The base impl's gaussianBlurImage default returns the input unchanged (isGaussianBlurSupported() defaults to false). The JS port doesn't override either, so blur is a no-op there. Baseline reflects that. The remaining graphics-inscribed-triangle-grid mismatch is a pre-existing font-rendering drift between the master baseline and current Chromium output - not related to this PR. The moving-initializr branch dropped that golden in commit `ci(js-port): drop bogus master golden for graphics-inscribed-triangle-grid`; leaving it untouched here so the maintainers can decide whether to refresh, drop, or fix the renderer drift in master separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * CSS compiler: route conic-gradient and repeating-*-gradient through the parser CssGradientsScreenshotTest failed on Android with "Missing gradient for CssGradientRepeatingRadial" because three of my new gradient functions were being silently rejected before reaching CN1Gradient.parse(). Two independent gaps both contributed: 1. The `background:` property handler at CSSTheme.apply() switched on the function name and accepted only `linear-gradient` and `radial-gradient` - it threw "Unsupported function in background property" for `conic-gradient`, `repeating-linear-gradient`, and `repeating-radial-gradient`. So the background shorthand was dropped entirely for those three rules and `getCN1Gradient()` was never invoked. Added all three function names to the accepted list. 2. Flute's SAC parser only special-cases the two natively-recognized gradient function names. For anything else it falls back to a generic function-argument parse that wraps bare identifiers in `attr(...)`, emitting SAC_ATTR (stringValue = "attr(circle)") instead of SAC_IDENT. My parsers compared against SAC_IDENT only, so `circle at center`, `from <angle>`, `at <pos>` keywords - and named-color stops like `red`, `yellow` - all silently fell through. Added isIdentLike() / identValue() helpers that accept both SAC_IDENT and SAC_ATTR and unwrap the `attr(...)` wrapper transparently. Routed every keyword check in parseLinearGradientExtended / parseRadialGradientExtended / parseConicGradient through them. Extended getColorString's SAC_IDENT / SAC_STRING_VALUE case to also match SAC_ATTR so named colors like `red, yellow, blue` resolve. Local verification: TestRadialRepeat (an ad-hoc harness that runs NoCefCSSCLI on theme.css and dumps theme.res entries) now reports bgType + bgGradientEx for all eight CssGradient* UIIDs with the expected concrete subclass for each. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix repeating-* gradient rendering + CSS compiler cache invalidation Repeating-linear and repeating-radial gradients rendered as a thin band at the corner with the last color filling the rest of the box. Native shaders (Android LinearGradient/RadialGradient, Java2D *GradientPaint) tile OUTSIDE the gradient line, so when CSS gives stops like `red 0%, white 10%` the entire stop period sits in the first 10% of the bounding-box span and the remaining 90% is the final color. Add computeShaderEndpoints / computeShaderRadii that clip the shader range to one stop-list period plus getNormalizedPositions() that rescales stops to [0, 1]. Switch Android/JavaSE port fillGradient paths to use them. NO_CYCLE behavior is unchanged. Also: scripts/build-{android,ios}-port.sh now include the `designer` module in their `-pl X -am` set. The maven plugin's CSS compile step runs designer_1.jar which embeds css-compiler classes; without -pl designer the CI cache restores a previous build's designer.jar even when CSSTheme.java has changed. That cache hit silently dropped the new conic / repeating-* gradient parsing from theme.res, which is why iOS screenshots were missing css-gradients and Android rendered the stale path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Split designer install from android/ios port build Including designer in -pl android,designer -am pulls javase into the reactor (designer -> javase-svg -> javase) and Ports/JavaSE has CEF imports that only resolve under the local-dev-javase profile. The combined build failed with "package org.cef.handler does not exist". Split into two maven invocations: first install designer with -Plocal-dev-javase so jcef.jar is on its classpath, then run the original port build set unchanged. The designer step still busts the ~/.m2 cache for css-compiler / designer, which was the whole point. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Split combined declaration to satisfy PMD OneDeclarationPerLine Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix software repeating-gradient sampling + smoother demo CSS Two issues caused the repeating-* gradients to render wrong on iOS (both Metal and GL) and to look uninformative on Android: 1. Gradient.sampleStops()'s CYCLE_REPEAT/REFLECT wrapped t with `t - floor(t)`, assuming the stop period is [0, 1]. CSS like `white 0%, red 16%` defines the period as [0, 0.16], so wrapped t values >= 0.16 fell off the end of the position table and returned the final color across the rest of the rect (white circle on a red background, white corner on a red background - exactly what iOS was showing). The wrap now uses positions[0]..positions[N-1] as the period. 2. The CssGradientRepeating{Linear,Radial} test UIIDs used four-stop hard-edged CSS (`gray 5%, red 5%`), which is technically valid CSS but renders as solid stripes instead of demonstrating a gradient. Replaced with two-stop patterns so the screenshot test actually shows smooth repeating bands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Bake new screenshot goldens for css-gradients + gradient-stops/gaussian-blur The CSS gradient + filter:blur work introduced three new screenshot tests (css-gradients, graphics-draw-gradient-stops, graphics-gaussian-blur) that had no baseline in the iOS/Android golden sets yet. With the iOS software repeating-* fix (Gradient.sampleStops period wrap) and the Android native shader endpoint clipping now landing, the repeating-linear / repeating-radial tiles render correctly on all ports - so capture the goldens. - scripts/android/screenshots/: css-gradients, graphics-draw-gradient-stops, graphics-gaussian-blur baked from emulator instrumentation run. - scripts/ios/screenshots/ (GL) + scripts/ios/screenshots-metal/ (Metal): same three goldens captured per backend. - scripts/javascript/screenshots/css-gradients.png: new JS port golden. - scripts/javascript/screenshots/graphics-inscribed-triangle-grid.png: refreshed - the triangles themselves are pixel-identical to the prior golden; only the title-text font scaling drifted, which kept the JS pipeline red on missing-vs-actual title rendering. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Android gradient AA + dither; trim skill css.md historical bits The radial gradient looked aliased on Android because fillGradient was explicitly disabling AA before drawing. Enable AA + dither for the duration of the shader fill (and restore both in finally) - the elliptical-radial transform stops stair-stepping the bands and slow stop-to-stop blends stop banding visibly in 8-bit RGB. Also trim the skill's css.md gradient/filter sections - the LLM doesn't need historical context about prior limits or Painter workarounds; just the current syntax + API surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * CSS filter: parse non-blur functions into a 4x5 color matrix `filter:` / `backdrop-filter:` now accept brightness, contrast, grayscale, hue-rotate, invert, opacity, saturate, and sepia in addition to blur. Each function reduces to a 4x5 color matrix (per the CSS filter / SVG feColorMatrix spec); a chain like `filter: brightness(1.2) contrast(0.9) saturate(1.3)` composes the matrices in CSS order, so a single matrix lands on the Style: - Style#getFilterColorMatrix() / setFilterColorMatrix(float[]) - Style#getBackdropFilterColorMatrix() / setBackdropFilterColorMatrix(float[]) The matrix is stored as row-major float[20] - 4 rows of [R, G, B, A, offset]; offset is in 0-255 RGB space so ports can hand it straight to Android's ColorMatrix / iOS CIColorMatrix without rescaling. Identity is represented as null to avoid wasting 80 bytes per style on the common case. Resource format bumps to minor version 14 to carry the new keys (`filterColorMatrix` / `backdropFilterColorMatrix`). UIManager picks them up from theme.res and CSSTheme writes them. Round-trip is verified by CssFilterBlurScreenshotTest (renamed conceptually but class kept for stability), which now also asserts grayscale(1) collapses to the Rec 709 luma weights. Paint-time application is the same follow-up as filter:blur - the matrix lives on Style; Component.paint plumbing will consume it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Dev guide: replace stale gradient images with new overview screenshots The CSS section's gradient block used to embed 10 small images, one per "natively supported" CSS form vs the rasterized-fallback warnings. The limits the images documented are gone (every form is natively supported now), so the per-example images were misleading. Drop them and replace with two overview screenshots taken from the framework's screenshot test suite (iOS GL): - css-gradients-overview.png: 4x2 grid showing linear-angled, linear-to-side, mismatched-alpha linear, radial farthest-corner, radial ellipse, conic, repeating-linear, repeating-radial. - css-filter-blur-overview.png: blur applied via Graphics.gaussianBlur to both RGB stripes and a gradient. Also expand the filter section to document the new chain functions (brightness, contrast, grayscale, hue-rotate, invert, opacity, saturate, sepia) that land on filterColorMatrix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Gradient: copyright, weak-ref raster cache, runtime CSS parser - Replace Oracle copyright on the four new Gradient classes with the Codename One copyright and drop the "Since 8.1" javadoc tails (also removed the matching mention from docs/developer-guide/css.asciidoc). - CSSBorder.LinearGradient/RadialGradient: replace `new ColorStop[0]` with a shared `ColorStop.EMPTY` sentinel. - Gradient now holds a weak-ref raster cache keyed by (width, height) via Display.createSoftWeakRef; the default CodenameOneImplementation.fillGradient pulls from the cache so a gradient painted into the same rect on subsequent frames no longer re-rasterises per-pixel. Setters in each subclass invalidate the cache. - New runtime parsers covering the same syntax as the build-time CSS compiler: * com.codename1.ui.Gradient.parseCss(String) -> Gradient (via the new CSSGradientParser). Recognises linear / radial / conic / repeating-* with multi-stops, arbitrary angles in deg/rad/grad/turn, `to <side>` keywords, radial shape+extent+at, and conic `from <angle> at <pos>`. * com.codename1.ui.plaf.CSSFilterParser.parse(String) returns a FilterChain with blur radius + composed 4x5 color matrix for the eight CSS filter functions. * CSSBorder.backgroundImage(String) now routes gradient strings through the new parser and paints them in paintBorderBackground. - Unit-test coverage for both runtime parsers and end-to-end through the build-time CSSTheme.load (CSSGradientParserTest -- 22 cases, CSSFilterParserTest -- 16 cases, CSSThemeGradientTest -- 19 cases, plus a CSSBorder gradient-string smoke test). core-unittests picks up css-compiler as a test-scope dependency so the end-to-end test can drive CSSTheme.load with TestCodenameOneImplementation attached. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * iOS Metal: GPU multi-stop gradients + separable Gaussian blur The new com.codename1.ui.Gradient hierarchy and Style.filterBlurRadius were previously falling through to the software ARGB rasterizer on iOS (the rasterizer's output was wrapped as a NativeImage and drawn via drawImage), and filter:blur used CIGaussianBlur. This commit adds real Metal shader paths for both, with the cached-raster fallback only firing on gradients that exceed the 8-stop shader budget or on GL builds. CN1MetalShaders.metal: * cn1_fs_multistop_gradient -- single fragment shader covering linear / radial / conic, up to 8 stops packed into a float4 positions buffer + a float4 colors buffer, premultiplied colors, cycle modes NONE / REPEAT / REFLECT. REPEAT and REFLECT wrap across [positions[0], positions[last]] to match CSS repeating-*-gradient semantics. * cn1_fs_gaussian_blur -- 13-tap separable kernel, horizontal / vertical pass selected by a uniform. CN1MetalPipelineCache: adds MultiStopGradient and GaussianBlur pipelines. Blur pipeline overrides stencilAttachmentPixelFormat to Invalid since offscreen blur targets carry no stencil. CN1Metalcompat: new C APIs CN1MetalFillGradient(...) (packs the header / geometry / stops, calls drawPrimitives via the existing matrix and clip state) and CN1MetalGaussianBlurImage(src, dst, radius) (allocates a private intermediate MTLTexture, encodes horizontal then vertical passes, commits and waits). DrawMultiStopGradient.h/.m -- ExecutableOp subclass capturing the gradient parameters into stack-sized arrays and calling CN1MetalFillGradient from execute (mirrors DrawGradient for mutable-image and global-graphics targeting). IOSNative.m / IOSNative.java -- JNI bridge fillGradient(...). gausianBlurImage on CN1_USE_METAL builds runs the Metal-native two-pass blur (with a private->shared blit + CGBitmapContext readback), falling back to CIGaussianBlur only if the Metal allocation fails. GL builds are unchanged. IOSImplementation.fillGradient(Object, Gradient, ...) override: on Metal builds with <=8 stops, unpacks the Gradient (radial radii via RadialGradient.computeRadii, premultiplies stop colors) and calls the new native; otherwise falls through to the base CodenameOneImplementation which now uses Gradient.getCachedRaster. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * CSSFilterParser: drop dead null check from chain compose loop SpotBugs flagged the `if (m == null) continue;` at line 78 as RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE - colorMatrixForFunction never returns null (every recognised function name returns a matrix and every unrecognised one throws), so the guard was unreachable. Drop it. RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE is in the core-unittests CI quality gate's forbidden list and was failing the build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * CSS parsers: fix PMD findings (foreach, unused param, FQN) CI's forbidden-PMD-rules gate flagged four findings introduced by the new CSS parsers and the fillGradient override: - CSSGradientParser.parsePositionCoord: the `horizontal` parameter was never read - drop it from the signature and both call sites. - CSSGradientParser.parseStops: index-style for-loop only used `i` to call `parts.get(i)` - convert to enhanced-for. - CSSFilterParser.parse: same index-style loop on `calls.get(i)` - convert to enhanced-for. - CodenameOneImplementation.fillGradient: drop the redundant `com.codename1.ui.Image` FQN, the package is already imported. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * iOS Metal blur: scale tap offsets by sigma; bake new gradient goldens The 13-tap separable Gaussian blur kernel was sampling at integer pixel offsets (i = 1..6 pixels from centre), which is fine for tiny radii but useless for the radii the test harness and CSS filter:blur(N) request in practice -- 30..40 pixels on a retina device after CN.convertToPixels. With sigma = radius / 2 = 15..20 the 6-pixel-wide kernel only samples the very peak of the Gaussian, all weights collapse to ~1.0, and the convolution degenerates into a ~13-pixel near-box filter that is visually indistinguishable from the input. Result: the Metal blur screenshot showed crisp stripes and a sharp gradient transition where the GL reference shows a heavy halo. Two fixes: 1. CN1MetalShaders.metal -- scale tap spacing by (sigma / 2) so the six taps each side cover +/-3 sigma (the visible Gaussian extent) regardless of sigma. Linear sampling smooths the result for non-integer tap distances. 2. CN1Metalcompat.m -- pass `radius` through to the shader as the standard deviation, matching CIGaussianBlur.inputRadius semantics (Apple treats inputRadius as sigma, not visible extent). The previous host computed sigma = radius / 2, halving the perceived blur radius vs the GL reference. Also bake new goldens for the two Metal gradient screenshots (css-gradients, graphics-draw-gradient-stops) -- the Metal shader output is correct but cosmetically differs from the previous baked references that were rendered through the software ARGB rasterizer fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * iOS Metal blur: use MPSImageGaussianBlur instead of hand-rolled shader A hand-rolled 13-tap separable Gaussian fragment shader cannot faithfully reproduce CIGaussianBlur across the full sigma range the test harness and CSS filter:blur request: - At small sigma, the fixed-pixel taps collapse all weights to ~1 and the kernel degenerates into a near-box filter that's visually indistinguishable from the input. - At large sigma, scaling the tap distances widens the visible extent but undersamples the curve: 6 taps each side at sigma spacing skips most of the Gaussian mass between samples, which reads as aliasing and tile-boundary artefacts instead of a smooth blur. MPSImageGaussianBlur is the same kernel CoreImage uses internally for CIGaussianBlur, exposed through MetalPerformanceShaders. It picks the kernel width automatically from sigma, switches to multipass / downsampled paths for very large radii, and matches the GL/CIFilter reference image visually. Drops the custom cn1_fs_gaussian_blur fragment shader, the CN1MetalPipelineGaussianBlur pipeline state, and the two-pass encode logic in CN1MetalGaussianBlurImage in favour of one MPS call. Adds MetalPerformanceShaders.framework to the iOS linker list when useMetal is on (IPhoneBuilder) so MPSImageGaussianBlur resolves at link time even when CLANG_ENABLE_MODULES doesn't pick it up automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * iOS Metal blur: route through CIGaussianBlur unconditionally Both attempts at a Metal-native Gaussian blur produced visually unacceptable output: - The hand-rolled 13-tap separable shader undersampled at large sigma (every 5+ pixels skipped, ~tile-stripe aliasing) and collapsed into a near-box filter at small sigma (all weights ~1). - MPSImageGaussianBlur with sigma = radius produced blur ~3x too strong vs CIGaussianBlur with the same numeric input, and no output-extent expansion, so the visible halo the goldens were baked with disappeared and the bars / gradients ended up looking washed and mixed instead of softly blurred. Matching CIGaussianBlur visually from MPS would require both empirical sigma scaling and padding the dst by ~3*sigma so the blur halo has room to fall off. CIGaussianBlur itself is Metal-backed (Apple uses MPSImageGaussianBlur internally for it) and already runs on iOS Metal builds via the existing fallback path through CN1MetalReadMutableImageAsUIImage, so no real performance regression - the read-back cost is paid once per blur invocation, not per frame. Drops the Metal-native blur entry point: - IOSNative.m -- remove the CN1_USE_METAL fast path; let gausianBlurImage flow into CIGaussianBlur as before for both GL and Metal builds - CN1Metalcompat.h/.m -- remove CN1MetalGaussianBlurImage and the MetalPerformanceShaders.h import - IPhoneBuilder.java -- remove the MetalPerformanceShaders.framework link line (no longer referenced) The Metal gradient pipeline (multi-stop linear / radial / conic via cn1_fs_multistop_gradient) is untouched and still ships in this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- 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