Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -143,6 +151,7 @@ private void emitNode(SVGNode n, SVGStyle parentStyle) {
line("try {");
indent++;
line("g.setClip(" + clipVar + ");");
clipPathDepth++;
}
}

Expand All @@ -169,6 +178,7 @@ private void emitNode(SVGNode n, SVGStyle parentStyle) {
}
} finally {
if (clip != null) {
clipPathDepth--;
indent--;
line("} finally {");
indent++;
Expand Down Expand Up @@ -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];");
Expand All @@ -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("}");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,53 @@ public void linearGradientEmitsPaint() throws Exception {
+ "<stop offset='1' stop-color='blue'/>"
+ "</linearGradient></defs>"
+ "<rect x='0' y='0' width='10' height='10' fill='url(#g1)'/></svg>");
// 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 <rect> filled with a
// gradient and clipped by a rounded-rect <clipPath>. 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("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>"
+ "<defs>"
+ "<clipPath id='rounded'>"
+ "<rect x='10' y='10' width='80' height='80' rx='20' ry='20'/>"
+ "</clipPath>"
+ "<linearGradient id='g' x1='0' y1='0' x2='1' y2='1'>"
+ "<stop offset='0' stop-color='red'/>"
+ "<stop offset='1' stop-color='blue'/>"
+ "</linearGradient>"
+ "</defs>"
+ "<rect x='0' y='0' width='100' height='100' fill='url(#g)' clip-path='url(#rounded)'/>"
+ "</svg>");
// 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
Expand Down
Binary file modified scripts/android/screenshots/SVGStatic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
}
});
}
});
}
});
}
Expand Down
Binary file modified scripts/ios/screenshots-metal/LightweightPickerButtons.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified scripts/ios/screenshots-metal/SVGStatic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified scripts/ios/screenshots/SVGStatic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading