Skip to content

Fix #5049 follow-ups: clipped_badge rounded clip + picker capture stability#5056

Merged
shai-almog merged 3 commits into
masterfrom
fix-svg-clip-path-and-picker-stability
May 28, 2026
Merged

Fix #5049 follow-ups: clipped_badge rounded clip + picker capture stability#5056
shai-almog merged 3 commits into
masterfrom
fix-svg-clip-path-and-picker-stability

Conversation

@shai-almog
Copy link
Copy Markdown
Collaborator

Summary

Two follow-ups to #5049's "left on the table" list. Both originate in the
transcoder / test layer rather than the iOS / Android ports.

  1. Clipped-badge rounded clip-path (scripts/hellocodenameone/common/src/main/css/clipped_badge.svg) rendered as a sharp-cornered square on every port — Android, iOS GL, iOS Metal.

    • Root cause: the transcoded gradient-fill recipe pushed an inner setClip(__p) to confine LinearGradientPaint.paint's bounding-rect fill, but setClip REPLACES the current clip. The outer rounded clipPath set by the enclosing <rect clip-path="url(#rounded)"> was wiped before the gradient ran.
    • Fix: when __p.isRectangle() at runtime, switch the inner clamp to clipRect (which intersects). Non-rectangular __p keeps setClip(__p) because the framework doesn't expose a shape-shape intersect on the public Graphics API, and a bounding-rect clip would let the gradient bleed past the shape outline (would regress gradient_circle.svg, which has no outer clip-path).
    • Locks the new branch in via linearGradientEmitsPaint codegen test.
  2. LightweightPickerButtonsScreenshotTest screenshots drifted from the goldens.

    • Two factors:
      • picker.setDate while the popup is showing rebuilds each spinner's ListModel and snaps scrollY, but never revalidates the popup container — so the wheels reach the right scroll position but the surrounding tick rows are still laid out for the pre-setDate model. PopupButtonActionListener calls revalidate() + repaint() for button-driven setDate paths, but the screenshot test calls setDate directly and bypasses that listener.
      • The 400 ms settle timer ran on the EDT but never traversed enough paint cycles to guarantee the CAMetalLayer front buffer matched on iOS Metal (same symptom as DualAppearanceBaseTest in ddf03deaf).
    • Fix: add an explicit form.revalidate() + repaint() after setDate, and pump three Display.callSerially hops before emitCurrentFormScreenshot so at least three EDT paint cycles land between the settle timer and cn1_captureView's afterScreenUpdates:NO grab.

Goldens

The matching screenshot goldens still encode the pre-fix behaviour and need to be refreshed from this PR's CI run:

  • scripts/{android,ios,ios-metal}/screenshots/SVGStatic.png — currently shows the badge as a square; after this PR the rounded clipPath is honoured.
  • scripts/{android,ios,ios-metal}/screenshots/LightweightPickerButtons*.png — captured under the old non-deterministic timing.

Test plan

  • mvn -pl svg-transcoder test (transcoder unit tests including the updated gradient-emission test) passes locally.
  • LightweightPickerButtonsScreenshotTest compiles cleanly under JDK 17.
  • Android instrumentation run on this branch — regenerate SVGStatic.png + LightweightPickerButtons*.png goldens from the artifact.
  • iOS GL screenshot run on this branch — regenerate matching goldens.
  • iOS Metal screenshot run on this branch — regenerate matching goldens.
  • Visual inspection of refreshed SVGStatic.png: clipped_badge cell shows a rounded square; rest of the SVG grid unchanged.

🤖 Generated with Claude Code

…bility

Two follow-up fixes from #5049's "left on the table" list:

