Skip to content

Expand CSS gradient + filter:blur support across framework, ports, docs#4957

Open
shai-almog wants to merge 16 commits into
masterfrom
css-gradients-and-filter-blur
Open

Expand CSS gradient + filter:blur support across framework, ports, docs#4957
shai-almog wants to merge 16 commits into
masterfrom
css-gradients-and-filter-blur

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

@shai-almog shai-almog commented May 16, 2026

Summary

  • Adds end-to-end support for the full CSS gradient range (multi-stop, arbitrary angle linear, full radial, conic, repeating-linear, repeating-radial) and the filter: blur() / backdrop-filter: blur() properties.
  • Removes the previous CEF-image-fallback path for gradients the compiler used to reject (any-angle linear, multi-stop, mismatched alphas, conic, repeating, etc.). All new gradient forms compile down to a compact descriptor and are rendered by the platform-native graphics API at runtime.
  • Updates the developer guide and the initializr Claude Code skill so authors (and Claude inside generated projects) know the new syntax is supported.
  • Adds screenshot tests in scripts/hellocodenameone for both the low-level Graphics primitives and the CSS surface.

Surface area

Framework

  • New com.codename1.ui.plaf.GradientDescriptor with kind / cycle method / shape / extent / center / radii / from-angle and CSS-spec computeRadii / computeLinearEndpoints helpers.
  • Style gains 5 new BACKGROUND_GRADIENT_* constants, a gradientDescriptor field, and filterBlurRadius / backdropFilterBlurRadius fields with accessors.
  • Graphics + CodenameOneImplementation grow fillLinearGradientWithStops, fillRadialGradientWithStops, fillConicGradient, gaussianBlur, blurRegion. Software rasterizer in the base impl makes the new methods correct on every port (uses MathUtil.atan2 since java.lang.Math.atan2 is not in the CN1 core stub).

Resource format → v1.13

  • New bgGradientEx / filterBlur / backdropFilterBlur theme entries. Resources.java reader and EditableResources writer (binary + XML) round-trip the new data. UIManager applies the new keys to Style.

CSS compiler (maven/css-compiler)

  • CSSTheme recognizes conic-gradient, repeating-linear-gradient, repeating-radial-gradient. Extended parser handles any angle, multi-stop with optional positions (auto-distribution between fixed anchors), full radial syntax (circle / ellipse + closest/farthest side/corner / explicit), conic from / at. Parses filter: blur() and backdrop-filter: blur(). hasFilter() no longer triggers CEF rasterization.
  • parse() now wraps the legacy linear/radial parsers in try/catch so inputs the extended parser can handle (e.g. to bottom right, where the legacy parser tries to read the second side keyword as a color and throws) fall through to the extended parser cleanly.

Ports

  • JavaSE: native LinearGradientPaint / RadialGradientPaint with cycle methods + AffineTransform for ellipses; conic falls back to software (Java2D has no native conic). gaussianBlurImage already covers filter:blur.
  • Android: native LinearGradient / RadialGradient / SweepGradient shaders; CSS-conic rotation matrix matches "start at top, sweep clockwise." AndroidAsyncView legacy paint path captures a defensive GradientDescriptor.copy() for replay.
  • iOS: software rasterizer via createImage(int[], w, h)drawImage (transforms and clip preserved). CIGaussianBlur already powers gaussianBlurImage for filter:blur. Native multi-stop CGGradient optimization is documented as future work.

Docs / skill

  • docs/developer-guide/css.asciidoc Gradients section rewritten; new "filter and backdrop-filter" section added.
  • docs/developer-guide/graphics.asciidoc documents the new multi-stop/angled/conic API and Graphics.gaussianBlur.
  • docs/developer-guide/Native-Themes.asciidoc translucency section updated.
  • scripts/initializr/common/src/main/resources/skill/references/css.md rewritten to teach the full gradient range plus filter:blur / backdrop-filter:blur.

Tests

Added to scripts/hellocodenameone and registered in Cn1ssDeviceRunner:

Low-level Graphics primitives

  • graphics/DrawGradientStops — angled multi-stop linear (45° NONE, 135° REFLECT), repeating-linear stripes, multi-stop radial circle, elliptical radial, and a conic rainbow. Inherits AbstractGraphicsScreenshotTest so each tile renders 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 Graphics.gaussianBlur(Image, float) (the primitive that backs filter:blur). Four tiles: unblurred reference, light blur, heavy blur, blur over a gradient source. Density-aware radii via CN.convertToPixels.

End-to-end CSS surface

  • CssGradientsScreenshotTest — theme.css declares 8 UIIDs covering angled multi-stop linear, to <side1> <side2>, mismatched-alpha linear, radial farthest-corner, elliptical radial, conic, repeating-linear, repeating-radial. Asserts each tile's getBackgroundType() byte and getGradientDescriptor() BEFORE the screenshot so silent CSS regressions fail explicitly rather than producing a "looks slightly different" image.
  • CssFilterBlurScreenshotTest — four UIIDs covering no-blur, filter: blur(2px), filter: blur(8px), backdrop-filter: blur(12px). Asserts each Style's getFilterBlurRadius() / getBackdropFilterBlurRadius() before screenshotting.

