diff --git a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java index a9cc64b2b8..f6184553d2 100644 --- a/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java +++ b/CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java @@ -801,7 +801,19 @@ public void paintDirty() { continue; } paintQueueTemp[iter] = null; - wrapper.translate(-wrapper.getTranslateX(), -wrapper.getTranslateY()); + // Reset wrapper graphics to a clean state before painting + // the next queued animation. matrixFrameworkTranslateX + // returns the right anchor (xTranslate in legacy mode, + // matrixFrameworkX in matrix mode) so the translate-based + // reset brings the wrapper to identity in both modes. + // resetAffine() then clears any leftover user + // scale/rotate; in matrix mode it also re-applies the + // framework anchor it had just zeroed, but since we + // already translate(-tx, -ty)'d to zero it stays at + // identity for the upcoming paintComponent. + int wtx = wrapper.matrixFrameworkTranslateX(); + int wty = wrapper.matrixFrameworkTranslateY(); + wrapper.translate(-wtx, -wty); wrapper.resetAffine(); wrapper.setClip(0, 0, dwidth, dheight); if (ani instanceof Component) { diff --git a/CodenameOne/src/com/codename1/maps/MapComponent.java b/CodenameOne/src/com/codename1/maps/MapComponent.java index 61e7921332..4964e9f82b 100644 --- a/CodenameOne/src/com/codename1/maps/MapComponent.java +++ b/CodenameOne/src/com/codename1/maps/MapComponent.java @@ -33,6 +33,7 @@ import com.codename1.ui.Graphics; import com.codename1.ui.Image; import com.codename1.ui.ImageFactory; +import com.codename1.ui.Transform; import com.codename1.ui.events.ActionEvent; import com.codename1.ui.events.ActionListener; import com.codename1.ui.geom.Dimension; @@ -269,11 +270,23 @@ public void paintBackground(Graphics g) { tx = -tx * (float) (scaleX - getWidth()) / sx; float ty = (float) zoomCenterY / (float) getHeight(); ty = -ty * (float) (scaleY - getHeight()) / sy; - g.translate((int) tx, (int) ty); - g.scale(sx, sy); - paintmap(g); - g.resetAffine(); - g.translate(-(int) tx, -(int) ty); + if (Graphics.useMatrixTranslation) { + // Matrix mode: resetAffine wipes the impl matrix to + // identity, which destroys the framework painting-chain + // translates the matrix carries. Use save/restore the + // impl matrix around the scale + user-translate instead. + Transform savedMatrix = g.getTransform(); + g.translate((int) tx, (int) ty); + g.scale(sx, sy); + paintmap(g); + g.setTransform(savedMatrix); + } else { + g.translate((int) tx, (int) ty); + g.scale(sx, sy); + paintmap(g); + g.resetAffine(); + g.translate(-(int) tx, -(int) ty); + } } else { g.translate(-translateX, -translateY); paintmap(g); diff --git a/CodenameOne/src/com/codename1/ui/Component.java b/CodenameOne/src/com/codename1/ui/Component.java index 368e0ec7e3..eda3e8578f 100644 --- a/CodenameOne/src/com/codename1/ui/Component.java +++ b/CodenameOne/src/com/codename1/ui/Component.java @@ -3031,9 +3031,12 @@ void internalPaintImpl(Graphics g, boolean paintIntersects) { public void paintIntersectingComponentsAbove(Graphics g) { Container parent = getParent(); Component component = this; - int tx = g.getTranslateX(); - int ty = g.getTranslateY(); - + // Snapshot-reset translate -- matrixFrameworkTranslateX returns + // xTranslate in legacy mode and the matrixFrameworkX shadow in + // matrix mode so the parent walk below paints each ancestor at + // its screen-absolute position. + int tx = g.matrixFrameworkTranslateX(); + int ty = g.matrixFrameworkTranslateY(); g.translate(-tx, -ty); int x1 = getAbsoluteX() + getScrollX(); int y1 = getAbsoluteY() + getScrollY(); @@ -3056,7 +3059,6 @@ public void paintIntersectingComponentsAbove(Graphics g) { parent = parent.getParent(); } g.translate(tx, ty); - } /// Paints the UI for the scrollbars on the component, this will be invoked only @@ -3344,8 +3346,8 @@ private void drawPainters(Graphics g, Component par, Component c, paintBackgroundImpl(tg); putClientProperty("$FLAT", i); } - int tx = g.getTranslateX(); - int ty = g.getTranslateY(); + int tx = g.matrixFrameworkTranslateX(); + int ty = g.matrixFrameworkTranslateY(); g.translate(-tx + absX, -ty + absY); g.drawImage(i, 0, 0); g.translate(tx - absX, ty - absY); diff --git a/CodenameOne/src/com/codename1/ui/Container.java b/CodenameOne/src/com/codename1/ui/Container.java index 00e5d163be..fc46c46e48 100644 --- a/CodenameOne/src/com/codename1/ui/Container.java +++ b/CodenameOne/src/com/codename1/ui/Container.java @@ -2186,8 +2186,12 @@ public void paint(Graphics g) { paintElevatedPane(g); } - int tx = g.getTranslateX(); - int ty = g.getTranslateY(); + // Snapshot-reset translate so glass/tensile draw at screen- + // absolute coords. matrixFrameworkTranslateX returns the right + // anchor for both modes (legacy: xTranslate; matrix mode: + // matrixFrameworkX shadow). + int tx = g.matrixFrameworkTranslateX(); + int ty = g.matrixFrameworkTranslateY(); g.translate(-tx, -ty); if (sidemenuBarTranslation > 0) { g.translate(sidemenuBarTranslation, 0); diff --git a/CodenameOne/src/com/codename1/ui/FontImage.java b/CodenameOne/src/com/codename1/ui/FontImage.java index e1bf2b0c7a..22d4c7e4b6 100644 --- a/CodenameOne/src/com/codename1/ui/FontImage.java +++ b/CodenameOne/src/com/codename1/ui/FontImage.java @@ -7796,8 +7796,8 @@ protected void drawImage(Graphics g, Object nativeGraphics, int x, int y) { g.concatenateAlpha(fgAlpha); } if (rotated != 0) { - int tX = g.getTranslateX(); - int tY = g.getTranslateY(); + int tX = g.matrixFrameworkTranslateX(); + int tY = g.matrixFrameworkTranslateY(); g.translate(-tX, -tY); g.rotate((float) Math.toRadians(rotated % 360), tX + x + width / 2, tY + y + height / 2); g.drawString(text, tX + x + width / 2 - w / 2, tY + y + height / 2 - h / 2); @@ -7837,8 +7837,8 @@ protected void drawImage(Graphics g, Object nativeGraphics, int x, int y, int w, } //int paddingPixels = Display.getInstance().convertToPixels(padding, true); if (rotated != 0) { - int tX = g.getTranslateX(); - int tY = g.getTranslateY(); + int tX = g.matrixFrameworkTranslateX(); + int tY = g.matrixFrameworkTranslateY(); g.translate(-tX, -tY); g.rotate((float) Math.toRadians(rotated % 360), tX + x + w / 2, tY + y + h / 2); g.drawString(text, tX + x + w / 2 - ww / 2, tY + y); diff --git a/CodenameOne/src/com/codename1/ui/Form.java b/CodenameOne/src/com/codename1/ui/Form.java index b2137c6785..74d448cd75 100644 --- a/CodenameOne/src/com/codename1/ui/Form.java +++ b/CodenameOne/src/com/codename1/ui/Form.java @@ -1117,8 +1117,13 @@ void paintGlassImpl(Graphics g) { return; } if (glassPane != null) { - int tx = g.getTranslateX(); - int ty = g.getTranslateY(); + // matrixFrameworkTranslateX returns xTranslate in legacy mode + // and the matrixFrameworkX shadow in matrix mode; the translate- + // based snapshot reset brings the impl matrix to identity in + // both modes so glassPane.paint draws at the form's screen + // coords via getBounds(). + int tx = g.matrixFrameworkTranslateX(); + int ty = g.matrixFrameworkTranslateY(); g.translate(-tx, -ty); glassPane.paint(g, getBounds()); g.translate(tx, ty); diff --git a/CodenameOne/src/com/codename1/ui/Graphics.java b/CodenameOne/src/com/codename1/ui/Graphics.java index 9224ead05c..4cef3343d3 100644 --- a/CodenameOne/src/com/codename1/ui/Graphics.java +++ b/CodenameOne/src/com/codename1/ui/Graphics.java @@ -50,6 +50,31 @@ public final class Graphics { /// /// - #getRenderingHints() public static final int RENDERING_HINT_FAST = 1; + /// When true, `#translate(int, int)` composes the translation onto the + /// impl-side affine matrix (via `#translateMatrix(float, float)`) instead + /// of accumulating it in a per-Graphics integer offset that's added to + /// every draw coordinate. Off by default for backwards compatibility with + /// apps that relied on the legacy "translate-then-scale multiplies the + /// translate" behavior. When on, `g.translate(...)` composes into the + /// same matrix as `g.scale` / `g.rotate`, so the container/component + /// painting chain (status bar offset, title-area offset, scroll position, + /// etc.) flows through the matrix and a user-issued `g.scale(...)` no + /// longer stretches those component-positioning translates. + /// + /// The flag is global because translates are issued by the framework + /// painting pipeline before user paint() runs; setting it per-Graphics + /// would race with that. Flip it once during app init. + /// + /// Only honored on ports where `CodenameOneImplementation + /// #isTranslateMatrixSupported()` returns true (iOS, Android, JavaSE, + /// HTML5 today). On ports that opt out, `g.translate` keeps using the + /// integer accumulator regardless of this flag. + /// + /// #### See also + /// + /// - #translateMatrix(float, float) + /// - #isTranslateMatrixSupported() + public static boolean useMatrixTranslation; private final CodenameOneImplementation impl; /// Flag that specifies that native peers are rendered "behind" the this /// graphics context. The main difference is that drawPeerComponent() will @@ -67,6 +92,22 @@ public final class Graphics { /// of any prior g.translate(). getTransform() returns this original /// (un-conjugated) matrix. private Transform userTransform; + /// Matrix-mode shadow of the framework painting-chain's accumulated + /// translate. Maintained by `#translate(int, int)` so `#setTransform` + /// can conjugate the user transform around the framework origin -- + /// `impl matrix = T(matrixFrameworkX, matrixFrameworkY) * userTransform` + /// -- matching the legacy `T(xt) * M * T(-xt)` recipe. Snapshot-reset + /// callers read it via `#matrixFrameworkTranslateX()`/`Y()` to do the + /// translate-based "bring matrix to identity then restore" dance the + /// legacy code does with `getTranslateX()`. + /// + /// Intentionally NOT exposed via `#getTranslateX()` -- bypass-Graphics + /// fast paths (Label.drawLabelComponent, BGPainter + /// .paintComponentBackground) read getTranslateX to compute coords for + /// direct impl calls, and in matrix mode the impl matrix already + /// encodes the translate so any addition would double-shift. + private int matrixFrameworkX; + private int matrixFrameworkY; private GeneralPath tmpClipShape; /// A buffer shape to use when we need to transform a shape private int color; @@ -104,6 +145,69 @@ private Transform translation() { return translation; } + /// True iff the global `#useMatrixTranslation` opt-in is on *and* this + /// Graphics' impl actually routes `#translate(int, int)` through the + /// matrix. Cached at zero cost; both reads are simple field/virtual + /// dispatches. All call sites that decide between "matrix already has + /// the translate, don't shift coords" and "legacy accumulator path, + /// shift coords by xTranslate" gate on this. + private boolean matrixMode() { + return useMatrixTranslation && impl.isTranslateMatrixSupported(); + } + + /// Framework-internal accessor returning the current accumulated + /// framework painting-chain translate. In legacy mode this is the + /// `xTranslate` integer accumulator; in matrix mode it is the + /// `matrixFrameworkX` shadow that mirrors the impl matrix translate. + /// Snapshot-reset callers in Form.paintGlassImpl, + /// Container.paintComponent tail, Component.paintIntersectingComponents- + /// Above, drawPainters $FLAT, TextSelection, FontImage rotation, + /// Graphics.beginNativeGraphicsAccess, and the + /// CodenameOneImplementation paint-queue wrapper use this for the + /// translate-based "bring matrix to identity for screen-absolute + /// drawing, restore" pattern -- the legacy recipe works in both modes + /// because matrix-mode g.translate composes onto the impl matrix and + /// matrixFrameworkX/Y stays in sync. + /// + /// `getTranslateX()` stays at zero in matrix mode -- bypass-Graphics + /// paths read that and would double-shift if it leaked the matrix + /// anchor. This separate getter is for framework callers that + /// explicitly want the screen offset. + public int matrixFrameworkTranslateX() { + // Legacy mode: delegate to getTranslateX so this is a strict + // no-op behavioural change vs. the master callsites that used + // getTranslateX directly. On ports whose impl maintains its own + // translate accumulator (iOS GL etc.) getTranslateX reads from + // impl, not the Java-side `xTranslate` -- returning xTranslate + // here would give zero and the snapshot-reset translate(-tx,-ty) + // would no-op, leaving the wrapper at the parent's coords. + if (matrixMode()) { + return matrixFrameworkX; + } + return getTranslateX(); + } + + /// Y-axis counterpart to `#matrixFrameworkTranslateX()`. + public int matrixFrameworkTranslateY() { + if (matrixMode()) { + return matrixFrameworkY; + } + return getTranslateY(); + } + + /// Returns the x coordinate to pass to impl draw primitives. In matrix + /// mode the impl-side affine already encodes the translate, so we hand + /// the impl the raw local-coord x. In legacy mode we add the shadow + /// xTranslate accumulator the way every drawing method historically has. + private int tx(int x) { + return matrixMode() ? x : (xTranslate + x); + } + + /// Y-axis counterpart to `#tx(int)`. + private int ty(int y) { + return matrixMode() ? y : (yTranslate + y); + } + private GeneralPath tmpClipShape() { if (tmpClipShape == null) { tmpClipShape = new GeneralPath(); @@ -142,19 +246,40 @@ void setGraphics(Object g) { public void translate(int x, int y) { if (impl.isTranslationSupported()) { impl.translate(nativeGraphics, x, y); - } else { - xTranslate += x; - yTranslate += y; - // The conjugation in setTransform() depends on the current - // xTranslate/yTranslate. If the user accumulated more - // translation after setting a non-identity transform, - // re-conjugate so the impl-side matrix stays in sync. + return; + } + if (matrixMode()) { + // Matrix-only: compose T(x, y) onto the impl-side matrix. The + // public-facing xTranslate/yTranslate stays at zero -- + // bypass-Graphics paths read getTranslateX and would double- + // shift. The internal matrixFrameworkX/Y shadow tracks the + // same framework anchor so setTransform's conjugation and + // snapshot-reset callsites via matrixFrameworkTranslateX have + // a consistent reference. If userTransform is set the impl + // matrix needs to stay at T(matrixFrameworkX) * userTransform; + // rebuild it here to absorb the new translate without + // duplicating the user transform. + matrixFrameworkX += x; + matrixFrameworkY += y; + impl.translateMatrix(nativeGraphics, x, y); if (userTransform != null) { - Transform composed = Transform.makeTranslation(xTranslate, yTranslate); + Transform composed = Transform.makeTranslation(matrixFrameworkX, matrixFrameworkY); composed.concatenate(userTransform); - composed.translate(-xTranslate, -yTranslate); impl.setTransform(nativeGraphics, composed); } + return; + } + xTranslate += x; + yTranslate += y; + // The conjugation in setTransform() depends on the current + // xTranslate/yTranslate. If the user accumulated more + // translation after setting a non-identity transform, + // re-conjugate so the impl-side matrix stays in sync. + if (userTransform != null) { + Transform composed = Transform.makeTranslation(xTranslate, yTranslate); + composed.concatenate(userTransform); + composed.translate(-xTranslate, -yTranslate); + impl.setTransform(nativeGraphics, composed); } } @@ -166,9 +291,15 @@ public void translate(int x, int y) { public int getTranslateX() { if (impl.isTranslationSupported()) { return impl.getTranslateX(nativeGraphics); - } else { - return xTranslate; } + // In matrix mode the integer accumulator is intentionally never + // bumped (the impl matrix is the single source of truth). Returning + // zero here is correct as the "amount you would add to a local + // coord to get the screen-absolute coord that drawing primitives + // *currently apply on top of*" -- which is zero, because drawing + // primitives no longer add it. Callers that need the actual + // screen offset (snapshot-reset patterns) must use getTransform(). + return xTranslate; } /// Returns the current y translate value @@ -179,9 +310,8 @@ public int getTranslateX() { public int getTranslateY() { if (impl.isTranslationSupported()) { return impl.getTranslateY(nativeGraphics); - } else { - return yTranslate; } + return yTranslate; } /// Returns the current color @@ -287,6 +417,14 @@ public void setFont(Font font) { /// /// the x clipping position public int getClipX() { + if (matrixMode()) { + // In matrix mode the impl pulls the clip's inverse-transformed + // bounds (see iOS NativeGraphics.loadClipBounds), which is + // already in matrix-local user coords. The legacy '- xTranslate' + // would double-subtract the translate that's now baked into the + // impl matrix. + return impl.getClipX(nativeGraphics); + } return impl.getClipX(nativeGraphics) - xTranslate; } @@ -366,7 +504,11 @@ public void setClip(int[] clip) { /// /// - #isShapeClipSupported public void setClip(Shape shape) { - if (xTranslate != 0 || yTranslate != 0) { + // Matrix mode: impl.setClip(Shape) applies the current impl transform + // to the shape itself (iOS NativeGraphics.setClip stores + // clip.setShape(newClip, transform)), so a manual pre-translate here + // would double-apply the translate. + if (!matrixMode() && (xTranslate != 0 || yTranslate != 0)) { GeneralPath p = tmpClipShape(); p.setShape(shape, translation()); shape = p; @@ -380,6 +522,9 @@ public void setClip(Shape shape) { /// /// the y clipping position public int getClipY() { + if (matrixMode()) { + return impl.getClipY(nativeGraphics); + } return impl.getClipY(nativeGraphics) - yTranslate; } @@ -414,7 +559,7 @@ public int getClipHeight() { /// /// - `height`: the height of the rectangle to intersect the clip with public void clipRect(int x, int y, int width, int height) { - impl.clipRect(nativeGraphics, xTranslate + x, yTranslate + y, width, height); + impl.clipRect(nativeGraphics, tx(x), ty(y), width, height); } /// Updates the clipping region to match the given region exactly @@ -429,7 +574,7 @@ public void clipRect(int x, int y, int width, int height) { /// /// - `height`: the height of the new clip rectangle. public void setClip(int x, int y, int width, int height) { - impl.setClip(nativeGraphics, xTranslate + x, yTranslate + y, width, height); + impl.setClip(nativeGraphics, tx(x), ty(y), width, height); } /// Pushes the current clip onto the clip stack. It can later be restored @@ -455,7 +600,7 @@ public void popClip() { /// /// - `y2`: second y position public void drawLine(int x1, int y1, int x2, int y2) { - impl.drawLine(nativeGraphics, xTranslate + x1, yTranslate + y1, xTranslate + x2, yTranslate + y2); + impl.drawLine(nativeGraphics, tx(x1), ty(y1), tx(x2), ty(y2)); } @@ -472,14 +617,14 @@ public void drawLine(int x1, int y1, int x2, int y2) { /// /// - `height`: the height of the rectangle to be filled. public void fillRect(int x, int y, int width, int height) { - impl.fillRect(nativeGraphics, xTranslate + x, yTranslate + y, width, height); + impl.fillRect(nativeGraphics, tx(x), ty(y), width, height); } /// #### Deprecated /// /// this method should have been internals public void drawShadow(Image img, int x, int y, int offsetX, int offsetY, int blurRadius, int spreadRadius, int color, float opacity) { - impl.drawShadow(nativeGraphics, img.getImage(), xTranslate + x, yTranslate + y, offsetX, offsetY, blurRadius, spreadRadius, color, opacity); + impl.drawShadow(nativeGraphics, img.getImage(), tx(x), ty(y), offsetX, offsetY, blurRadius, spreadRadius, color, opacity); } /// Clears rectangular area of the graphics context. This will remove any color @@ -504,7 +649,7 @@ public void drawShadow(Image img, int x, int y, int offsetX, int offsetY, int bl /// /// - `height`: The height of the box to clear. public void clearRect(int x, int y, int width, int height) { - clearRectImpl(xTranslate + x, yTranslate + y, width, height); + clearRectImpl(tx(x), ty(y), width, height); } /// Clears rectangular area of the graphics context. This will remove any color @@ -544,7 +689,7 @@ private void clearRectImpl(int x, int y, int width, int height) { /// /// - `height`: the height of the rectangle to be drawn. public void drawRect(int x, int y, int width, int height) { - impl.drawRect(nativeGraphics, xTranslate + x, yTranslate + y, width, height); + impl.drawRect(nativeGraphics, tx(x), ty(y), width, height); } /// Draws a rectangle in the given coordinates with the given thickness @@ -561,7 +706,7 @@ public void drawRect(int x, int y, int width, int height) { /// /// - `thickness`: the thickness in pixels public void drawRect(int x, int y, int width, int height, int thickness) { - impl.drawRect(nativeGraphics, xTranslate + x, yTranslate + y, width, height, thickness); + impl.drawRect(nativeGraphics, tx(x), ty(y), width, height, thickness); } /// Draws a rounded corner rectangle in the given coordinates with the arcWidth/height @@ -581,7 +726,7 @@ public void drawRect(int x, int y, int width, int height, int thickness) { /// /// - `arcHeight`: the vertical diameter of the arc at the four corners. public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { - impl.drawRoundRect(nativeGraphics, xTranslate + x, yTranslate + y, width, height, arcWidth, arcHeight); + impl.drawRoundRect(nativeGraphics, tx(x), ty(y), width, height, arcWidth, arcHeight); } /// Makes the current color slightly lighter, this is useful for many visual effects @@ -636,7 +781,7 @@ public void darkerColor(int factor) { /// /// - #drawRoundRect public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { - impl.fillRoundRect(nativeGraphics, xTranslate + x, yTranslate + y, width, height, arcWidth, arcHeight); + impl.fillRoundRect(nativeGraphics, tx(x), ty(y), width, height, arcWidth, arcHeight); } /// Fills a circular or elliptical arc based on the given angles and bounding @@ -683,7 +828,7 @@ public void fillArc(int x, int y, int width, int height, int startAngle, int arc if (width < 1 || height < 1) { throw new IllegalArgumentException("Width & Height of fillAsrc must be greater than 0"); } - impl.fillArc(nativeGraphics, xTranslate + x, yTranslate + y, width, height, startAngle, arcAngle); + impl.fillArc(nativeGraphics, tx(x), ty(y), width, height, startAngle, arcAngle); } /// Draws a circular or elliptical arc based on the given angles and bounding @@ -703,7 +848,7 @@ public void fillArc(int x, int y, int width, int height, int startAngle, int arc /// /// - `arcAngle`: the angular extent of the arc, relative to the start angle. public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { - impl.drawArc(nativeGraphics, xTranslate + x, yTranslate + y, width, height, startAngle, arcAngle); + impl.drawArc(nativeGraphics, tx(x), ty(y), width, height, startAngle, arcAngle); } /// Draw a string using the current font and color in the x,y coordinates. The font is drawn @@ -731,7 +876,7 @@ public void drawString(String str, int x, int y, int textDecoration) { if (current instanceof CustomFont) { current.drawString(this, str, x, y); } else { - impl.drawString(nativeGraphics, nativeFont, str, x + xTranslate, y + yTranslate, textDecoration); + impl.drawString(nativeGraphics, nativeFont, str, tx(x), ty(y), textDecoration); } } @@ -869,17 +1014,17 @@ public void drawImage(Image img, int x, int y, int w, int h) { void drawImageWH(Object nativeImage, int x, int y, int w, int h) { - impl.drawImage(nativeGraphics, nativeImage, x + xTranslate, y + yTranslate, w, h); + impl.drawImage(nativeGraphics, nativeImage, tx(x), ty(y), w, h); } void drawImage(Object img, int x, int y) { - impl.drawImage(nativeGraphics, img, x + xTranslate, y + yTranslate); + impl.drawImage(nativeGraphics, img, tx(x), ty(y)); } /// Draws an image with a MIDP trasnform for fast rotation void drawImage(Object img, int x, int y, int transform) { if (transform != 0) { - impl.drawImageRotated(nativeGraphics, img, x + xTranslate, y + yTranslate, transform); + impl.drawImageRotated(nativeGraphics, img, tx(x), ty(y), transform); } else { drawImage(img, x, y); } @@ -939,7 +1084,9 @@ void drawImage(Object img, int x, int y, int transform) { /// - #isShapeSupported public void drawShape(Shape shape, Stroke stroke) { if (isShapeSupported()) { - if (xTranslate != 0 || yTranslate != 0) { + // Matrix mode: impl applies the affine to shape vertices, so the + // legacy pre-translate would double-shift. + if (!matrixMode() && (xTranslate != 0 || yTranslate != 0)) { GeneralPath p = tmpClipShape(); p.setShape(shape, translation()); shape = p; @@ -1003,7 +1150,7 @@ public void fillShape(Shape shape) { int clipH = getClipHeight(); setClip(shape); clipRect(clipX, clipY, clipW, clipH); - if (xTranslate != 0 || yTranslate != 0) { + if (!matrixMode() && (xTranslate != 0 || yTranslate != 0)) { GeneralPath p = tmpClipShape(); p.setShape(shape, translation()); shape = p; @@ -1014,7 +1161,7 @@ public void fillShape(Shape shape) { return; } - if (xTranslate != 0 || yTranslate != 0) { + if (!matrixMode() && (xTranslate != 0 || yTranslate != 0)) { GeneralPath p = tmpClipShape(); p.setShape(shape, translation()); shape = p; @@ -1126,8 +1273,18 @@ public Transform getTransform() { if (userTransform != null) { return userTransform.copy(); } + if (matrixMode()) { + // Return identity, NOT impl.getTransform() -- the impl matrix + // in matrix mode carries T(matrixFrameworkX), and returning it + // here would cause snapshot/restore patterns + // (saved = getTransform(); setTransform(saved)) to feed the + // framework translates back into the conjugation step, double- + // translating on restore. Legacy returns impl.getTransform() + // which is identity by default since the framework chain lives + // in xTranslate -- same effective answer. + return Transform.makeIdentity(); + } return impl.getTransform(nativeGraphics); - } /// Sets the transformation `com.codename1.ui.geom.Matrix` to apply to drawing in this graphics context. @@ -1167,6 +1324,38 @@ public void setTransform(Transform transform) { // off-screen. Conjugate the user's matrix with T(xTranslate, // yTranslate) so its effect is independent of any prior g.translate // call, matching Android Skia / JavaSE Graphics2D semantics. + if (matrixMode()) { + // Matrix-mode conjugation: in legacy mode the impl matrix is + // T(xt) * M * T(-xt) so M applies in local coords around xt; + // in matrix mode the impl matrix already encodes the framework + // chain AND drawing primitives don't pre-shift coords, so the + // equivalent composition is just T(matrixFrameworkX) * M (no + // terminal T(-matrixFrameworkX)). Drawing at local (lx, ly) + // then lands at T(matrixFrameworkX) * M * (lx, ly) -- same + // screen result as legacy. + // + // setTransform(null) / setTransform(identity) restores the + // framework matrix T(matrixFrameworkX, matrixFrameworkY), NOT + // a true identity -- otherwise subsequent paint calls would + // land at screen origin instead of the component's screen + // position. Snapshot-reset patterns that need a true identity + // matrix use translate-based reset via + // matrixFrameworkTranslateX (see the framework callsites + // catalogued in Form.paintGlassImpl etc.). + if (transform == null || transform.isIdentity()) { + userTransform = null; + impl.resetAffine(nativeGraphics); + if (matrixFrameworkX != 0 || matrixFrameworkY != 0) { + impl.translateMatrix(nativeGraphics, matrixFrameworkX, matrixFrameworkY); + } + } else { + userTransform = transform.copy(); + Transform composed = Transform.makeTranslation(matrixFrameworkX, matrixFrameworkY); + composed.concatenate(transform); + impl.setTransform(nativeGraphics, composed); + } + return; + } if (transform != null && !transform.isIdentity() && (xTranslate != 0 || yTranslate != 0)) { userTransform = transform.copy(); @@ -1190,6 +1379,11 @@ public void getTransform(Transform t) { t.setTransform(userTransform); return; } + if (matrixMode()) { + // See `#getTransform()` -- return identity, not impl matrix. + t.setIdentity(); + return; + } impl.getTransform(nativeGraphics, t); } @@ -1213,7 +1407,7 @@ public void getTransform(Transform t) { /// /// - `y3`: the y coordinate of the third vertex of the triangle public void fillTriangle(int x1, int y1, int x2, int y2, int x3, int y3) { - impl.fillTriangle(nativeGraphics, xTranslate + x1, yTranslate + y1, xTranslate + x2, yTranslate + y2, xTranslate + x3, yTranslate + y3); + impl.fillTriangle(nativeGraphics, tx(x1), ty(y1), tx(x2), ty(y2), tx(x3), ty(y3)); } /// Draws the RGB values based on the MIDP API of a similar name. Renders a @@ -1246,7 +1440,7 @@ public void fillTriangle(int x1, int y1, int x2, int y2, int x3, int y3) { /// - `processAlpha`: @param processAlpha true if rgbData has an alpha channel, false if /// all pixels are fully opaque void drawRGB(int[] rgbData, int offset, int x, int y, int w, int h, boolean processAlpha) { - impl.drawRGB(nativeGraphics, rgbData, offset, x + xTranslate, y + yTranslate, w, h, processAlpha); + impl.drawRGB(nativeGraphics, rgbData, offset, tx(x), ty(y), w, h, processAlpha); } /// Draws a radial gradient in the given coordinates with the given colors, @@ -1268,7 +1462,7 @@ void drawRGB(int[] rgbData, int offset, int x, int y, int w, int h, boolean proc /// /// - `height`: the height of the region to be filled public void fillRadialGradient(int startColor, int endColor, int x, int y, int width, int height) { - impl.fillRadialGradient(nativeGraphics, startColor, endColor, x + xTranslate, y + yTranslate, width, height); + impl.fillRadialGradient(nativeGraphics, startColor, endColor, tx(x), ty(y), width, height); } /// Draws a radial gradient in the given coordinates with the given colors, @@ -1294,7 +1488,7 @@ public void fillRadialGradient(int startColor, int endColor, int x, int y, int w /// /// - `arcAngle`: the angular extent of the arc, relative to the start angle. Positive angles are counter-clockwise. public void fillRadialGradient(int startColor, int endColor, int x, int y, int width, int height, int startAngle, int arcAngle) { - impl.fillRadialGradient(nativeGraphics, startColor, endColor, x + xTranslate, y + yTranslate, width, height, startAngle, arcAngle); + impl.fillRadialGradient(nativeGraphics, startColor, endColor, tx(x), ty(y), width, height, startAngle, arcAngle); } /// Draws a radial gradient in the given coordinates with the given colors, @@ -1330,7 +1524,7 @@ public void fillRectRadialGradient(int startColor, int endColor, int x, int y, i fillRect(x, y, width, height, (byte) 0xff); return; } - impl.fillRectRadialGradient(nativeGraphics, startColor, endColor, x + xTranslate, y + yTranslate, width, height, relativeX, relativeY, relativeSize); + impl.fillRectRadialGradient(nativeGraphics, startColor, endColor, tx(x), ty(y), width, height, relativeX, relativeY, relativeSize); } /// Draws a linear gradient in the given coordinates with the given colors, @@ -1358,7 +1552,7 @@ public void fillLinearGradient(int startColor, int endColor, int x, int y, int w fillRect(x, y, width, height, (byte) 0xff); return; } - impl.fillLinearGradient(nativeGraphics, startColor, endColor, x + xTranslate, y + yTranslate, width, height, horizontal); + impl.fillLinearGradient(nativeGraphics, startColor, endColor, tx(x), ty(y), width, height, horizontal); } /// Fills the rectangle (x, y, width, height) with the given multi-stop @@ -1408,7 +1602,7 @@ public boolean blurRegion(int x, int y, int width, int height, float radius) { /// /// - `alpha`: the alpha values specify semitransparency public void fillRect(int x, int y, int w, int h, byte alpha) { - impl.fillRect(nativeGraphics, x + xTranslate, y + yTranslate, w, h, alpha); + impl.fillRect(nativeGraphics, tx(x), ty(y), w, h, alpha); } /// Fills a closed polygon defined by arrays of x and y coordinates. @@ -1426,7 +1620,8 @@ public void fillPolygon(int[] xPoints, int nPoints) { int[] cX = xPoints; int[] cY = yPoints; - if ((!impl.isTranslationSupported()) && (xTranslate != 0 || yTranslate != 0)) { + if ((!impl.isTranslationSupported()) && !matrixMode() + && (xTranslate != 0 || yTranslate != 0)) { cX = new int[nPoints]; cY = new int[nPoints]; System.arraycopy(xPoints, 0, cX, 0, nPoints); @@ -1473,7 +1668,8 @@ void drawImageArea(Image img, int x, int y, int imageX, int imageY, int imageWid public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { int[] cX = xPoints; int[] cY = yPoints; - if ((!impl.isTranslationSupported()) && (xTranslate != 0 || yTranslate != 0)) { + if ((!impl.isTranslationSupported()) && !matrixMode() + && (xTranslate != 0 || yTranslate != 0)) { cX = new int[nPoints]; cY = new int[nPoints]; System.arraycopy(xPoints, 0, cX, 0, nPoints); @@ -1625,9 +1821,25 @@ public boolean isAffineSupported() { return impl.isAffineSupported(); } - /// Resets the affine transform to the default value + /// Resets the affine transform to the default value. + /// + /// In matrix-translation mode this wipes the impl matrix to identity, + /// including the framework painting-chain translates the matrix carries. + /// Callers that need to preserve those translates across a transform + /// reset (e.g. user paint snippets in MapComponent / Scene / + /// CommonTransitions / FontImage rotation) must save the impl matrix + /// via `#getTransform(com.codename1.ui.Transform)` first and restore + /// it via `#setTransform(com.codename1.ui.Transform)` after. public void resetAffine() { impl.resetAffine(nativeGraphics); + if (matrixMode() && (matrixFrameworkX != 0 || matrixFrameworkY != 0)) { + // Preserve the framework painting-chain anchor that the impl + // matrix carries in matrix mode -- otherwise subsequent draws + // land at screen origin instead of the component's position. + // Equivalent to legacy semantics where resetAffine wipes the + // impl matrix to identity while xTranslate stays. + impl.translateMatrix(nativeGraphics, matrixFrameworkX, matrixFrameworkY); + } scaleX = 1; scaleY = 1; userTransform = null; @@ -1777,6 +1989,14 @@ public void rotate(float angle, int pivotX, int pivotY) { /// /// 6.0 public void rotateRadians(float angle, int pivotX, int pivotY) { + if (matrixMode()) { + // Matrix mode: the impl matrix already encodes the translate, so + // the pivot lives in matrix-local coords. The legacy '+ + // xTranslate' offset compensated for the per-Graphics integer + // accumulator, which doesn't apply here. + impl.rotate(nativeGraphics, angle, pivotX, pivotY); + return; + } impl.rotate(nativeGraphics, angle, pivotX + xTranslate, pivotY + yTranslate); } @@ -1814,9 +2034,15 @@ public Object beginNativeGraphicsAccess() { a = Boolean.TRUE; } + // Snapshot the framework anchor (xTranslate in legacy mode, + // matrixFrameworkX in matrix mode) and zero it via translate so + // the native graphics is handed over at screen-absolute origin in + // both modes. + int tx = matrixFrameworkTranslateX(); + int ty = matrixFrameworkTranslateY(); nativeGraphicsState = new Object[]{ - Integer.valueOf(getTranslateX()), - Integer.valueOf(getTranslateY()), + Integer.valueOf(tx), + Integer.valueOf(ty), Integer.valueOf(getColor()), Integer.valueOf(getAlpha()), Integer.valueOf(getClipX()), @@ -1825,7 +2051,7 @@ public Object beginNativeGraphicsAccess() { Integer.valueOf(getClipHeight()), a, b }; - translate(-getTranslateX(), -getTranslateY()); + translate(-tx, -ty); setAlpha(255); setClip(0, 0, Display.getInstance().getDisplayWidth(), Display.getInstance().getDisplayHeight()); return nativeGraphics; @@ -1888,7 +2114,7 @@ public void tileImage(Image img, int x, int y, int w, int h) { } setClip(clipX, clipY, clipW, clipH); } else { - impl.tileImage(nativeGraphics, img.getImage(), x + xTranslate, y + yTranslate, w, h); + impl.tileImage(nativeGraphics, img.getImage(), tx(x), ty(y), w, h); } } @@ -1920,7 +2146,23 @@ public float getScaleY() { /// - `peer`: The peer component to be drawn. void drawPeerComponent(PeerComponent peer) { if (paintPeersBehind) { - clearRectImpl(peer.getAbsoluteX(), peer.getAbsoluteY(), peer.getWidth(), peer.getHeight()); + // clearRectImpl forwards to impl.clearRect which honours the + // impl matrix on iOS (applyTransform before clearing). In + // matrix mode the matrix encodes the framework painting-chain + // translates -- passing screen-absolute peer coords would + // double-translate the punch-hole, so the native peer would + // appear as a solid filled rect over the canvas at the wrong + // screen position. Subtract matrixFrameworkX so the impl + // matrix lands the cleared rect at the peer's screen coords. + // Legacy mode keeps the raw absolute coords (matrix is + // identity, no shift applied). + int absX = peer.getAbsoluteX(); + int absY = peer.getAbsoluteY(); + if (matrixMode()) { + absX -= matrixFrameworkX; + absY -= matrixFrameworkY; + } + clearRectImpl(absX, absY, peer.getWidth(), peer.getHeight()); } } diff --git a/CodenameOne/src/com/codename1/ui/TextSelection.java b/CodenameOne/src/com/codename1/ui/TextSelection.java index c3edab883e..6d92457da3 100644 --- a/CodenameOne/src/com/codename1/ui/TextSelection.java +++ b/CodenameOne/src/com/codename1/ui/TextSelection.java @@ -1316,8 +1316,11 @@ public void paint(Graphics g) { g.setColor(0x0000ff); int alph = g.getAlpha(); g.setAlpha(50); - int tx = g.getTranslateX(); - int ty = g.getTranslateY(); + // Snapshot-reset translate so subsequent translate(originX, + // originY) lands at selectionRoot's screen-absolute origin in + // both modes. + int tx = g.matrixFrameworkTranslateX(); + int ty = g.matrixFrameworkTranslateY(); g.translate(-tx, -ty); int originX = selectionRoot.getAbsoluteX(); int originY = selectionRoot.getAbsoluteY(); diff --git a/CodenameOne/src/com/codename1/ui/animations/CommonTransitions.java b/CodenameOne/src/com/codename1/ui/animations/CommonTransitions.java index 9e834a156e..3d66ce7fcc 100644 --- a/CodenameOne/src/com/codename1/ui/animations/CommonTransitions.java +++ b/CodenameOne/src/com/codename1/ui/animations/CommonTransitions.java @@ -33,6 +33,7 @@ import com.codename1.ui.Image; import com.codename1.ui.Painter; import com.codename1.ui.RGBImage; +import com.codename1.ui.Transform; import com.codename1.ui.plaf.Style; import com.codename1.util.LazyValue; @@ -949,6 +950,13 @@ public void paint(Graphics g) { Component c = getDialogParent(getDestination()); float ratio = ((float) position) / 1000.0f; if (g.isAffineSupported()) { + // Matrix mode: resetAffine would clobber the + // framework painting-chain translates. Save the + // pre-scale matrix and restore it after drawing. + Transform savedTransitionMatrix = null; + if (Graphics.useMatrixTranslation) { + savedTransitionMatrix = g.getTransform(); + } g.scale(ratio, ratio); int w = (int) (originalWidth * ratio); int h = (int) (originalHeight * ratio); @@ -960,7 +968,11 @@ public void paint(Graphics g) { g.drawImage(buffer, currentDlgX, currentDlgY); //paint(g, c, 0, 0); - g.resetAffine(); + if (Graphics.useMatrixTranslation) { + g.setTransform(savedTransitionMatrix); + } else { + g.resetAffine(); + } } else { c.setWidth((int) (originalWidth * ratio)); c.setHeight((int) (originalHeight * ratio)); diff --git a/CodenameOne/src/com/codename1/ui/scene/Scene.java b/CodenameOne/src/com/codename1/ui/scene/Scene.java index c93de4062b..45a0cb49be 100644 --- a/CodenameOne/src/com/codename1/ui/scene/Scene.java +++ b/CodenameOne/src/com/codename1/ui/scene/Scene.java @@ -25,6 +25,7 @@ import com.codename1.properties.Property; import com.codename1.ui.Container; import com.codename1.ui.Graphics; +import com.codename1.ui.Transform; /// A scene graph. Supports 3D on platforms where `com.codename1.ui.Transform#isPerspectiveSupported()` is true (iOS and Android currently). /// @@ -64,7 +65,17 @@ public void setRoot(Node root) { public void paint(Graphics g) { super.paint(g); if (root != null) { - g.resetAffine(); + // In matrix mode resetAffine would wipe the framework painting- + // chain translates that the impl matrix carries. Save/restore + // the impl matrix around the render call instead; the legacy + // path keeps using resetAffine since the integer accumulator + // already preserves the framework translate. + Transform savedMatrix = null; + if (Graphics.useMatrixTranslation) { + savedMatrix = g.getTransform(); + } else { + g.resetAffine(); + } int clipX = g.getClipX(); int clipY = g.getClipY(); int clipW = g.getClipWidth(); @@ -73,7 +84,11 @@ public void paint(Graphics g) { g.setAntiAliased(true); root.render(g); g.translate(-getX(), -getY()); - g.resetAffine(); + if (Graphics.useMatrixTranslation) { + g.setTransform(savedMatrix); + } else { + g.resetAffine(); + } g.setClip(clipX, clipY, clipW, clipH); } } diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidAsyncView.java b/Ports/Android/src/com/codename1/impl/android/AndroidAsyncView.java index d0f7798dbc..5ff28962f6 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidAsyncView.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidAsyncView.java @@ -759,6 +759,40 @@ public String toString() { }); } + @Override + public void translateMatrix(final float x, final float y) { + // Mirror the scale / rotate / setTransform overrides above: + // update the AsyncGraphics local transform AND queue an op so + // the underlying canvas's matrix is updated at playback time. + // Without this override, Graphics.useMatrixTranslation routes + // every framework g.translate(absX, absY) through + // impl.translateMatrix -- which inherits AndroidGraphics's + // local-mutation-only translateMatrix -- so the queued draw + // ops paint with the underlying canvas matrix that never saw + // any of the framework painting-chain translates, leaving the + // form chrome (title bar, theme buttons, etc.) painting at + // screen origin with no offset. + getTransform().translate(x, y); + transformDirty = true; + inverseTransformDirty = true; + clipFresh = false; + pendingRenderingOperations.add(new AsyncOp(clip, clipP, clipIsPath) { + @Override + public void execute(AndroidGraphics underlying) { + underlying.translateMatrix(x, y); + } + + @Override + public void executeWithClip(AndroidGraphics underlying) { + execute(underlying); + } + + public String toString() { + return "translateMatrix"; + } + }); + } + @Override public void resetAffine() { getTransform().setIdentity(); diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java index be2d4688c2..cb6e9d50ce 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java @@ -4620,14 +4620,23 @@ public void paint(final Graphics g) { if(superPeerMode) { Object nativeGraphics = com.codename1.ui.Accessor.getNativeGraphics(g); + // Use matrixFrameworkTranslateX -- returns g.getTranslateX + // in legacy mode and matrixFrameworkX (which mirrors the + // framework painting-chain translate that lives in the + // impl matrix) in matrix mode. The plain getTranslateX + // is intentionally zero in matrix mode and would place + // the peer at the component's *local* coords instead of + // screen-absolute. + int peerScreenX = getX() + g.matrixFrameworkTranslateX(); + int peerScreenY = getY() + g.matrixFrameworkTranslateY(); Object o = v.getLayoutParams(); AndroidAsyncView.LayoutParams lp; if(o instanceof AndroidAsyncView.LayoutParams) { lp = (AndroidAsyncView.LayoutParams) o; if (lp == null) { lp = new AndroidAsyncView.LayoutParams( - getX() + g.getTranslateX(), - getY() + g.getTranslateY(), + peerScreenX, + peerScreenY, getWidth(), getHeight(), AndroidPeer.this); final AndroidAsyncView.LayoutParams finalLp = lp; @@ -4639,8 +4648,8 @@ public void run() { }); lp.dirty = true; } else { - int x = getX() + g.getTranslateX(); - int y = getY() + g.getTranslateY(); + int x = peerScreenX; + int y = peerScreenY; int w = getWidth(); int h = getHeight(); if (x != lp.x || y != lp.y || w != lp.w || h != lp.h) { @@ -4653,8 +4662,8 @@ public void run() { } } else { final AndroidAsyncView.LayoutParams finalLp = new AndroidAsyncView.LayoutParams( - getX() + g.getTranslateX(), - getY() + g.getTranslateY(), + peerScreenX, + peerScreenY, getWidth(), getHeight(), AndroidPeer.this); activity.runOnUiThread(new Runnable() { diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/BufferedGraphics.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/BufferedGraphics.java index 536b76553a..68d09c8cfe 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/BufferedGraphics.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/BufferedGraphics.java @@ -238,6 +238,29 @@ public void scale(double sx, double sy) { applyTransform(); } + @Override + public void translateMatrix(double tx, double ty) { + // Master added Graphics.translateMatrix in commit 826d60f32 / the + // InscribedTriangleGrid test; the framework dispatches to + // HTML5Implementation.translateMatrix which delegates to + // ((HTML5Graphics) graphics).translateMatrix(...). Without this + // override BufferedGraphics inherits HTML5Graphics's translateMatrix, + // which mutates the parent class's `transform` field -- a + // *different* field from the one BufferedGraphics's own + // scale/rotate/etc. overrides use. The result: translateMatrix on + // the form's graphics silently no-ops as far as queued ops are + // concerned, which under the new matrix-mode Graphics layer + // collapses every framework painting-chain translate to (0,0) on + // the form's main canvas -- Toolbar titles, chart-pie etc. + // Override here so the BufferedGraphics-side `transform` field + // receives the composition and the next applyTransform() submits + // a SetTransform op carrying the right matrix. + if (transform == null) transform = Transform.makeIdentity(); + transform.translate((float)tx, (float)ty); + setTransformChanged(); + applyTransform(); + } + //@Override //public void shear(double shx, double shy) { // setTransform(JSAffineTransform.Factory.getShearInstance(shx, shy), false); diff --git a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java index f776937442..7d3732ae1c 100644 --- a/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java +++ b/Ports/JavaScriptPort/src/main/java/com/codename1/impl/html5/HTML5Implementation.java @@ -2183,6 +2183,22 @@ public void setGraphicsLocked(boolean locked) { } CanvasRenderingContext2D context = (CanvasRenderingContext2D)outputCanvas.getContext("2d"); context.save(); + // Reset to identity BEFORE the crop clip is set. Without this, if + // a prior drain ended with a non-identity transform on the canvas + // state (e.g. ClipShape's setTransform leftover that the outer + // save/restore preserves across drains, or the matrix-mode + // T(framework_anchor) the new Graphics path now stashes there), + // the `rect(cropX, cropY, cropW, cropH); clip();` below evaluates + // under that leaked transform -- the resulting clip is a + // rotated/scaled/translated polygon, not the intended axis- + // aligned crop. Ops in this drain then paint UNDER the leaked + // transform AND through the wrong clip, producing missing + // Toolbar/title regions (the chart-line, chart-pie and chart- + // rotated-pie JS regressions). Force identity now; the per-op + // SetTransform queue inside this drain still sets the per-paint + // transform, and the outer `restore()` pops back to whatever + // pre-drain state was active. + context.setTransform(1, 0, 0, 1, 0, 0); context.beginPath(); context.rect(frame.getCropX(), frame.getCropY(), frame.getCropW(), frame.getCropH()); context.clip(); diff --git a/Ports/iOSPort/nativeSources/CN1Metalcompat.m b/Ports/iOSPort/nativeSources/CN1Metalcompat.m index ec09e2d8df..648ebf6db5 100644 --- a/Ports/iOSPort/nativeSources/CN1Metalcompat.m +++ b/Ports/iOSPort/nativeSources/CN1Metalcompat.m @@ -1248,6 +1248,18 @@ void CN1MetalDrawAlphaMaskRadial(id texture, static int savedScreenFw = 0; static int savedScreenFh = 0; static uint32_t savedScreenStencilReference = 0; +// currentTransform is a GLOBAL set by every SetTransform ExecutableOp's +// execute() (via CN1MetalSetTransform). When draining a mutable +// side-trip, the mutable target's SetTransform ops mutate this same +// global; without saving/restoring it here, subsequent screen-target +// ops on the drain queue read the mutable's last transform instead of +// the screen's own framework_anchor. This shows up dramatically once +// matrix-mode g.translate routes every framework translate through +// SetTransform: Switches, FABs and other components whose first paint +// lazily builds a mutable image drift off-screen, become invisible, +// or stack at the wrong y because the screen draw that follows the +// mutable build uses the mutable's coord system. +static simd_float4x4 savedScreenTransform; static BOOL savedScreenStateValid = NO; // Build a Y-down ortho projection for an offscreen (w x h) framebuffer. @@ -1456,6 +1468,7 @@ BOOL CN1MetalBeginMutableImageDraw(GLUIImage *image) { savedScreenFw = currentFramebufferWidth; savedScreenFh = currentFramebufferHeight; savedScreenStencilReference = currentStencilReference; + savedScreenTransform = currentTransform; savedScreenStateValid = YES; activeEncoder = enc; @@ -1494,6 +1507,13 @@ void CN1MetalEndMutableImageDraw(GLUIImage *image) { // pre-detour polygon clip's writes are still distinguishable // from a fresh post-detour clip (see Begin's note). currentStencilReference = savedScreenStencilReference; + // Restore the screen-side render transform. The mutable side- + // trip's SetTransform ops mutated this global; leaving it as the + // mutable's last transform would make every screen-target draw + // queued after this Begin/End pair (e.g. Switch's + // g.drawImage(track,...) following its lazy track-image build) + // pick up the mutable's coord system instead of the screen's. + currentTransform = savedScreenTransform; savedScreenEncoder = nil; savedScreenStateValid = NO; } diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h index 5513a36ad0..b2b6370ba6 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.h @@ -238,6 +238,27 @@ MFMessageComposeViewControllerDelegate, CLLocationManagerDelegate, AVAudioRecord -(BOOL)isPaintFinished; -(void)flushBuffer:(UIImage *)buff x:(int)x y:(int)y width:(int)width height:(int)height; +#ifdef CN1_USE_METAL +// Drain only the ExecutableOps queued against `image` (target == image), +// leaving every other op untouched in the upcoming queue. Opens a fresh +// mutable encoder for the image, executes the extracted ops against it, +// and commits -- so a follow-up CN1MetalFlushMutableImageSync(image) call +// can waitUntilCompleted on the buffer and read back actual pixels. +// +// Why not just call flushBuffer here? flushBuffer drains the *entire* +// upcoming queue, including the form's SetTransform / Draw ops, then +// drawFrame's CN1MetalBeginFrame resets the global currentTransform to +// identity at the start of the *next* drawFrame. The form's +// NativeGraphics.transformApplied flag stays true through that round- +// trip, so the next form draw queues with no preceding SetTransform op +// and executes against currentTransform=identity at the next drain -- +// every Switch / FAB / etc. whose first paint blurs a mutable thumb +// lands at screen (local_x, local_y). The targeted drain keeps the +// form's ops in the queue exactly where they were, so they drain in +// order against the correct currentTransform. +-(void)flushOpsForMutableImage:(GLUIImage*)image; +#endif + -(void)drawString:(int)color alpha:(int)alpha font:(UIFont*)font str:(NSString*)str x:(int)x y:(int)y; - (void)drawScreen; - (void)drawFrame:(CGRect)rect; diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m index e2bca4ecb0..6efa746060 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m @@ -3708,6 +3708,72 @@ -(void)flushBuffer:(UIImage *)buff x:(int)x y:(int)y width:(int)width height:(in }*/ } +#ifdef CN1_USE_METAL +-(void)flushOpsForMutableImage:(GLUIImage*)image { + if (image == nil) return; + dispatch_sync(dispatch_get_main_queue(), ^{ + // Partition the upcoming queue: ops targeting `image` get + // extracted (they're what we need to execute right now so a + // follow-up readback sees the painted pixels); everything else + // -- the form's SetTransform / Draw ops, ops for OTHER mutable + // images, ClipShape state -- stays in the queue and drains at + // the next normal drawFrame. The global flushBuffer used to + // drain everything; that left the form's NativeGraphics. + // transformApplied flag stale relative to the Metal currentTransform + // (which CN1MetalBeginFrame resets to identity each frame), so + // the form's follow-up drawImage / drawString lacked a preceding + // SetTransform op and rendered at currentTransform=identity at + // the next drain. Visible: Switch's track/thumb lands at screen + // (local_x, local_y) in matrix mode. + NSMutableArray *opsForImage = [[NSMutableArray alloc] init]; + NSMutableArray *remainingOps = [[NSMutableArray alloc] init]; + @synchronized([CodenameOne_GLViewController instance]) { + for (ExecutableOp *op in self->upcomingTarget) { + if ([op target] == image) { + [opsForImage addObject:op]; + } else { + [remainingOps addObject:op]; + } + } + [self->upcomingTarget setArray:remainingOps]; + } + if ([opsForImage count] == 0) { +#ifndef CN1_USE_ARC + [opsForImage release]; + [remainingOps release]; +#endif + return; + } + // Open a fresh mutable encoder for this image. Begin saves the + // current screen state (encoder/projection/framebuffer/stencil/ + // transform) so the followup End can restore -- so even though + // we're mid-paint and currentTransform may carry a leftover + // form/mutable matrix from a prior drain, the side-trip ends + // with currentTransform back where it was. + BOOL encoderOpen = CN1MetalBeginMutableImageDraw(image); + if (!encoderOpen) { +#ifndef CN1_USE_ARC + [opsForImage release]; + [remainingOps release]; +#endif + return; + } + for (ExecutableOp *op in opsForImage) { + [op executeWithClipping]; + } + CN1MetalEndMutableImageDraw(image); + // Now the mutable's MTLCommandBuffer is committed; the caller + // (typically gausianBlurImage in IOSNative.m) can call + // CN1MetalFlushMutableImageSync(image) to waitUntilCompleted + // before reading back the texture. +#ifndef CN1_USE_ARC + [opsForImage release]; + [remainingOps release]; +#endif + }); +} +#endif + -(void)drawString:(int)color alpha:(int)alpha font:(UIFont*)font str:(NSString*)str x:(int)x y:(int)y { POOL_BEGIN(); UIColor* col = UIColorFromRGB(color,alpha); diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index 28117fdfbf..4efd276010 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -809,9 +809,27 @@ JAVA_LONG com_codename1_impl_ios_IOSNative_gausianBlurImage___long_float(CN1_THR UIImage* original = nil; #ifdef CN1_USE_METAL if ([glu mtlMutableTexture] != nil) { - extern int displayWidth; - extern int displayHeight; - [[CodenameOne_GLViewController instance] flushBuffer:nil x:0 y:0 width:displayWidth height:displayHeight]; + // Drain any pending ExecutableOps targeting THIS specific mutable + // image so the GPU executes the shadow-ring fillArc calls before + // CN1MetalReadMutableImageAsUIImage samples the texture. + // + // Previously this called the global flushBuffer, which drained the + // entire upcoming queue (form ops too) and triggered a full + // drawFrame mid-paint. That worked for the readback but left + // form-side NativeGraphics state (transformApplied / clipApplied) + // stale relative to the iOS Metal globals, so the next form draw + // queued without a preceding SetTransform op and rendered at + // currentTransform=identity at the *next* drawFrame -- + // visible as Switch's track/thumb landing at screen (local_x, + // local_y) instead of the component's screen position + // (matrix-mode SwitchTheme / kotlin failures). + // + // flushOpsForMutableImage only walks the queue and executes the + // ops whose target == glu, leaving form ops in place. The form's + // SetTransform / Draw ops stay in upcomingTarget so the next + // drawFrame drains them in their original order against the + // correct currentTransform. + [[CodenameOne_GLViewController instance] flushOpsForMutableImage:glu]; original = CN1MetalReadMutableImageAsUIImage(glu); } if (original == nil) { diff --git a/maven/core-unittests/spotbugs-exclude.xml b/maven/core-unittests/spotbugs-exclude.xml index acfc0cca06..1356d1deee 100644 --- a/maven/core-unittests/spotbugs-exclude.xml +++ b/maven/core-unittests/spotbugs-exclude.xml @@ -112,6 +112,25 @@ + + + + + + + + + + + +