1. clipped_badge.svg's rounded clip-path rendered as a sharp-cornered
   square on every port. The transcoded gradient-fill recipe ran
   pushClip -> setClip(__p) -> paint.paint -> popClip, but setClip
   REPLACES the current clip instead of intersecting, so the outer
   rounded clipPath (set by the enclosing element's clip-path attr)
   was wiped before the gradient ran. Switch the inner clamp to
   clipRect when __p.isRectangle() at runtime -- clipRect intersects,
   so the rounded outer clip survives; non-rectangular __p keeps
   setClip(__p) because the framework doesn't expose a shape-shape
   intersect on the public Graphics API and bounding-rect clipping
   would let the gradient bleed past the shape (visible regression on
   gradient_circle.svg which has no outer clip-path). Updates the
   linearGradientEmitsPaint codegen test to lock the new branch in.

2. LightweightPickerButtonsScreenshotTest's screenshots drifted from
   the goldens because (a) picker.setDate while the popup is showing
   rebuilds each spinner's ListModel and snaps scrollY but never
   revalidates the popup container -- so the wheels reach the right
   scroll position but the surrounding tick rows are still laid out
   for the pre-setDate model, and (b) the existing 400 ms settle
   timer ran on the EDT but never traversed enough paint cycles to
   guarantee the CAMetalLayer front buffer matched on iOS Metal
   (same symptom DualAppearanceBaseTest hit in ddf03de). Add an
   explicit form.revalidate() + repaint() after setDate to drive the
   layout PopupButtonActionListener does for button-driven setDates
   (the test bypasses that listener since it calls setDate directly),
   and pump three Display.callSerially hops before
   emitCurrentFormScreenshot so at least three EDT paint cycles land
   between the settle timer and cn1_captureView's
   afterScreenUpdates:NO grab.

The matching screenshot goldens (SVGStatic.png on all three ports
plus LightweightPickerButtons*.png) need to be refreshed from the
CI run that lands this PR -- the SVGStatic goldens currently encode
the bug (clipped_badge as a square), and the picker goldens were
captured under the old non-deterministic timing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 27, 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 27, 2026

Compared 116 screenshots: 116 matched.

Native Android coverage

  • 📊 Line coverage: 12.45% (7215/57972 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 10.15% (36277/357360), branch 4.27% (1445/33836), complexity 5.31% (1724/32486), method 9.24% (1406/15210), class 15.16% (321/2117)
    • 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: 12.45% (7215/57972 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 10.15% (36277/357360), branch 4.27% (1445/33836), complexity 5.31% (1724/32486), method 9.24% (1406/15210), class 15.16% (321/2117)
    • 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 1026.000 ms
Base64 CN1 encode 113.000 ms
Base64 encode ratio (CN1/native) 0.110x (89.0% faster)
Base64 native decode 954.000 ms
Base64 CN1 decode 234.000 ms
Base64 decode ratio (CN1/native) 0.245x (75.5% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 27, 2026

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

…act)

The first attempt at fixing clipped_badge.svg switched the inner
setClip(__p) to a runtime clipRect when __p.isRectangle(). That
preserved the outer rounded clip in theory, but Android's
clipRect-on-a-rounded-clip path runs through
ShapeUtil.intersection, and the CI run on this branch showed the
result rendered as a star/spike pattern -- the rounded clip
survived but the interior of the badge was XOR'd against an
unexplained diamond. The cleaner fix doesn't need that code path
at all.

Replace the runtime branch with a codegen-time decision: track the
depth of the active clip-path stack in clipPathDepth (incremented
when emitNode enters a clip-path block, popped on the way out). When
emitting a gradient fill, skip the inner pushClip/setClip(__p)/popClip
wrapping entirely if clipPathDepth > 0 -- the outer clip-path is
already constraining the gradient to the visible region. Every SVG
authoring tool we've seen builds clipPath as a subset of the element
it clips, so the outer is strictly more restrictive and the inner
clamp is redundant. Outside a clip-path block we keep the original
setClip(__p) clamp because LinearGradientPaint.paint rasterises
bands wider than __p's bounding box and would otherwise bleed past
non-rectangular shapes (gradient_circle.svg).

Updates linearGradientEmitsPaint to assert the standalone gradient
still emits the inner clip, and adds a new
gradientInsideClipPathSkipsInnerClip test that exercises the
clipped_badge structure and asserts exactly one setClip( call
(the outer clipPath) reaches the emitted source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 27, 2026

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

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 89000 ms
Simulator Boot (Run) 0 ms
App Install 11000 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 1137.000 ms
Base64 CN1 encode 1764.000 ms
Base64 encode ratio (CN1/native) 1.551x (55.1% slower)
Base64 native decode 269.000 ms
Base64 CN1 decode 1170.000 ms
Base64 decode ratio (CN1/native) 4.349x (334.9% slower)
Base64 SIMD encode 614.000 ms
Base64 encode ratio (SIMD/native) 0.540x (46.0% faster)
Base64 encode ratio (SIMD/CN1) 0.348x (65.2% faster)
Base64 SIMD decode 498.000 ms
Base64 decode ratio (SIMD/native) 1.851x (85.1% slower)
Base64 decode ratio (SIMD/CN1) 0.426x (57.4% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 70.000 ms
Image createMask (SIMD on) 31.000 ms
Image createMask ratio (SIMD on/off) 0.443x (55.7% faster)
Image applyMask (SIMD off) 414.000 ms
Image applyMask (SIMD on) 83.000 ms
Image applyMask ratio (SIMD on/off) 0.200x (80.0% faster)
Image modifyAlpha (SIMD off) 376.000 ms
Image modifyAlpha (SIMD on) 58.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.154x (84.6% faster)
Image modifyAlpha removeColor (SIMD off) 487.000 ms
Image modifyAlpha removeColor (SIMD on) 295.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.606x (39.4% faster)
Image PNG encode (SIMD off) 1199.000 ms
Image PNG encode (SIMD on) 920.000 ms
Image PNG encode ratio (SIMD on/off) 0.767x (23.3% faster)
Image JPEG encode 706.000 ms

@shai-almog
Copy link
Copy Markdown
Collaborator Author

shai-almog commented May 27, 2026

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

Benchmark Results

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

Build and Run Timing

Metric Duration
Simulator Boot 59000 ms
Simulator Boot (Run) 0 ms
App Install 11000 ms
App Launch 6000 ms
Test Execution 301000 ms

Detailed Performance Metrics

Metric Duration
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 native encode 840.000 ms
Base64 CN1 encode 2029.000 ms
Base64 encode ratio (CN1/native) 2.415x (141.5% slower)
Base64 native decode 582.000 ms
Base64 CN1 decode 1452.000 ms
Base64 decode ratio (CN1/native) 2.495x (149.5% slower)
Base64 SIMD encode 527.000 ms
Base64 encode ratio (SIMD/native) 0.627x (37.3% faster)
Base64 encode ratio (SIMD/CN1) 0.260x (74.0% faster)
Base64 SIMD decode 852.000 ms
Base64 decode ratio (SIMD/native) 1.464x (46.4% slower)
Base64 decode ratio (SIMD/CN1) 0.587x (41.3% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 58.000 ms
Image createMask (SIMD on) 9.000 ms
Image createMask ratio (SIMD on/off) 0.155x (84.5% faster)
Image applyMask (SIMD off) 124.000 ms
Image applyMask (SIMD on) 74.000 ms
Image applyMask ratio (SIMD on/off) 0.597x (40.3% faster)
Image modifyAlpha (SIMD off) 344.000 ms
Image modifyAlpha (SIMD on) 79.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.230x (77.0% faster)
Image modifyAlpha removeColor (SIMD off) 176.000 ms
Image modifyAlpha removeColor (SIMD on) 186.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.057x (5.7% slower)
Image PNG encode (SIMD off) 1241.000 ms
Image PNG encode (SIMD on) 901.000 ms
Image PNG encode ratio (SIMD on/off) 0.726x (27.4% faster)
Image JPEG encode 583.000 ms

CI run 26534422667 (Android Default: 8) / 26534422711 (iOS UI builds)
on this branch produced the four screenshots that drifted off the
pre-fix goldens:

- scripts/android/screenshots/SVGStatic.png -- clipped_badge cell
  now renders the rounded outline + gradient fill (was a sharp-
  cornered square pre-fix).
- scripts/ios/screenshots/SVGStatic.png -- same fix on iOS GL.
- scripts/ios/screenshots-metal/SVGStatic.png -- same fix on iOS
  Metal.
- scripts/ios/screenshots-metal/LightweightPickerButtons.png -- iOS
  Metal-only refresh from the picker capture-stability changes; the
  three extra Display.callSerially hops + post-setDate revalidate
  shifted a few subpixel rows of the spinner enough to fail the
  channel-delta tolerance on Metal (Android + iOS GL still matched
  their existing goldens).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@shai-almog shai-almog merged commit 6d746cc into master May 28, 2026
22 checks passed
shai-almog added a commit that referenced this pull request May 29, 2026
- Section heading "Sizing in millimeters is the important knob"
  shortened to "Sizing in millimeters".
- The "Explicit non-coverage in v1" sentence was wrong: text and
  clip-path are supported in this release and visible in the
  static-SVG fixture screenshot we ship in the post. Rewritten to
  call out that text and clipPath landed via PR #5056, show what
  is supported (<text> / <tspan> with single-style fills and
  transforms; <clipPath> via clip-path="url(#id)" against rect /
  circle / path clip shapes), and accurately list what is still
  not covered (filter primitives, mask with alpha, radialGradient,
  CSS-in-SVG with selector style rules).
shai-almog added a commit that referenced this pull request May 30, 2026
- Section heading "Sizing in millimeters is the important knob"
  shortened to "Sizing in millimeters".
- The "Explicit non-coverage in v1" sentence was wrong: text and
  clip-path are supported in this release and visible in the
  static-SVG fixture screenshot we ship in the post. Rewritten to
  call out that text and clipPath landed via PR #5056, show what
  is supported (<text> / <tspan> with single-style fills and
  transforms; <clipPath> via clip-path="url(#id)" against rect /
  circle / path clip shapes), and accurately list what is still
  not covered (filter primitives, mask with alpha, radialGradient,
  CSS-in-SVG with selector style rules).
shai-almog added a commit that referenced this pull request May 30, 2026
- Section heading "Sizing in millimeters is the important knob"
  shortened to "Sizing in millimeters".
- The "Explicit non-coverage in v1" sentence was wrong: text and
  clip-path are supported in this release and visible in the
  static-SVG fixture screenshot we ship in the post. Rewritten to
  call out that text and clipPath landed via PR #5056, show what
  is supported (<text> / <tspan> with single-style fills and
  transforms; <clipPath> via clip-path="url(#id)" against rect /
  circle / path clip shapes), and accurately list what is still
  not covered (filter primitives, mask with alpha, radialGradient,
  CSS-in-SVG with selector style rules).
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