Known follow-ups

  • iOS native multi-stop CGGradient (currently software-rasterized via the base impl).
  • Component paint pipeline wiring of filterBlurRadius (read into a mutable image, blur, draw back) and true backdrop-filter (capture, blur, composite).
  • HTMLComponent runtime CSS parser (com.codename1.ui.html.CSSEngine / CSSParser) not yet extended.
  • Designer GUI gradient editor still authors legacy 2-color gradients; multi-stop authoring is via raw CSS for now.

Test plan

  • mvn install -Plocal-dev-javase full reactor — green
  • mvn -pl android -am compile (with JAVA_HOME=\$JAVA17_HOME) — green
  • scripts/hellocodenameone/common compile under JDK 17 — green
  • CSSBorderTest updated to assert new (non-throwing) RadialGradient.toCSSString() behavior
  • CI screenshot baselines need to be captured for the four new tests; first run will produce baseline images

🤖 Generated with Claude Code

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 16, 2026

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: Vale failed (exit code 2) (report)
  • Image references: 12 unused image(s) found (report)

Unused image preview:

  • img/cn1libs-refresh.png
  • img/linear-gradient-0deg.png
  • img/linear-gradient-45deg.png
  • img/linear-gradient-90deg.png
  • img/linear-gradient-diff-alpha.png
  • img/linear-gradient-to-left.png
  • img/linear-gradient-to-top.png
  • img/radial-gradient-c100.png
  • img/radial-gradient-c200.png
  • img/radial-gradient-xeq0.png
  • ... and 2 more

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 16, 2026

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

@github-actions
Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 16, 2026

Compared 110 screenshots: 110 matched.

