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
11 changes: 11 additions & 0 deletions .github/workflows/_build-ios-port.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,19 @@ jobs:
id: src_hash
run: |
set -euo pipefail
# `maven/svg-transcoder/src/main` belongs in here: the cn1-built
# cache contains ~/.m2/repository/com/codenameone, including the
# svg-transcoder JAR that the test-app build resolves at
# transcode-svg time. Without svg-transcoder in the source hash, a
# fix to SVGParser etc. doesn't change the cache key, the cn1-built
# cache hit short-circuits setup-workspace, the test app keeps
# generating against the stale JAR, and the SVG render bug never
# gets fixed in CI (most visible on clipped_badge.svg, whose outer
# rect kept rendering as a square long after the clip-path
# forwarding fix landed in the transcoder source).
SRC_HASH=$(find CodenameOne/src Ports/iOSPort vm/JavaAPI vm/ByteCodeTranslator Themes native-themes \
maven/codenameone-maven-plugin/src/main \
maven/svg-transcoder/src/main \
-type f \( -name '*.java' -o -name '*.m' -o -name '*.h' -o -name '*.xml' -o -name '*.properties' -o -name '*.css' \) 2>/dev/null \
| sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
POM_HASH=$(find . -name 'pom.xml' -not -path './scripts/*' 2>/dev/null \
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/developer-guide-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -289,17 +289,27 @@ jobs:
fi

- name: Set up Java 17 for LanguageTool
if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true'
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'

- name: Install language-tool-python
if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true'
run: |
set -euo pipefail
pip install --user language-tool-python==2.9.4

- name: Run LanguageTool grammar check
# LanguageTool consumes the rendered HTML produced by "Build
# Developer Guide HTML and PDF". That step is gated on the same
# paths filter (docs/demos/workflow). Without this gate the
# LanguageTool step ran on PRs that didn't touch docs and exited
# non-zero because `developer-guide.html` was never generated,
# turning every unrelated PR (e.g. iOS port fixes) red on the
# final quality-gate check.
if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true'
run: |
set -euo pipefail
REPORT_DIR="build/developer-guide/reports"
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/ios-packaging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,18 @@ jobs:
cn1-binaries-${{ runner.os }}-

- name: Restore built CN1 + iOS port artifacts
# Use the exact key build-port saved against (published as a job
# output). Recomputing src_hash locally drifts whenever new
# source paths are added to the build-port hash without
# mirroring the change here, causing fail-on-cache-miss to
# blow up the entire packaging suite.
uses: actions/cache/restore@v4
with:
path: |
~/.m2/repository/com/codenameone
Themes
Ports/iOSPort/nativeSources
key: cn1-built-${{ runner.os }}-${{ steps.src_hash.outputs.hash }}
key: ${{ needs.build-port.outputs.cn1_built_cache_key }}
fail-on-cache-miss: true

- name: Build sample iOS app
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/scripts-ios-native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,17 @@ jobs:
cn1-binaries-${{ runner.os }}-

- name: Restore built CN1 + iOS port artifacts
# Use the exact key build-port saved against (published as a job
# output). Recomputing src_hash locally drifts whenever new
# source paths are added to the build-port hash without
# mirroring the change here.
uses: actions/cache/restore@v4
with:
path: |
~/.m2/repository/com/codenameone
Themes
Ports/iOSPort/nativeSources
key: cn1-built-${{ runner.os }}-${{ steps.src_hash.outputs.hash }}
key: ${{ needs.build-port.outputs.cn1_built_cache_key }}
fail-on-cache-miss: true

- name: Build sample iOS app
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/scripts-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,20 @@ jobs:
cn1-binaries-${{ runner.os }}-

- name: Restore built CN1 + iOS port artifacts
# Use the exact key build-port saved against (published as a job
# output). Recomputing the src_hash on this runner has been
# observed to produce a different value than build-port on the
# same SHA -- and even when the algorithm is identical, any new
# source path added to the hash here has to land in three
# places at once or the keys diverge. The same workaround
# protects the build-ios-metal job a few hundred lines below.
uses: actions/cache/restore@v4
with:
path: |
~/.m2/repository/com/codenameone
Themes
Ports/iOSPort/nativeSources
key: cn1-built-${{ runner.os }}-${{ steps.src_hash.outputs.hash }}
key: ${{ needs.build-port.outputs.cn1_built_cache_key }}
fail-on-cache-miss: true

- name: Build sample iOS app and compile workspace
Expand Down
27 changes: 20 additions & 7 deletions CodenameOne/src/com/codename1/ui/LinearGradientPaint.java
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ private float[] reverseFractions() {
@Override
@SuppressWarnings("UnusedFormalParameter")
public void paint(Graphics g, double x, double y, double w, double h) {
// TODO: x and y probably need to be taken into consideration here...
paint(g, w, h, true);
}

Expand All @@ -200,11 +199,26 @@ private void paint(Graphics g, double w, double h, boolean processCycles) {
if (getTransform() != null) {
t2.concatenate(getTransform());
}
int tx = g.getTranslateX();
int ty = g.getTranslateY();
g.translate(-tx, -ty);

t2.translate((float) (startX + tx), (float) (startY + ty));
// Build the gradient frame on top of the caller's transform. The
// previous version captured `g.getTranslateX()/getTranslateY()` and
// baked them into `t2.translate(startX + tx, startY + ty)`, then
// zeroed `g.translate(-tx, -ty)` before `g.setTransform(t2)`. On
// ports where `isTranslationSupported()` is false (iOS, Android,
// JavaSE -- every active port today), Graphics already conjugates
// setTransform with `T(xTranslate)` so the user matrix operates in
// local coordinates regardless of prior g.translate; that
// conjugation re-applies the cell offset at the *screen* level
// automatically. Baking `tx, ty` into a translate that sits
// *inside* the SVG / theme scale meant the cell offset went
// through the scale a second time, shifting the gradient fill
// away from the stroke. Most visible on SVGStaticScreenshotTest's
// gradient_circle, where the filled circle appeared stacked below
// the dark-blue outline on Android (and would have appeared the
// same on iOS Metal once the triangle-clip bug was unmasked).
// Just build `t * Translate(startX, startY) * Rotate * Translate(0,
// -ph/2)` and let Graphics.setTransform's existing conjugation
// restore the screen-level offset.
t2.translate((float) startX, (float) startY);
t2.rotate((float) theta, 0, 0);
t2.translate(0, -(float) ph / 2);

Expand Down Expand Up @@ -293,7 +307,6 @@ private void paint(Graphics g, double w, double h, boolean processCycles) {
}*/
g.setAlpha(alpha);
g.setTransform(t);
g.translate(tx, ty);
if (p != null) {
g.setColor(p);
}
Expand Down
104 changes: 96 additions & 8 deletions Ports/iOSPort/nativeSources/CN1Metalcompat.m
Original file line number Diff line number Diff line change
Expand Up @@ -755,13 +755,81 @@ void CN1MetalTileImage(id<MTLTexture> texture, int alpha,
// the failure rather than papering over it with a different pipeline
// that would silently mask the bug.

// Returns the effective screen-pixel scale baked into the current
// transform. The vertex shader applies `projection * modelView *
// transform * pos`; projection / modelView are stable per frame and
// expressed in framebuffer units, so any *additional* scaling comes
// from `currentTransform`. For text rendering we want to know that
// effective scale up front so the glyph atlas can rasterise at the
// matching pixel size; otherwise the atlas glyph art (rasterised at
// font.pointSize) is sampled through a stretched quad and the glyph
// turns into a smear at every `g.setTransform(scale)` site -- e.g.
// the SVG transcoder painting `<text>` under a viewBox-to-display
// scale, which is the most visible offender.
//
// Pulls a uniform scale by averaging the magnitudes of the two basis
// vectors of the upper-left 2x2 (sx along the X column, sy along the
// Y column). Shear-only or pure-rotation matrices return 1 because
// both column magnitudes stay at 1; pure scale returns the scale.
// We do *not* try to handle non-uniform scale separately -- the
// glyph atlas slot key is one float (pointSize), so even if the
// SVG draws with sx != sy we have to pick one. Going with the
// geometric mean keeps the rasterised glyph close to either bound
// and the residual GPU stretch only kicks in along the dimension
// that's farther from the mean.
static inline float currentTransformGlyphScale(void) {
float c0x = currentTransform.columns[0].x;
float c0y = currentTransform.columns[0].y;
float c1x = currentTransform.columns[1].x;
float c1y = currentTransform.columns[1].y;
float sx = sqrtf(c0x * c0x + c0y * c0y);
float sy = sqrtf(c1x * c1x + c1y * c1y);
float s = (sx + sy) * 0.5f;
// Reject NaN / inf / non-positive values: any of those would
// poison `font.pointSize * s` below and produce a UIFont with
// bad metrics that hangs the CTLine layout. `isfinite` is true
// only for finite numbers; treat anything else as "use unscaled
// font" by returning 1.0 (the `useScaledFont` gate at the call
// site clears that to the fast path).
if (!isfinite(s) || s <= 0.0f) return 1.0f;
// Cap at 8x to keep the atlas from rasterising absurdly large
// bitmaps for a runaway transform; well past 8x the difference
// between "atlas-perfect" and "sampled-and-filtered" is below
// what the user can see anyway.
if (s < 1.0f) s = 1.0f; // No down-rasterising; 1px atlas is fine for downscale.
if (s > 8.0f) s = 8.0f;
return s;
}

void CN1MetalDrawString(NSString *str, UIFont *font, int color, int alpha, int x, int y) {
if (str == nil || font == nil || str.length == 0) return;

CN1MetalGlyphAtlas *atlas = [CN1MetalGlyphAtlas atlasForFont:font];
// CoreText shapes glyphs and the atlas rasterises them at font.pointSize
// — but the active Graphics transform may be scaling the whole drawing
// up before the framebuffer write. If we hand the shader a quad sized to
// the unscaled glyph and let the transform stretch it on the GPU, the
// result is a smeared/blurry glyph (Codename One's SVG transcoder paints
// viewBox-relative text through `g.setTransform(scale*translate)`, so the
// screen scale is routinely 2x-4x). Detect the scale baked into
// `currentTransform` and rasterise the atlas at the effective pixel size
// so the shader transform produces a 1:1 sample. We then divide the
// returned glyph metrics back down by the same factor so the vertex
// coords stay in unscaled space — the GPU re-applies `currentTransform`
// for free and the final on-screen position matches the unscaled path.
float glyphScale = currentTransformGlyphScale();
BOOL useScaledFont = (glyphScale > 1.01f);
UIFont *renderFont = useScaledFont
? [font fontWithSize:font.pointSize * glyphScale]
: font;
if (renderFont == nil) {
renderFont = font;
useScaledFont = NO;
}

CN1MetalGlyphAtlas *atlas = [CN1MetalGlyphAtlas atlasForFont:renderFont];
if (atlas == nil) {
NSLog(@"CN1MetalDrawString: no atlas available for font %@ pt=%g; string skipped",
font.fontName, (double)font.pointSize);
renderFont.fontName, (double)renderFont.pointSize);
return;
}

Expand All @@ -774,7 +842,7 @@ void CN1MetalDrawString(NSString *str, UIFont *font, int color, int alpha, int x
// fresh form, which surfaced as the TL panel of graphics-draw-string-
// decorated rendering larger/wider glyphs than TR/BL/BR despite
// identical Java state.
NSDictionary *attrs = @{ (__bridge NSString *)kCTFontAttributeName: font };
NSDictionary *attrs = @{ (__bridge NSString *)kCTFontAttributeName: renderFont };
CFAttributedStringRef attrStr = CFAttributedStringCreate(NULL,
(__bridge CFStringRef)str,
(__bridge CFDictionaryRef)attrs);
Expand All @@ -798,7 +866,14 @@ void CN1MetalDrawString(NSString *str, UIFont *font, int color, int alpha, int x
// (not CTFontGetAscent) is intentional — UIKit's metric is what
// drawAtPoint references and the values can disagree slightly across
// fonts.
//
// Use the ORIGINAL font's ascender (and the original pointSize) so the
// baseline lands where the caller-side framework expects, even when we
// upscaled the atlas. The atlas-internal metrics (renderFont) reflect
// the rasterised size; we divide them by `glyphScale` below to bring
// them back into caller-side coords.
float baselineY = (float)y + (float)font.ascender;
float invScale = useScaledFont ? (1.0f / glyphScale) : 1.0f;

simd_float4 colorV = premultipliedColor(color, alpha);
int textureW = atlas.textureWidth;
Expand Down Expand Up @@ -838,11 +913,24 @@ void CN1MetalDrawString(NSString *str, UIFont *font, int color, int alpha, int x
// bbox-left-on-screen = x + posX + bearingX
// bbox-top-on-screen = baselineY - posY - (bearingY + bbox.height)
// Slot extends 1px above and to the left of the bbox.
float gx = (float)x + (float)posPtr[i].x + slot.bearingX - 1.0f;
float gy = baselineY - (float)posPtr[i].y
- (slot.bearingY + slot.bboxHeight) - 1.0f;
float gw = (float)slot.width;
float gh = (float)slot.height;
//
// When the atlas was rasterised at the upscaled size, the
// CoreText positions and slot metrics are in renderFont-pixel
// space (which is glyphScale times the caller-side pixel space).
// Divide each one back down by glyphScale so the emitted vertex
// coords live in caller-side space — the vertex shader will
// re-apply currentTransform (the same scale we factored out) and
// produce a quad of the correct on-screen size, sampling
// 1:1 against the now-matching atlas.
float posX = (float)posPtr[i].x * invScale;
float posY = (float)posPtr[i].y * invScale;
float bearingX = slot.bearingX * invScale;
float bearingY = slot.bearingY * invScale;
float bboxHeight = slot.bboxHeight * invScale;
float gx = (float)x + posX + bearingX - invScale;
float gy = baselineY - posY - (bearingY + bboxHeight) - invScale;
float gw = (float)slot.width * invScale;
float gh = (float)slot.height * invScale;

float vertices[8] = {
gx, gy,
Expand Down
9 changes: 5 additions & 4 deletions Ports/iOSPort/nativeSources/DrawPath.m
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,15 @@ -(void)execute
JAVA_INT outputBounds[4];

Renderer_getOutputBounds(renderer, (JAVA_INT*)&outputBounds);
if ( outputBounds[2] < 0 || outputBounds[3] < 0 ){
return;
}
// outputBounds is { minX, minY, maxX, maxY } in renderer pixel
// space; maxX / maxY are legitimately negative when the path sits
// in the negative quadrant. Filter on width / height (computed
// below) rather than the raw max values.
JAVA_INT x = min(outputBounds[0], outputBounds[2]);
JAVA_INT y = min(outputBounds[1], outputBounds[3]);
JAVA_INT width = outputBounds[2]-outputBounds[0];
JAVA_INT height = outputBounds[3]-outputBounds[1];

if ( width < 0 ) width = -width;
if ( height < 0 ) height = -height;
if (width == 0 || height == 0) {
Expand Down
47 changes: 32 additions & 15 deletions Ports/iOSPort/nativeSources/IOSNative.m
Original file line number Diff line number Diff line change
Expand Up @@ -9238,19 +9238,23 @@ JAVA_OBJECT com_codename1_impl_ios_IOSNative_nativePathRendererToARGB___long_int
JAVA_INT outputBounds[4];

Renderer_getOutputBounds(renderer, (JAVA_INT*)&outputBounds);
if ( outputBounds[2] < 0 || outputBounds[3] < 0 ){
return 0;
}

// outputBounds is { minX, minY, maxX, maxY }; maxX / maxY can be
// legitimately negative for shapes drawn at negative coordinates
// (see the comment in nativePathRendererCreateTexture above).
// Filter on the actual width / height below.

//GLuint tex=0;
JAVA_INT x = min(outputBounds[0], outputBounds[2]);
JAVA_INT y = min(outputBounds[1], outputBounds[3]);
JAVA_INT width = outputBounds[2]-outputBounds[0];
JAVA_INT height = outputBounds[3]-outputBounds[1];

if ( width < 0 ) width = -width;
if ( height < 0 ) height = -height;

if (width == 0 || height == 0) {
return 0;
}

AlphaConsumer ac = {
x,
y,
Expand Down Expand Up @@ -9300,7 +9304,19 @@ JAVA_LONG com_codename1_impl_ios_IOSNative_nativePathRendererCreateTexture___lon
Renderer *r = (Renderer*)renderer;
JAVA_INT outputBounds[4];
Renderer_getOutputBounds(renderer, (JAVA_INT*)&outputBounds);
if (outputBounds[2] < 0 || outputBounds[3] < 0) return 0;
// outputBounds is { minX, minY, maxX, maxY } in renderer pixel
// space, which can legitimately be entirely negative when the
// input shape sits at negative coordinates (e.g. the SVG
// transcoder emits `<rect x="-5" y="-40" width="10" height="20">`
// for the spinner_animated.svg children -- after the SVG scale
// bake the renderer sees a path with bounds (-7, -60, 8, -30)).
// The previous check rejected those legitimate negative maxX /
// maxY values, returned 0 / nil texture, and silently dropped
// every fillShape on negatively-positioned paths -- the
// spinner column was blank on iOS Metal screenshots as a
// result. Only reject *empty* bounds (max <= min on either
// axis); the unsigned width / height computed below carry the
// actual extent.
JAVA_INT x = min(outputBounds[0], outputBounds[2]);
JAVA_INT y = min(outputBounds[1], outputBounds[3]);
JAVA_INT width = outputBounds[2] - outputBounds[0];
Expand Down Expand Up @@ -9350,20 +9366,21 @@ JAVA_LONG com_codename1_impl_ios_IOSNative_nativePathRendererCreateTexture___lon

Renderer *r = (Renderer*)renderer;
JAVA_INT outputBounds[4];

Renderer_getOutputBounds(renderer, (JAVA_INT*)&outputBounds);
if ( outputBounds[2] < 0 || outputBounds[3] < 0 ){
//return 0;
POOL_END();
return;
}

// outputBounds is { minX, minY, maxX, maxY }; the maxX/maxY
// values can legitimately be negative when the shape sits in
// the negative quadrant (e.g. the spinner SVG draws each
// rotated rect at y in [-40, -20]). The width / height check
// below filters degenerate / empty paths. Mirrors the Metal
// branch above.

GLuint tex=0;
JAVA_INT x = min(outputBounds[0], outputBounds[2]);
JAVA_INT y = min(outputBounds[1], outputBounds[3]);
JAVA_INT width = outputBounds[2]-outputBounds[0];
JAVA_INT height = outputBounds[3]-outputBounds[1];

if ( width < 0 ) width = -width;
if ( height < 0 ) height = -height;
if (width == 0 || height == 0) {
Expand Down
Loading
Loading