Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
53b463e
hellocodenameone: fix graphics screenshot tests for scale and perspec…
shai-almog May 7, 2026
b04af90
hellocodenameone: rewrite perspective/camera viewport math for visibl…
shai-almog May 7, 2026
0204a73
hellocodenameone: project perspective/camera corners via transformPoint
shai-almog May 7, 2026
39502b7
iOS port: invalidate clip caches on impl.setTransform
shai-almog May 7, 2026
ae52375
cn1ss: tag every emitted PNG with FNV-1a 64 hash and detect duplicates
shai-almog May 7, 2026
360ca72
cn1ss: avoid HashMap static-init that breaks iOS class loading
shai-almog May 7, 2026
3f12fc9
hellocodenameone: refresh Android goldens for the four updated tests
shai-almog May 7, 2026
f65c05d
cn1ss: use negative lookahead in INFO-line regexes to avoid prefix match
shai-almog May 7, 2026
1115e78
run-ios-ui-tests: include fallback-decoded tests in compare set
shai-almog May 7, 2026
4c19316
hellocodenameone: promote iOS Metal goldens for transform-perspective…
shai-almog May 7, 2026
277ce81
Transform: mark composed transform dirty after copyTransform
shai-almog May 7, 2026
26f9f6b
Graphics.setTransform: conjugate user matrix with xTranslate/yTranslate
shai-almog May 8, 2026
5d355ab
PostPrComment: retry preview push with rebase on race-condition reject
shai-almog May 8, 2026
59274ba
Gate setTransform xTranslate-conjugation behind impl opt-in
shai-almog May 8, 2026
dc6b18e
Revert "Gate setTransform xTranslate-conjugation behind impl opt-in"
shai-almog May 8, 2026
271ae25
Revert "Graphics.setTransform: conjugate user matrix with xTranslate/…
shai-almog May 8, 2026
45b7ee5
hellocodenameone: render Scale/AffineScale gradients via mutable Image
shai-almog May 8, 2026
5f4c9f9
Revert "hellocodenameone: render Scale/AffineScale gradients via muta…
shai-almog May 8, 2026
512eba0
Graphics.setTransform: conjugate user matrix uniformly across iOS/And…
shai-almog May 8, 2026
1d94caf
hellocodenameone: cover ChartComponent + opt JS port into setTransfor…
shai-almog May 9, 2026
0873b6b
hellocodenameone: fix two chart-test runtime errors flagged by Androi…
shai-almog May 9, 2026
19a2e2b
hellocodenameone: unstick Android/JS chart pipelines + promote 11 And…
shai-almog May 9, 2026
3ddad10
charts: drop xTranslate from Canvas.rotate centre
shai-almog May 9, 2026
2ecfff5
charts: replace em-dash with ASCII in Canvas.rotate comment
shai-almog May 9, 2026
b11d702
charts: add diagnostic logging + promote iOS round-chart goldens
shai-almog May 9, 2026
a718b5e
charts: add CN1SS:DBG enter/bounds/exit logging in XYChart.draw
shai-almog May 9, 2026
beb795c
DIAGNOSTIC: turn off labels/legend/grid/axes in chart-line
shai-almog May 9, 2026
e17c424
DIAGNOSTIC: empty dataset in chart-line to isolate drawSeries
shai-almog May 9, 2026
79a0155
charts: revert diagnostic logging + chart-line test config
shai-almog May 9, 2026
46c0dc7
charts: clip ChartComponent.paint to component bounds (fix iOS blank)
shai-almog May 9, 2026
d9952d4
charts: collapse redundant moveTos in drawPath to fix iOS blank
shai-almog May 9, 2026
a95fc70
DIAGNOSTIC: switch chart Paint default join from BEVEL to MITER
shai-almog May 9, 2026
006435b
Revert "DIAGNOSTIC: switch chart Paint default join from BEVEL to MITER"
shai-almog May 9, 2026
e3da378
DIAGNOSTIC: native CN1SS:DBG logs in Metal alpha-mask + present path
shai-almog May 10, 2026
3942fd2
DIAGNOSTIC: log large CN1MetalFillRect calls
shai-almog May 10, 2026
6ef4eb7
DIAGNOSTIC: log mutable-image target on alpha-mask op queue
shai-almog May 10, 2026
64980c5
DIAGNOSTIC: log ChartComponent.paint entry every frame
shai-almog May 10, 2026
d2917a2
charts: re-queue ChartComponent on paint queue to fix iOS blank
shai-almog May 10, 2026
37f184e
Revert "charts: re-queue ChartComponent on paint queue to fix iOS blank"
shai-almog May 10, 2026
e56c060
hellocodenameone: standalone repro for iOS large-stroke dirty-clip bug
shai-almog May 10, 2026
d8e988e
hellocodenameone: refine LargeStrokeDirtyClipTest to drive dirty hear…
shai-almog May 10, 2026
8926797
hellocodenameone: simplify LargeStrokeDirtyClipTest, remove ticker
shai-almog May 10, 2026
1b000bd
hellocodenameone: LargeStrokeDirtyClipTest with two drawShape calls
shai-almog May 10, 2026
2795934
hellocodenameone: match chart-line path coords + BEVEL stroke join
shai-almog May 10, 2026
d743b06
hellocodenameone: match ChartComponent.paint flow precisely
shai-almog May 10, 2026
4c8a2f7
hellocodenameone: add XYChart margin-fillRect repro to LargeStroke test
shai-almog May 10, 2026
635bc3a
iOS port: log + stack trace whenever Graphics alpha is clobbered to 0
shai-almog May 10, 2026
4e3f8b4
Fix chart blank-render: restore alpha after each chart Canvas draw
shai-almog May 10, 2026
b86be3a
iOS UI tests: add chart screenshot baselines (GL + Metal)
shai-almog May 10, 2026
fedf01c
Drop isSetTransformTranslationConjugationRequired() + diagnostic NSLogs
shai-almog May 10, 2026
3512866
iOS UI tests: refresh graphics-* screenshot baselines
shai-almog May 10, 2026
21a9e13
LargeStrokeDirtyClipTest: use relative coords + refresh chart goldens
shai-almog May 11, 2026
bf07fd2
JS port: emit chunk indices as byte offsets, not sequential counts
shai-almog May 11, 2026
027a44f
hellocodenameone: add JavaScript chart + LargeStroke screenshot basel…
shai-almog May 11, 2026
e6c7fef
hellocodenameone: add relative-coords graphics-large-stroke goldens
shai-almog May 11, 2026
e254bec
hellocodenameone: add Android chart-transform golden
shai-almog May 11, 2026
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
11 changes: 8 additions & 3 deletions CodenameOne/src/com/codename1/charts/ChartComponent.java
Original file line number Diff line number Diff line change
Expand Up @@ -288,12 +288,17 @@ public void paint(Graphics g) {
g.getTransform(tmpTransform);

if (currentTransform == null) {
currentTransform = Transform.makeTranslation(getAbsoluteX(), getAbsoluteY());
currentTransform = Transform.makeIdentity();
} else {
currentTransform.setTranslation(getAbsoluteX(), getAbsoluteY());
currentTransform.setIdentity();
}
currentTransform.concatenate(transform);
currentTransform.translate(-getAbsoluteX(), -getAbsoluteY());
// Earlier this conjugated `transform` with T(absX, absY) to
// compensate for the xTranslate/yTranslate the platform was
// adding to vertex coords. Graphics.setTransform() now performs
// that conjugation uniformly across iOS / Android / JavaSE, so
// doing it manually here would shift the chart by 2*absX,
// 2*absY. Pass the user's transform through unchanged.

g.setTransform(currentTransform);
} else {
Expand Down
172 changes: 106 additions & 66 deletions CodenameOne/src/com/codename1/charts/compat/Canvas.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,34 @@ public void getClipBounds(Rectangle mRect) {
}


private void applyPaint(Paint paint) {
applyPaint(paint, false);
private int applyPaint(Paint paint) {
return applyPaint(paint, false);
}


private void applyPaint(Paint paint, boolean forText) {
//Log.p("Applyingn paint : "+paint);
/// Applies the chart-package Paint (color + alpha + optional typeface)
/// onto `g` and returns the alpha value that was on `g` before this call.
///
/// The return value is critical: callers MUST pass it to `g.setAlpha(...)`
/// after each draw. `Graphics.concatenateAlpha(int)` is *multiplicative*
/// -- alpha = oldAlpha * newAlpha / 255 -- and the chart's grid-line color
/// is `argb(75, 200, 200, 200)`, so a single grid-line draw collapses
/// alpha to 75/255 ~ 29%. After three or four grid-line draws alpha
/// underflows to 0, and every later draw on the same Graphics is invisible
/// (paint-color alpha 255 short-circuits in concatenateAlpha so it can
/// never recover from 0). On iOS Metal+GL this manifested as XYChart
/// screenshots rendering as 45927-byte blank PNGs while RoundChart
/// (pie/doughnut/radar) -- whose paint chain runs through paths that hit
/// `g.setAlpha(...)` directly -- rendered fine.
///
/// Restoring alpha to the entry value after each draw keeps each
/// `drawXxx` operation alpha-isolated, which is the contract Android's
/// real `Canvas` already enforces (its `Paint.alpha` is per-draw, not
/// accumulated). Other CN1 ports masked the cascade because their
/// renderers don't latch alpha across draws the way the iOS GL/Metal
/// op-queue does, but the bug was always present at the API level.
private int applyPaint(Paint paint, boolean forText) {
int oldAlpha = g.getAlpha();
g.setColor(paint.getColor());
int alpha = ColorUtil.alpha(paint.getColor());
g.concatenateAlpha(alpha);
Expand All @@ -92,39 +113,44 @@ private void applyPaint(Paint paint, boolean forText) {
g.setFont(null);
}
}


return oldAlpha;
}


public void drawRect(float left, float top, float right, float bottom, Paint paint) {
applyPaint(paint);
Paint.Style style = paint.getStyle();
if (Paint.Style.FILL.equals(style)) {
//Log.p("Filling it");
g.fillRect((int) left, (int) top, (int) right - (int) left, (int) bottom - (int) top);
} else if (Paint.Style.STROKE.equals(style)) {
g.drawRect((int) left, (int) top, (int) right - (int) left, (int) bottom - (int) top);
} else if (Paint.Style.FILL_AND_STROKE.equals(style)) {
g.fillRect((int) left, (int) top, (int) right - (int) left, (int) bottom - (int) top);
//g.drawRect((int)left+bounds.getX(), (int)top+bounds.getY(), (int)right-(int)left, (int)bottom-(int)top);
int oldAlpha = applyPaint(paint);
try {
Paint.Style style = paint.getStyle();
if (Paint.Style.FILL.equals(style)) {
//Log.p("Filling it");
g.fillRect((int) left, (int) top, (int) right - (int) left, (int) bottom - (int) top);
} else if (Paint.Style.STROKE.equals(style)) {
g.drawRect((int) left, (int) top, (int) right - (int) left, (int) bottom - (int) top);
} else if (Paint.Style.FILL_AND_STROKE.equals(style)) {
g.fillRect((int) left, (int) top, (int) right - (int) left, (int) bottom - (int) top);
//g.drawRect((int)left+bounds.getX(), (int)top+bounds.getY(), (int)right-(int)left, (int)bottom-(int)top);
}
} finally {
g.setAlpha(oldAlpha);
}


}

public void drawText(String string, float x, float y, Paint paint) {
applyPaint(paint, true);
int offX = 0;
int offY = 0;
if (paint.getTextAlign() == Component.CENTER) {
offX = -g.getFont().stringWidth(string) / 2;
} else if (paint.getTextAlign() == Component.RIGHT) {
offX = -g.getFont().stringWidth(string);
}
int h = g.getFont().getAscent();
int oldAlpha = applyPaint(paint, true);
try {
int offX = 0;
int offY = 0;
if (paint.getTextAlign() == Component.CENTER) {
offX = -g.getFont().stringWidth(string) / 2;
} else if (paint.getTextAlign() == Component.RIGHT) {
offX = -g.getFont().stringWidth(string);
}
int h = g.getFont().getAscent();

g.drawString(string, (int) x + offX, (int) y - h + offY);
g.drawString(string, (int) x + offX, (int) y - h + offY);
} finally {
g.setAlpha(oldAlpha);
}
}

public int getHeight() {
Expand Down Expand Up @@ -154,32 +180,41 @@ private Stroke getStroke(Paint paint) {


public void drawPath(Shape p, Paint paint) {

applyPaint(paint);
Paint.Style style = paint.getStyle();
if (style.equals(Paint.Style.FILL)) {
g.fillShape(p);
//g.drawShape(p, getStroke(paint));
} else if (style.equals(Paint.Style.STROKE)) {
g.drawShape(p, getStroke(paint));
} else if (style.equals(Paint.Style.FILL_AND_STROKE)) {
g.fillShape(p);
g.drawShape(p, getStroke(paint));
int oldAlpha = applyPaint(paint);
try {
Paint.Style style = paint.getStyle();
if (style.equals(Paint.Style.FILL)) {
g.fillShape(p);
//g.drawShape(p, getStroke(paint));
} else if (style.equals(Paint.Style.STROKE)) {
g.drawShape(p, getStroke(paint));
} else if (style.equals(Paint.Style.FILL_AND_STROKE)) {
g.fillShape(p);
g.drawShape(p, getStroke(paint));
}
} finally {
g.setAlpha(oldAlpha);
}

}

public void drawLine(float x1, float y1, float x2, float y2, Paint paint) {
applyPaint(paint);
g.drawLine((int) x1, (int) y1, (int) x2, (int) y2);
int oldAlpha = applyPaint(paint);
try {
g.drawLine((int) x1, (int) y1, (int) x2, (int) y2);
} finally {
g.setAlpha(oldAlpha);
}
}

public void rotate(float angle, float x, float y) {
//Log.p("Rotating by angle "+angle);
// (x, y) is in chart-local coords; Graphics.setTransform now
// conjugates with the active xTranslate/yTranslate, so we must NOT
// bake `absoluteX - bounds.getX()` (= xTranslate) into the rotation
// centre here -- that would apply the conjugation twice and rotate
// the chart around a point well off-screen.
Transform t = g.getTransform();
t.rotate((float) (angle * Math.PI / 180.0), x + absoluteX - bounds.getX(), y + absoluteY - bounds.getY());
t.rotate((float) (angle * Math.PI / 180.0), x, y);
g.setTransform(t);

}

public void scale(float x, float y) {
Expand All @@ -203,20 +238,23 @@ public void drawCircle(float cx, float cy, float r, Paint paint) {


public void drawArc(Rectangle2D oval, float currentAngle, float sweepAngle, boolean useCenter, Paint paint) {
applyPaint(paint);
Paint.Style style = paint.getStyle();
if (Paint.Style.FILL.equals(style)) {
g.fillArc((int) Math.round(oval.getX()), (int) Math.round(oval.getY()), (int) Math.round(oval.getWidth()), (int) Math.round(oval.getHeight()), -(int) Math.floor(currentAngle), -(int) Math.ceil(sweepAngle));
int oldAlpha = applyPaint(paint);
try {
Paint.Style style = paint.getStyle();
if (Paint.Style.FILL.equals(style)) {
g.fillArc((int) Math.round(oval.getX()), (int) Math.round(oval.getY()), (int) Math.round(oval.getWidth()), (int) Math.round(oval.getHeight()), -(int) Math.floor(currentAngle), -(int) Math.ceil(sweepAngle));

} else if (Paint.Style.STROKE.equals(style)) {
g.drawArc((int) Math.round(oval.getX()), (int) Math.round(oval.getY()), (int) Math.round(oval.getWidth()), (int) Math.round(oval.getHeight()), -(int) Math.floor(currentAngle), -(int) Math.ceil(sweepAngle));
} else if (Paint.Style.STROKE.equals(style)) {
g.drawArc((int) Math.round(oval.getX()), (int) Math.round(oval.getY()), (int) Math.round(oval.getWidth()), (int) Math.round(oval.getHeight()), -(int) Math.floor(currentAngle), -(int) Math.ceil(sweepAngle));

} else if (Paint.Style.FILL_AND_STROKE.equals(style)) {
g.fillArc((int) Math.round(oval.getX()), (int) Math.round(oval.getY()), (int) Math.round(oval.getWidth()), (int) Math.round(oval.getHeight()), -(int) Math.floor(currentAngle), -(int) Math.ceil(sweepAngle));
g.drawArc((int) Math.round(oval.getX()), (int) Math.round(oval.getY()), (int) Math.round(oval.getWidth()), (int) Math.round(oval.getHeight()), -(int) Math.floor(currentAngle), -(int) Math.ceil(sweepAngle));
} else if (Paint.Style.FILL_AND_STROKE.equals(style)) {
g.fillArc((int) Math.round(oval.getX()), (int) Math.round(oval.getY()), (int) Math.round(oval.getWidth()), (int) Math.round(oval.getHeight()), -(int) Math.floor(currentAngle), -(int) Math.ceil(sweepAngle));
g.drawArc((int) Math.round(oval.getX()), (int) Math.round(oval.getY()), (int) Math.round(oval.getWidth()), (int) Math.round(oval.getHeight()), -(int) Math.floor(currentAngle), -(int) Math.ceil(sweepAngle));

}
} finally {
g.setAlpha(oldAlpha);
}

}

public void drawArcWithGradient(Rectangle2D oval, float currentAngle, float sweepAngle, boolean useCenter, Paint paint, GradientDrawable gradient) {
Expand All @@ -228,19 +266,21 @@ public void drawPoint(Float get, Float get0, Paint paint) {
}

public void drawRoundRect(Rectangle2D rect, float rx, float ry, Paint mPaint) {
applyPaint(mPaint);
Paint.Style style = mPaint.getStyle();
if (Paint.Style.FILL.equals(style)) {
g.fillRoundRect((int) rect.getX(), (int) rect.getY(), (int) (rect.getWidth()), (int) (rect.getHeight()), (int) rx, (int) ry);
} else if (Paint.Style.STROKE.equals(style)) {
int oldAlpha = applyPaint(mPaint);
try {
Paint.Style style = mPaint.getStyle();
if (Paint.Style.FILL.equals(style)) {
g.fillRoundRect((int) rect.getX(), (int) rect.getY(), (int) (rect.getWidth()), (int) (rect.getHeight()), (int) rx, (int) ry);
} else if (Paint.Style.STROKE.equals(style)) {
g.drawRoundRect((int) rect.getX(), (int) rect.getY(), (int) (rect.getWidth()), (int) (rect.getHeight()), (int) rx, (int) ry);
} else if (Paint.Style.FILL_AND_STROKE.equals(style)) {
g.fillRoundRect((int) rect.getX(), (int) rect.getY(), (int) (rect.getWidth()), (int) (rect.getHeight()), (int) rx, (int) ry);
g.drawRoundRect((int) rect.getX(), (int) rect.getY(), getWidth(), (int) (rect.getHeight()), (int) rx, (int) ry);
}
g.drawRoundRect((int) rect.getX(), (int) rect.getY(), (int) (rect.getWidth()), (int) (rect.getHeight()), (int) rx, (int) ry);
} else if (Paint.Style.FILL_AND_STROKE.equals(style)) {
g.fillRoundRect((int) rect.getX(), (int) rect.getY(), (int) (rect.getWidth()), (int) (rect.getHeight()), (int) rx, (int) ry);
g.drawRoundRect((int) rect.getX(), (int) rect.getY(), getWidth(), (int) (rect.getHeight()), (int) rx, (int) ry);
} finally {
g.setAlpha(oldAlpha);
}
g.drawRoundRect((int) rect.getX(), (int) rect.getY(), (int) (rect.getWidth()), (int) (rect.getHeight()), (int) rx, (int) ry);


}

public void drawBitmap(Image img, float left, float top, Paint paint) {
Expand Down
50 changes: 48 additions & 2 deletions CodenameOne/src/com/codename1/charts/views/AbstractChart.java
Original file line number Diff line number Diff line change
Expand Up @@ -372,18 +372,47 @@ protected void drawPath(Canvas canvas, List<Float> points, Paint paint, boolean
path.moveTo(tempDrawPoints[0], tempDrawPoints[1]);
path.lineTo(tempDrawPoints[2], tempDrawPoints[3]);

// Track the running endpoint of the open subpath so we can emit a
// moveTo only when the data actually skips a segment (an off-screen
// point was filtered out via `continue`). The original loop emitted
// `path.moveTo(tempDrawPoints[0..1])` before EVERY lineTo, which
// produces N disjoint single-segment subpaths for an N-point line.
// The iOS GL+Metal form-Graphics drawShape -> TextureAlphaMask path
// crashes the on-screen frame buffer when fed a multi-subpath stroke
// covering most of the form (the chart fills BorderLayout.CENTER) --
// every other port (Skia / JavaFX / mutable-image NativeGraphics)
// happily collapses the redundant moveTos but iOS form-Graphics
// ends up with the entire frame dropped (form title bar disappears
// along with the chart). Emit moveTo only when the previous segment
// was skipped or the next segment doesn't continue from the running
// endpoint, so an unfiltered line series renders as a single
// continuous polyline -- byte-equivalent to the historical output on
// every port that handled the multi-subpath form, and the form
// actually paints on iOS.
float lastEndX = tempDrawPoints[2];
float lastEndY = tempDrawPoints[3];
boolean haveOpenSubpath = true;

int length = points.size();
for (int i = 4; i < length; i += 2) {
if ((points.get(i - 1) < 0 && points.get(i + 1) < 0)
|| (points.get(i - 1) > height && points.get(i + 1) > height)) {
haveOpenSubpath = false;
continue;
}
tempDrawPoints = calculateDrawPoints(points.get(i - 2), points.get(i - 1), points.get(i),
points.get(i + 1), height, width);
if (!circular) {
path.moveTo(tempDrawPoints[0], tempDrawPoints[1]);
if (!haveOpenSubpath
|| tempDrawPoints[0] != lastEndX
|| tempDrawPoints[1] != lastEndY) {
path.moveTo(tempDrawPoints[0], tempDrawPoints[1]);
}
}
path.lineTo(tempDrawPoints[2], tempDrawPoints[3]);
lastEndX = tempDrawPoints[2];
lastEndY = tempDrawPoints[3];
haveOpenSubpath = true;
}
if (circular) {
path.lineTo(points.get(0), points.get(1));
Expand Down Expand Up @@ -415,18 +444,35 @@ protected void drawPath(Canvas canvas, float[] points, Paint paint, boolean circ
path.moveTo(tempDrawPoints[0], tempDrawPoints[1]);
path.lineTo(tempDrawPoints[2], tempDrawPoints[3]);

// See the List<Float> overload above for the rationale: collapse
// redundant moveTos so an unfiltered line series renders as a
// single continuous polyline rather than N disjoint single-segment
// subpaths. iOS form-Graphics drawShape -> TextureAlphaMask drops
// the entire frame when fed the multi-subpath form.
float lastEndX = tempDrawPoints[2];
float lastEndY = tempDrawPoints[3];
boolean haveOpenSubpath = true;

int length = points.length;
for (int i = 4; i < length; i += 2) {
if ((points[i - 1] < 0 && points[i + 1] < 0)
|| (points[i - 1] > height && points[i + 1] > height)) {
haveOpenSubpath = false;
continue;
}
tempDrawPoints = calculateDrawPoints(points[i - 2], points[i - 1], points[i], points[i + 1],
height, width);
if (!circular) {
path.moveTo(tempDrawPoints[0], tempDrawPoints[1]);
if (!haveOpenSubpath
|| tempDrawPoints[0] != lastEndX
|| tempDrawPoints[1] != lastEndY) {
path.moveTo(tempDrawPoints[0], tempDrawPoints[1]);
}
}
path.lineTo(tempDrawPoints[2], tempDrawPoints[3]);
lastEndX = tempDrawPoints[2];
lastEndY = tempDrawPoints[3];
haveOpenSubpath = true;
}
if (circular) {
path.lineTo(points[0], points[1]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1517,7 +1517,7 @@ public boolean isTranslationSupported() {
return false;
}

/// Translates the X/Y location for drawing on the underlying surface. Translation
/// Translates the X/Y location for drawing on the underlying surface. Translation
/// is incremental so the new value will be added to the current translation and
/// in order to reset translation we have to invoke
/// `translate(-getTranslateX(), -getTranslateY())`
Expand Down
Loading
Loading