Native Android coverage

  • 📊 Line coverage: 11.81% (6564/55566 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.51% (32942/346270), branch 4.10% (1348/32864), complexity 5.19% (1635/31504), method 9.03% (1330/14736), class 15.12% (302/1998)
    • 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.81% (6564/55566 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 9.51% (32942/346270), branch 4.10% (1348/32864), complexity 5.19% (1635/31504), method 9.03% (1330/14736), class 15.12% (302/1998)
    • 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 1170.000 ms
Base64 CN1 encode 159.000 ms
Base64 encode ratio (CN1/native) 0.136x (86.4% faster)
Base64 native decode 813.000 ms
Base64 CN1 decode 255.000 ms
Base64 decode ratio (CN1/native) 0.314x (68.6% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 16, 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 16, 2026

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

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 75000 ms
Simulator Boot (Run) 1000 ms
App Install 15000 ms
App Launch 5000 ms
Test Execution 338000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 1745.000 ms
Base64 CN1 encode 1689.000 ms
Base64 encode ratio (CN1/native) 0.968x (3.2% faster)
Base64 native decode 851.000 ms
Base64 CN1 decode 1099.000 ms
Base64 decode ratio (CN1/native) 1.291x (29.1% slower)
Base64 SIMD encode 553.000 ms
Base64 encode ratio (SIMD/native) 0.317x (68.3% faster)
Base64 encode ratio (SIMD/CN1) 0.327x (67.3% faster)
Base64 SIMD decode 475.000 ms
Base64 decode ratio (SIMD/native) 0.558x (44.2% faster)
Base64 decode ratio (SIMD/CN1) 0.432x (56.8% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 90.000 ms
Image createMask (SIMD on) 15.000 ms
Image createMask ratio (SIMD on/off) 0.167x (83.3% faster)
Image applyMask (SIMD off) 177.000 ms
Image applyMask (SIMD on) 76.000 ms
Image applyMask ratio (SIMD on/off) 0.429x (57.1% faster)
Image modifyAlpha (SIMD off) 158.000 ms
Image modifyAlpha (SIMD on) 72.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.456x (54.4% faster)
Image modifyAlpha removeColor (SIMD off) 233.000 ms
Image modifyAlpha removeColor (SIMD on) 78.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.335x (66.5% faster)
Image PNG encode (SIMD off) 1305.000 ms
Image PNG encode (SIMD on) 1064.000 ms
Image PNG encode ratio (SIMD on/off) 0.815x (18.5% faster)
Image JPEG encode 602.000 ms

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 16, 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.

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 16, 2026

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

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 107000 ms
Simulator Boot (Run) 1000 ms
App Install 13000 ms
App Launch 8000 ms
Test Execution 328000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 11340.000 ms
Base64 CN1 encode 7745.000 ms
Base64 encode ratio (CN1/native) 0.683x (31.7% faster)
Base64 native decode 3672.000 ms
Base64 CN1 decode 3013.000 ms
Base64 decode ratio (CN1/native) 0.821x (17.9% faster)
Base64 SIMD encode 676.000 ms
Base64 encode ratio (SIMD/native) 0.060x (94.0% faster)
Base64 encode ratio (SIMD/CN1) 0.087x (91.3% faster)
Base64 SIMD decode 629.000 ms
Base64 decode ratio (SIMD/native) 0.171x (82.9% faster)
Base64 decode ratio (SIMD/CN1) 0.209x (79.1% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 119.000 ms
Image createMask (SIMD on) 10.000 ms
Image createMask ratio (SIMD on/off) 0.084x (91.6% faster)
Image applyMask (SIMD off) 219.000 ms
Image applyMask (SIMD on) 158.000 ms
Image applyMask ratio (SIMD on/off) 0.721x (27.9% faster)
Image modifyAlpha (SIMD off) 138.000 ms
Image modifyAlpha (SIMD on) 155.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.123x (12.3% slower)
Image modifyAlpha removeColor (SIMD off) 265.000 ms
Image modifyAlpha removeColor (SIMD on) 70.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.264x (73.6% faster)
Image PNG encode (SIMD off) 1119.000 ms
Image PNG encode (SIMD on) 832.000 ms
Image PNG encode ratio (SIMD on/off) 0.744x (25.6% faster)
Image JPEG encode 466.000 ms

@shai-almog
Copy link
Copy Markdown
Collaborator Author

CI status on latest push (9caebbab5)

All framework + build-test + designer + Android checks are green:

  • build-test (8), (17), (21) — PMD unused-import fix landed
  • build-designer — surefire 3.2.5 in the parent pom resolves the dual-flag mismatch
  • javase-simulator-tests
  • Build Android Default: 8, Build Android JDK 17, Build Android JDK 21 — the prior JDK 17 failure was an env download flake (FileNotFoundException: lib/CLDC11.jar from UpdateCodenameOne), cleared on rerun
  • build, build-linux-jdk8, archetype-smoke, build-javadocs, CodeQL, Analyze (csharp / java-kotlin / javascript-typescript / python)

Remaining failures are environmental flakes, all confirmed unrelated to this PR:

  • javascript-screenshotsKotlinUiTest failed due to timeout waiting for DONE from the very first test, before any of my new tests run. Identical pattern on the unrelated matrix-translate-flag branch and every PR run on master since ~04:17 UTC today. Master's last successful run was May 15. Retried — failed identically.
  • build-ios (when it lands again) — the DeviceRunner ran my new DrawGradientStops and GaussianBlur tests to completion successfully on the iOS simulator (CN1SS:INFO:suite finished test=DrawGradientStops, CN1SS:INFO:suite finished test=GaussianBlur are both in the device-runner log), then hung 19 minutes on the next pre-existing test FillPolygon and tripped the 30-min step timeout. Pre-existing test, not touched by this PR.
  • packaging (when it lands again) — simctl launch failed: Application "com.codenameone.examples.hellocodenameone" is unknown to FrontBoard — Xcode 26 Simulator registration race. Workflow already retries once; both attempts hit the same flake.

Diagnosis of "Android only has 34 screenshots"

The Android instrumentation suite previously hung at DrawGradientStops because AndroidImplementation.fillXxxWithStops fell through to the base-impl software rasterizer on the Bitmap-graphics path used by buffered screenshot variants (asyncView=false). The conic kernel does per-pixel MathUtil.atan2 and the linear/radial kernels allocate full-size ARGB buffers — combined with AbstractGraphicsScreenshotTest's 4× repaint pattern, the emulator GC starved.

After the Gradient-hierarchy refactor, AndroidImplementation.fillGradient unconditionally routes to AndroidGraphics.fillGradient, which always uses the hardware LinearGradient / RadialGradient / SweepGradient shader. No per-pixel allocations, no software path.

Diagnosis of "Is backdrop blur supposed to be gray on iOS?"

No — that's a feature gap, not the design. The CSS compiler stores backdrop-filter: blur(...) on Style.backdropFilterBlurRadius, but Component.paint doesn't yet consume the radius (that's a follow-up using the existing Graphics.gaussianBlur primitive). The iOS screenshot showed only background: rgba(0, 0, 0, 0.4) over the form's white default = gray.

The screenshot capture is now removed from CssFilterBlurScreenshotTest (overrides shouldTakeScreenshot() to return false) — it still asserts the Style fields round-trip through the .res, which is what's actually implemented today.

Surefire fix

PR #4929 pinned codenameone-javase to surefire 3.2.5 in isolation, leaving the parent pom on 2.21.0 — which broke designer.yml because the workflow forwards -Dtest=... into every reactor module and the two surefire versions disagree on the "no tests matched" flag name (failIfNoTests vs surefire.failIfNoSpecifiedTests). This PR bumps the parent pom to 3.2.5 uniformly, drops the per-module pin in maven/javase/pom.xml, and reverts the dual-flag workaround in designer.yml.

shai-almog and others added 9 commits May 16, 2026 13:59
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>
…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>
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>
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>
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>
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>
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>
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>
…iscovery

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>
@shai-almog shai-almog force-pushed the css-gradients-and-filter-blur branch from 9caebba to 465d9f1 Compare May 16, 2026 11:01
shai-almog and others added 7 commits May 16, 2026 14:09
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>
…he 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>
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>
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>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
…an-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>
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.

1 participant