Skip to content

Fix CSSWatcher live reload: drop stale bindings + extract m2 designer jar#4929

Merged
shai-almog merged 3 commits into
masterfrom
fix/csswatcher-live-reload-regression
May 12, 2026
Merged

Fix CSSWatcher live reload: drop stale bindings + extract m2 designer jar#4929
shai-almog merged 3 commits into
masterfrom
fix/csswatcher-live-reload-regression

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

Summary

CSSWatcher's live reload stopped applying CSS edits in two distinct ways after the recent CSS/localization push. This PR fixes both, adds regression tests, and wires the maven/javase test module into CI so similar regressions are caught next time.

1. addThemeProps was stomping 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. CSSWatcher reloads the recompiled theme.res through the same code path — but addThemeProps never clears themeConstants.

When the user replaced a var() CSS rule with a literal, the recompiled theme.res no longer emitted the matching @cn1-bind:<key> entry, but the previous binding was still sitting in themeConstants from the initial load. 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. Pure @accent-color-only 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; fails before the fix
    • cssReloadKeepsBindingWhenStillEmittedTogether — guard against an over-eager fix that drops bindings on every reload
    • overrideOnlyReloadKeepsBindings — repeated @accent-color retunes still work
  • MavenUtilsTest (new) covers the wrapper-vs-inner-jar resolution with five cases: happy path, re-use of extracted 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 those tests actually executable, the javase pom pins maven-surefire-plugin to 3.2.5 (the parent's 2.21.0 doesn't auto-discover JUnit Jupiter — that's why the pre-existing CSSWatcherTest etc. compiled but never ran).
  • pr.yml gets a new "Run JavaSE port unit tests" step. Without it, the CSSWatcher/MavenUtils/JavaSEPort helper regressions would continue to slip through — which is the original gap behind this issue.

Test plan

  • mvn -pl core-unittests test — all binding tests pass, including the new reproducer; pre-existing BorderAndPlafTest.testRoundBorderShadowSpreadAndPaintingCaches flake reproduces on master (cross-test pollution, unrelated to this PR)
  • mvn -pl javase test -Plocal-dev-javase — 15 tests pass (CSSWatcherTest × 2, LocationSimulationTest × 3, JavaSEPortFontMappingTest × 5, MavenUtilsTest × 5)
  • CI matrix (Java 8/17/21) reproduces locally-green results
  • Manual smoke: edit theme.css in a generated initializr project, confirm the simulator's running form picks up both literal and var() changes without restart

🤖 Generated with Claude Code

… 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>
Comment thread Ports/JavaSE/src/com/codename1/impl/javase/util/MavenUtils.java Fixed
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 12, 2026

Compared 7 screenshots: 7 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 12, 2026

Compared 15 screenshots: 15 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 12, 2026

Compared 105 screenshots: 105 matched.

Native Android coverage

  • 📊 Line coverage: 11.25% (6218/55280 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.94% (30802/344403), branch 3.86% (1266/32764), complexity 4.99% (1567/31415), method 8.73% (1283/14698), class 14.77% (294/1991)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 11.25% (6218/55280 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.94% (30802/344403), branch 3.86% (1266/32764), complexity 4.99% (1567/31415), method 8.73% (1283/14698), class 14.77% (294/1991)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6327 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.ClassReader – 0.00% (0/1519 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1148 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.MethodWriter – 0.00% (0/923 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/730 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/623 lines covered)
      • org.jacoco.agent.rt.internal_b6258fc.asm.org.jacoco.agent.rt.internal_b6258fc.asm.Frame – 0.00% (0/564 lines covered)
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysJvmKt – 0.00% (0/495 lines covered)
      • kotlinx.coroutines.kotlinx.coroutines.JobSupport – 0.00% (0/423 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 869.000 ms
Base64 CN1 encode 196.000 ms
Base64 encode ratio (CN1/native) 0.226x (77.4% faster)
Base64 native decode 788.000 ms
Base64 CN1 decode 309.000 ms
Base64 decode ratio (CN1/native) 0.392x (60.8% faster)
Image encode benchmark status skipped (SIMD unsupported)

- 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>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 12, 2026

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

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>
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 12, 2026

Compared 104 screenshots: 104 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 360 seconds

Build and Run Timing

Metric Duration
Simulator Boot 92000 ms
Simulator Boot (Run) 1000 ms
App Install 14000 ms
App Launch 7000 ms
Test Execution 283000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 2198.000 ms
Base64 CN1 encode 2359.000 ms
Base64 encode ratio (CN1/native) 1.073x (7.3% slower)
Base64 native decode 1475.000 ms
Base64 CN1 decode 1311.000 ms
Base64 decode ratio (CN1/native) 0.889x (11.1% faster)
Base64 SIMD encode 1056.000 ms
Base64 encode ratio (SIMD/native) 0.480x (52.0% faster)
Base64 encode ratio (SIMD/CN1) 0.448x (55.2% faster)
Base64 SIMD decode 829.000 ms
Base64 decode ratio (SIMD/native) 0.562x (43.8% faster)
Base64 decode ratio (SIMD/CN1) 0.632x (36.8% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 106.000 ms
Image createMask (SIMD on) 18.000 ms
Image createMask ratio (SIMD on/off) 0.170x (83.0% faster)
Image applyMask (SIMD off) 339.000 ms
Image applyMask (SIMD on) 128.000 ms
Image applyMask ratio (SIMD on/off) 0.378x (62.2% faster)
Image modifyAlpha (SIMD off) 214.000 ms
Image modifyAlpha (SIMD on) 99.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.463x (53.7% faster)
Image modifyAlpha removeColor (SIMD off) 369.000 ms
Image modifyAlpha removeColor (SIMD on) 187.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.507x (49.3% faster)
Image PNG encode (SIMD off) 2085.000 ms
Image PNG encode (SIMD on) 1377.000 ms
Image PNG encode ratio (SIMD on/off) 0.660x (34.0% faster)
Image JPEG encode 1163.000 ms

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 12, 2026

Compared 105 screenshots: 105 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 329 seconds

Build and Run Timing

Metric Duration
Simulator Boot 119000 ms
Simulator Boot (Run) 1000 ms
App Install 21000 ms
App Launch 11000 ms
Test Execution 310000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1474.000 ms
Base64 CN1 encode 1427.000 ms
Base64 encode ratio (CN1/native) 0.968x (3.2% faster)
Base64 native decode 936.000 ms
Base64 CN1 decode 1049.000 ms
Base64 decode ratio (CN1/native) 1.121x (12.1% slower)
Base64 SIMD encode 600.000 ms
Base64 encode ratio (SIMD/native) 0.407x (59.3% faster)
Base64 encode ratio (SIMD/CN1) 0.420x (58.0% faster)
Base64 SIMD decode 472.000 ms
Base64 decode ratio (SIMD/native) 0.504x (49.6% faster)
Base64 decode ratio (SIMD/CN1) 0.450x (55.0% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 61.000 ms
Image createMask (SIMD on) 44.000 ms
Image createMask ratio (SIMD on/off) 0.721x (27.9% faster)
Image applyMask (SIMD off) 163.000 ms
Image applyMask (SIMD on) 65.000 ms
Image applyMask ratio (SIMD on/off) 0.399x (60.1% faster)
Image modifyAlpha (SIMD off) 128.000 ms
Image modifyAlpha (SIMD on) 92.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.719x (28.1% faster)
Image modifyAlpha removeColor (SIMD off) 205.000 ms
Image modifyAlpha removeColor (SIMD on) 76.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.371x (62.9% faster)
Image PNG encode (SIMD off) 1926.000 ms
Image PNG encode (SIMD on) 1195.000 ms
Image PNG encode ratio (SIMD on/off) 0.620x (38.0% faster)
Image JPEG encode 685.000 ms

@shai-almog shai-almog merged commit c9d078e into master May 12, 2026
19 of 21 checks passed
shai-almog added a commit that referenced this pull request May 16, 2026
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>
shai-almog added a commit that referenced this pull request May 16, 2026
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>
shai-almog added a commit that referenced this pull request May 16, 2026
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>
shai-almog added a commit that referenced this pull request May 16, 2026
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants