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