diff --git a/maven/svg-transcoder/src/main/java/com/codename1/svg/transcoder/codegen/JavaCodeGenerator.java b/maven/svg-transcoder/src/main/java/com/codename1/svg/transcoder/codegen/JavaCodeGenerator.java
index a92c7c7d43..7e4ae9ec26 100644
--- a/maven/svg-transcoder/src/main/java/com/codename1/svg/transcoder/codegen/JavaCodeGenerator.java
+++ b/maven/svg-transcoder/src/main/java/com/codename1/svg/transcoder/codegen/JavaCodeGenerator.java
@@ -27,6 +27,14 @@ public final class JavaCodeGenerator {
private final StringBuilder body = new StringBuilder();
private int indent = 2;
private int idSeq;
+ /// Depth of the currently active clip-path stack -- bumped on the way
+ /// into an emitNode that resolved its element's clip-path attr, popped
+ /// on the way out. Read by emitGradientFill to decide whether the
+ /// inner setClip(__p) is needed: an outer clip-path already constrains
+ /// the gradient to the visible region, and the inner setClip would
+ /// REPLACE the outer clip and wipe the rounded clipPath
+ /// (clipped_badge.svg) before the gradient runs.
+ private int clipPathDepth;
public JavaCodeGenerator(SVGDocument doc, String packageName, String className) {
this.doc = doc;
@@ -143,6 +151,7 @@ private void emitNode(SVGNode n, SVGStyle parentStyle) {
line("try {");
indent++;
line("g.setClip(" + clipVar + ");");
+ clipPathDepth++;
}
}
@@ -169,6 +178,7 @@ private void emitNode(SVGNode n, SVGStyle parentStyle) {
}
} finally {
if (clip != null) {
+ clipPathDepth--;
indent--;
line("} finally {");
indent++;
@@ -613,12 +623,25 @@ private void emitGradientFill(SVGPaint fill, AnimatedFloat opacity, AnimatedFloa
fracs.append("}");
cols.append("}");
- // Gradient fills on iOS Metal currently misrender because
- // setClip(non-rect Shape) substitutes a degenerate polygon for
- // arc-decomposed paths -- the gradient ends up shaped like a
- // triangle. That is being tracked as a Metal port bug; the
- // transcoder emits the simple setClip + LinearGradientPaint.paint
- // recipe and the screenshot goldens capture the current behavior.
+ // Confining the gradient: LinearGradientPaint.paint() rasterises
+ // bands wider than `__p`'s bounding box (Math.max(w, h) inset on
+ // both sides), so a clip is needed to keep the gradient inside
+ // the shape. The natural choice is `setClip(__p)`, but setClip
+ // REPLACES the current clip -- and when the element carries a
+ // clip-path attribute, the enclosing block already set the clip
+ // to the clipPath. setClip(__p) wipes the clipPath, and
+ // clipped_badge.svg's rounded outline ends up rendered as a
+ // sharp-cornered square. When we're already inside a clipPath
+ // block, that outer clip is more restrictive than __p (every
+ // SVG sample we ship and every authoring tool we've seen builds
+ // a clipPath that's a subset of the element it clips), so we
+ // can skip the inner clip and let the outer one constrain the
+ // gradient -- which is what the rounded badge needs to render.
+ // Outside a clipPath block we still need the inner setClip(__p)
+ // to confine the gradient to non-rectangular shapes
+ // (gradient_circle.svg would bleed past the circle into the
+ // bounding-rect corners without it).
+ boolean outerClipActive = clipPathDepth > 0;
line("{");
indent++;
line("float[] __b = new float[4];");
@@ -644,17 +667,21 @@ private void emitGradientFill(SVGPaint fill, AnimatedFloat opacity, AnimatedFloa
+ "MultipleGradientPaint.ColorSpaceType.SRGB, "
+ "Transform.makeIdentity());");
line("g.setAlpha(" + alphaExpression(opacity, elementOpacity, 0xFF) + ");");
- line("g.pushClip();");
- line("try {");
- indent++;
- line("g.setClip(__p);");
- line("__paint.paint(g, __bx, __by, __bw, __bh);");
- indent--;
- line("} finally {");
- indent++;
- line("g.popClip();");
- indent--;
- line("}");
+ if (outerClipActive) {
+ line("__paint.paint(g, __bx, __by, __bw, __bh);");
+ } else {
+ line("g.pushClip();");
+ line("try {");
+ indent++;
+ line("g.setClip(__p);");
+ line("__paint.paint(g, __bx, __by, __bw, __bh);");
+ indent--;
+ line("} finally {");
+ indent++;
+ line("g.popClip();");
+ indent--;
+ line("}");
+ }
indent--;
line("}");
}
diff --git a/maven/svg-transcoder/src/test/java/com/codename1/svg/transcoder/codegen/JavaCodeGeneratorTest.java b/maven/svg-transcoder/src/test/java/com/codename1/svg/transcoder/codegen/JavaCodeGeneratorTest.java
index 1b53a28cea..c1b4bca468 100644
--- a/maven/svg-transcoder/src/test/java/com/codename1/svg/transcoder/codegen/JavaCodeGeneratorTest.java
+++ b/maven/svg-transcoder/src/test/java/com/codename1/svg/transcoder/codegen/JavaCodeGeneratorTest.java
@@ -115,13 +115,53 @@ public void linearGradientEmitsPaint() throws Exception {
+ ""
+ ""
+ "");
- // Gradient fills use the setClip(path) + LinearGradientPaint.paint
- // recipe. iOS Metal currently misrenders these (substitutes a
- // degenerate polygon clip) -- see SVG-Transcoder.asciidoc; the
- // Metal port bug is being tracked separately and the screenshot
- // goldens capture the platform-current behavior.
+ // Standalone gradient (no enclosing clip-path) keeps the existing
+ // setClip(__p) inner clamp: LinearGradientPaint.paint rasterises
+ // bands wider than __p's bounding box and would bleed without it.
assertTrue(out.contains("new LinearGradientPaint("));
assertTrue(out.contains("CycleMethod.NO_CYCLE"));
+ assertTrue("standalone gradient must keep the inner setClip(__p): " + out,
+ out.contains("g.setClip(__p);"));
+ }
+
+ @Test
+ public void gradientInsideClipPathSkipsInnerClip() throws Exception {
+ // clipped_badge.svg-style structure: a filled with a
+ // gradient and clipped by a rounded-rect . The
+ // generator must NOT emit the inner setClip(__p) inside the
+ // clipPath block -- that setClip REPLACES the outer rounded
+ // clip and the badge would render as a sharp-cornered square
+ // on every port. Outer clip alone constrains the gradient.
+ String out = transcode("");
+ // The outer clip-path block IS emitted.
+ assertTrue("expected outer clip-path setClip block: " + out,
+ out.contains("g.setClip(__clip0);"));
+ // The gradient runs but with NO inner setClip / pushClip /
+ // popClip wrapping it. We assert paint(g, ...) appears without
+ // an inner setClip(__p) anywhere in the file -- the only
+ // setClip should be the outer one.
+ assertTrue("expected paint(g, __bx, __by, __bw, __bh): " + out,
+ out.contains("__paint.paint(g, __bx, __by, __bw, __bh);"));
+ // Count setClip occurrences -- exactly 1 (the outer clipPath).
+ int setClipCount = 0;
+ int idx = 0;
+ while ((idx = out.indexOf("g.setClip(", idx)) != -1) {
+ setClipCount++;
+ idx++;
+ }
+ assertTrue("expected exactly one setClip call (the outer clipPath); found " + setClipCount
+ + " in:\n" + out, setClipCount == 1);
}
@Test
diff --git a/scripts/android/screenshots/SVGStatic.png b/scripts/android/screenshots/SVGStatic.png
index 60fdc265f9..3965b44927 100644
Binary files a/scripts/android/screenshots/SVGStatic.png and b/scripts/android/screenshots/SVGStatic.png differ
diff --git a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LightweightPickerButtonsScreenshotTest.java b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LightweightPickerButtonsScreenshotTest.java
index 18505b5d8c..ad0e9ccda0 100644
--- a/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LightweightPickerButtonsScreenshotTest.java
+++ b/scripts/hellocodenameone/common/src/main/java/com/codenameone/examples/hellocodenameone/tests/LightweightPickerButtonsScreenshotTest.java
@@ -155,18 +155,56 @@ public void run() {
// showing, so the wheels would stay on "today" and the
// screenshot diff would fail any day other than April 11.
picker.setDate(fixedDate);
+ // setDate -> currentSpinner.setValue rebuilds the wheels'
+ // ListModels and snaps each scroller to the new selected
+ // index, but neither setListModel nor the snap calls
+ // revalidate() on the popup container. Without an explicit
+ // pump, the wheels reach the right scrollY but the
+ // surrounding tick rows are still laid out for the
+ // pre-setDate model (visible as off-by-one row offsets in
+ // the screenshot diff) until the next user interaction.
+ // PopupButtonActionListener does this revalidate+repaint
+ // for button-driven setDate paths; the screenshot test
+ // calls setDate directly so we have to drive it here.
+ Form current = Display.getInstance().getCurrent();
+ if (current != null) {
+ current.revalidate();
+ current.repaint();
+ }
// Second wait: give the wheels a couple of frames to settle
- // at the new month/day/year before snapping the PNG.
+ // at the new month/day/year before the capture chain below.
UITimer.timer(400, false, form, new Runnable() {
@Override
public void run() {
- Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshot(variant.imageName, new Runnable() {
+ // Three nested callSerially hops before capture so
+ // at least three EDT paint cycles (and on iOS Metal
+ // three CAMetalLayer presents) land between the
+ // settle timer and cn1_captureView's
+ // afterScreenUpdates:NO grab. Same pattern as
+ // DualAppearanceBaseTest where the previous frame's
+ // pixels were lingering in the front buffer.
+ Display.getInstance().callSerially(new Runnable() {
@Override
public void run() {
- picker.stopEditing(new Runnable() {
+ Display.getInstance().callSerially(new Runnable() {
@Override
public void run() {
- runVariantsFrom(index + 1);
+ Display.getInstance().callSerially(new Runnable() {
+ @Override
+ public void run() {
+ Cn1ssDeviceRunnerHelper.emitCurrentFormScreenshot(variant.imageName, new Runnable() {
+ @Override
+ public void run() {
+ picker.stopEditing(new Runnable() {
+ @Override
+ public void run() {
+ runVariantsFrom(index + 1);
+ }
+ });
+ }
+ });
+ }
+ });
}
});
}
diff --git a/scripts/ios/screenshots-metal/LightweightPickerButtons.png b/scripts/ios/screenshots-metal/LightweightPickerButtons.png
index b832921691..a15691252b 100644
Binary files a/scripts/ios/screenshots-metal/LightweightPickerButtons.png and b/scripts/ios/screenshots-metal/LightweightPickerButtons.png differ
diff --git a/scripts/ios/screenshots-metal/SVGStatic.png b/scripts/ios/screenshots-metal/SVGStatic.png
index 2d5ac70ffb..def9b4b8d3 100644
Binary files a/scripts/ios/screenshots-metal/SVGStatic.png and b/scripts/ios/screenshots-metal/SVGStatic.png differ
diff --git a/scripts/ios/screenshots/SVGStatic.png b/scripts/ios/screenshots/SVGStatic.png
index 7e96b7d9b5..91e92c3deb 100644
Binary files a/scripts/ios/screenshots/SVGStatic.png and b/scripts/ios/screenshots/SVGStatic.png differ