Skip to content

Skin designer: safe-area insets are points, and a notch lives in the display - #5626

Merged
shai-almog merged 7 commits into
masterfrom
fix/skin-designer-safe-area-units
Aug 30, 2026
Merged

shai-almog merged 7 commits into
masterfrom
fix/skin-designer-safe-area-units

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Fixes the skin designer half of discussion #5580 — a skin built for an iPhone 13 Pro Max doesn't reserve the safe area the hardware does. Two independent causes.

The insets were scaled by the wrong number

The device catalog stores safeTop / safeBottom in the units Apple and Google publish — iOS points, Android dp. build_devices_json.py writes the canonical values: 47/34 for a notch, 59/34 for a Dynamic Island, 24/24 for a tablet, 20/0 otherwise.

buildProperties converted them with resolutionW / DevicePreview.VB_W. That 320-wide figure is the designer's drawing viewbox; it has nothing to do with the device's density and only coincides with it on a 320pt-wide phone, which is why nobody caught it. The error is pointWidth / 320, so every device was wrong:

device was now hardware
iPhone 13 Pro Max top 189 px 141 141 (47pt × 3)
iPhone 13 Pro Max bottom 136 px 102 102 (34pt × 3)
iPhone 15 Pro Max top 238 px 177 177 (59pt × 3)
iPad Pro 12.9 top 154 px 48 48 (24pt × 2)

The catalog now carries a scale per device — devices.json moves to version 3, written by a new density_scale() in the generator (iOS rounds ppi/163 because Apple only ships integral @2x/@3x panels; Android keeps ppi/160 continuous). DeviceDatabase exposes it as densityScale, with a fallback that mirrors the same rule for a pre-v3 catalog.

The Math.max against the in-screen cutout extent was mixing units too — comparing points against viewbox units. Both sides are converted to pixels first now.

The Info tab's fields say pt instead of px to match, and the developer guide's "in viewbox pixels" claim is corrected.

A notch belongs inside the display

Notches were rendered into a frame extension above the screen rect. That made the skin taller than the device, pushed displayY down by the notch height, and left the status-bar band conspicuously empty — and, since nothing covered the screen, notches were deliberately excluded from the safe-area top.

On an iPhone X through 13 the notch is inside the display: the panel is 1284×2778 and the notch eats into the top of it. It's now painted on the screen rect like every other cutout — anchored to the top edge, square at the top, rounded at the bottom — and counted in the safe-area top. applyTopFrameCutouts and applyInScreenCutouts collapse into one applyCutouts; computeTopCutoutPx is gone.

This is also what the wizard's own live preview has always drawn, so the editor and the exported skin finally agree, and the existing editor screenshots in the guide stay accurate.

Simulator-side state leak

JavaSEPort never cleared roundedSkin, safeAreaPortrait or safeAreaLandscape — they were only ever assigned on the branches that had a value for them. Loading a legacy skin after a skin designer one kept the previous device's safe area in force and kept painting the skin over the UI. They're cleared at the top of the skin load now.

Verification

Drove the real private raster and property code through reflection for four devices (13 Pro Max / 16 Pro / SE 2022 / iPad Pro 12.9) and asserted:

  • displayX and displayY equal the bezel — no frame extension above the screen
  • display size equals the device resolution
  • safe insets equal the vendor value × the device scale
  • a cutout is painted inside the screen rect exactly when the device has one
  • no cutout material reaches below the safe area

All pass. For the reported device safePortraitY is 141 with the notch bottom at row 119, inside the reserved band.

Also: scripts/skindesigner/common compiles clean; JavaSEPort compiles clean on JDK 8; Vale reports 0/0/0 at suggestion and LanguageTool status: ok, total: 0 on the changed guide chapter (dp added to the accept list).

Not changed

The default notch is 180 of 320 viewbox units — calibrated for an iPhone X, where the notch really is 56% of the screen width. It's too wide for a 13 Pro Max (38%), but it's a user-draggable default and purely cosmetic now that the safe area is computed from the catalog rather than from the drawn shape.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-08-29T17:49:29.469195Z 896ea3a New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0a00a7f653

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/skindesigner/tools/devicedb/build_devices_json.py
@github-actions

github-actions Bot commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@shai-almog

shai-almog commented Aug 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Compared 12 screenshots: 12 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@github-actions

github-actions Bot commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ✅ ByteCodeTranslator: 0 findings (no issues)
    • ✅ android: 0 findings (no issues)
    • ✅ build-hint-catalog: 0 findings (no issues)
    • ✅ build-hint-tools: 0 findings (no issues)
    • ✅ codenameone-maven-plugin: 0 findings (no issues)
    • ✅ core-unittests: 0 findings (no issues)
    • ✅ ios: 0 findings (no issues)
  • ✅ PMD: 0 findings (no issues) [Report archive]
  • ✅ Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b9a5d93872

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@shai-almog
shai-almog force-pushed the fix/skin-designer-safe-area-units branch from b9a5d93 to d4204f0 Compare August 29, 2026 12:20

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4204f0978

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/skindesigner/tools/devicedb/build_devices_json.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e47cfd5c2f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/demos/common/src/main/snippets/developer-guide/skin-designer.properties Outdated
@shai-almog

shai-almog commented Aug 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 9.08% (8992/99063 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.87% (46410/523509), branch 3.46% (1718/49585), complexity 3.47% (1834/52893), method 5.32% (1482/27832), class 10.72% (399/3721)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 9.08% (8992/99063 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.87% (46410/523509), branch 3.46% (1718/49585), complexity 3.47% (1834/52893), method 5.32% (1482/27832), class 10.72% (399/3721)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend scalar fallback (no native SIMD)
SIMD int-add (64K x300) java 127ms / native 39ms = 3.2x speedup
SIMD float-mul (64K x300) java 60ms / native 38ms = 1.5x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 56.000 ms
Base64 CN1 decode 54.000 ms
Base64 native encode 355.000 ms
Base64 encode ratio (CN1/native) 0.158x (84.2% faster)
Base64 native decode 206.000 ms
Base64 decode ratio (CN1/native) 0.262x (73.8% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog

shai-almog commented Aug 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 311 seconds

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 55ms / native 3ms = 18.3x speedup
SIMD float-mul (64K x300) java 56ms / native 3ms = 18.6x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 175.000 ms
Base64 CN1 decode 101.000 ms
Base64 native encode 775.000 ms
Base64 encode ratio (CN1/native) 0.226x (77.4% faster)
Base64 native decode 239.000 ms
Base64 decode ratio (CN1/native) 0.423x (57.7% faster)
Base64 SIMD encode 53.000 ms
Base64 encode ratio (SIMD/CN1) 0.303x (69.7% faster)
Base64 SIMD decode 49.000 ms
Base64 decode ratio (SIMD/CN1) 0.485x (51.5% faster)
Base64 encode ratio (SIMD/native) 0.068x (93.2% faster)
Base64 decode ratio (SIMD/native) 0.205x (79.5% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 8.000 ms
Image createMask (SIMD on) 1.000 ms
Image createMask ratio (SIMD on/off) 0.125x (87.5% faster)
Image applyMask (SIMD off) 50.000 ms
Image applyMask (SIMD on) 35.000 ms
Image applyMask ratio (SIMD on/off) 0.700x (30.0% faster)
Image modifyAlpha (SIMD off) 60.000 ms
Image modifyAlpha (SIMD on) 57.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.950x (5.0% faster)
Image modifyAlpha removeColor (SIMD off) 86.000 ms
Image modifyAlpha removeColor (SIMD on) 110.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.279x (27.9% slower)

@shai-almog

shai-almog commented Aug 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@shai-almog

shai-almog commented Aug 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog

shai-almog commented Aug 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Compared 217 screenshots: 217 matched.
✅ Native Apple Watch (watchOS, Core Graphics) screenshot tests passed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 16c109ef90

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
@shai-almog

shai-almog commented Aug 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Compared 143 screenshots: 143 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 953 seconds

Build and Run Timing

Metric Duration
Simulator Boot 72000 ms
Simulator Boot (Run) 1000 ms
App Install 15000 ms
App Launch 0 ms
Test Execution 451000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 60ms / native 3ms = 20.0x speedup
SIMD float-mul (64K x300) java 62ms / native 3ms = 20.6x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 245.000 ms
Base64 CN1 decode 104.000 ms
Base64 native encode 523.000 ms
Base64 encode ratio (CN1/native) 0.468x (53.2% faster)
Base64 native decode 497.000 ms
Base64 decode ratio (CN1/native) 0.209x (79.1% faster)
Base64 SIMD encode 54.000 ms
Base64 encode ratio (SIMD/CN1) 0.220x (78.0% faster)
Base64 SIMD decode 85.000 ms
Base64 decode ratio (SIMD/CN1) 0.817x (18.3% faster)
Base64 encode ratio (SIMD/native) 0.103x (89.7% faster)
Base64 decode ratio (SIMD/native) 0.171x (82.9% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 10.000 ms
Image createMask (SIMD on) 4.000 ms
Image createMask ratio (SIMD on/off) 0.400x (60.0% faster)
Image applyMask (SIMD off) 330.000 ms
Image applyMask (SIMD on) 365.000 ms
Image applyMask ratio (SIMD on/off) 1.106x (10.6% slower)
Image modifyAlpha (SIMD off) 347.000 ms
Image modifyAlpha (SIMD on) 631.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.818x (81.8% slower)
Image modifyAlpha removeColor (SIMD off) 358.000 ms
Image modifyAlpha removeColor (SIMD on) 522.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.458x (45.8% slower)

@shai-almog

shai-almog commented Aug 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Compared 149 screenshots: 149 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 1295 seconds

Build and Run Timing

Metric Duration
Simulator Boot 82000 ms
Simulator Boot (Run) 0 ms
App Install 14000 ms
App Launch 4000 ms
Test Execution 426000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 71ms / native 4ms = 17.7x speedup
SIMD float-mul (64K x300) java 71ms / native 3ms = 23.6x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 179.000 ms
Base64 CN1 decode 96.000 ms
Base64 native encode 254.000 ms
Base64 encode ratio (CN1/native) 0.705x (29.5% faster)
Base64 native decode 214.000 ms
Base64 decode ratio (CN1/native) 0.449x (55.1% faster)
Base64 SIMD encode 49.000 ms
Base64 encode ratio (SIMD/CN1) 0.274x (72.6% faster)
Base64 SIMD decode 44.000 ms
Base64 decode ratio (SIMD/CN1) 0.458x (54.2% faster)
Base64 encode ratio (SIMD/native) 0.193x (80.7% faster)
Base64 decode ratio (SIMD/native) 0.206x (79.4% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 7.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.286x (71.4% faster)
Image applyMask (SIMD off) 42.000 ms
Image applyMask (SIMD on) 37.000 ms
Image applyMask ratio (SIMD on/off) 0.881x (11.9% faster)
Image modifyAlpha (SIMD off) 36.000 ms
Image modifyAlpha (SIMD on) 28.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.778x (22.2% faster)
Image modifyAlpha removeColor (SIMD off) 37.000 ms
Image modifyAlpha removeColor (SIMD on) 34.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.919x (8.1% faster)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 955e310615

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

shai-almog and others added 7 commits August 29, 2026 20:41
…display

Reported in discussion #5580 -- a skin built for an iPhone 13 Pro Max does
not reserve the safe area the hardware does. Two independent causes.

Wrong unit. The device catalog stores safeTop/safeBottom in the units the
vendors publish -- iOS points, Android dp: 47/34 for a notch, 59/34 for a
Dynamic Island, 24/24 for a tablet, 20/0 otherwise. buildProperties scaled
them by resolutionW / 320, the designer's drawing viewbox, which has
nothing to do with the device's density and only coincides with it on a
320pt-wide phone. Every device was wrong: the 13 Pro Max reserved 189px
where the hardware reserves 141, an iPad Pro 12.9 reserved 154px where the
hardware reserves 48. The catalog now carries a `scale` per device
(devices.json version 3, written by density_scale() in the generator), and
the two conversions use it. The Info tab's fields say "pt" instead of "px"
to match, and the max against the in-screen cutout extent -- which really
is in viewbox units -- now happens after both sides are in pixels rather
than between two different units.

Notch placement. Notches were rendered into a frame extension ABOVE the
screen rect, which made the skin taller than the device, pushed displayY
down by the notch height, and left the status bar band conspicuously empty
-- and, because nothing covered the screen, notches were excluded from the
safe-area top. On an iPhone X through 13 the notch is inside the display:
the panel is 1284x2778 and the notch eats into the top of it. It is now
painted on the screen rect like every other cutout, anchored to the top
edge with a rounded bottom, and counted in the safe-area top. This is also
what the wizard's own live preview always drew, so the editor and the
exported skin finally agree.

Also clears roundedSkin / safeAreaPortrait / safeAreaLandscape at the top
of JavaSEPort's skin load. They were only ever assigned on the branches
that had a value for them, so loading a legacy skin after a skin designer
one kept the previous device's safe area in force and kept painting the
skin over the UI.

Verified against the real private raster and property code for four
devices: displayX/Y equal the bezel with no extension, display size equals
the device resolution, the safe insets equal the vendor values times the
device scale, a cutout is painted inside the screen rect exactly when the
device has one, and no cutout material reaches below the safe area. For
the reported device safePortraitY is now 141 (47pt x 3) instead of 189,
with the notch bottom at row 119, inside the reserved band.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing as iOS

Review feedback on the density scale, plus a catalog bug the same check
uncovered, plus the copyright headers the CI gate wants on the three
skin designer files this branch touches.

Downsampled panels. Rounding ppi/163 to an integer is right for every
Apple panel except the iPhone 6/6s/7/8 Plus, which lays out a 414pt-wide
surface, renders it at 3x into a 1242x2208 buffer and downsamples that
onto the 1080x1920 panel. Its scale is 1080/414, neither 2 nor 3, and the
integral rule turned the 20pt status-bar inset into 40px where the panel
wants about 52. DOWNSAMPLED_IOS_PANELS records the logical width, keyed on
the panel geometry because that is the physical fact identifying it rather
than a model name, and check_density_scales() now refuses to write a
catalog in which any iOS record's implied point density leaves the band
Apple actually ships (120-170 ppi per point). Emptying the table makes the
check report exactly the six Plus records, so it is not vacuous; a future
downsampled panel fails the generator instead of silently producing a
wrong safe area.

Platform detection. `"ios" in os_str` also matches Tecno's HIOS and
Meizu's Flyme AIOS, so 113 Android phones were on the iOS path: iOS theme,
SF Pro, iOS override names, iOS notch/home-indicator inference, and now an
integral iOS density scale. Detection matches on a word boundary and lets
an explicit "Android" win, since a Tecno record names both. The catalog is
migrated in place for exactly those 113 records and nothing is dropped --
it carries entries from scrapes whose platform rules predate the current
one, and re-running today's normalise() over all of them would have
deleted 143 real devices.

The fields that follow from the platform move into derive_platform_fields()
so the migration re-derives them through the generator's own rules instead
of a second implementation.

iPhone 8 Plus now reports safePortraitY=52 against a 2.6087 scale, and the
four devices checked before are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…as dp

Two review findings on the notch change.

Landscape safe area. skin_l.png is skin.png through Image.rotate90Degrees,
which maps source (x, y) to (height - 1 - y, x) -- the portrait TOP row
becomes the landscape RIGHT column, so the notch is drawn on the right.
buildProperties wrote safeTopPx as safeLandscapeX, reserving the LEFT edge,
which was harmless while the notch lived in a frame extension outside the
display and is not harmless now that it is painted on the screen: app
content rendered underneath the opaque shape on the right while an unused
band was reserved on the left. snapToSafeAreaInternal derives the right
margin as surfaceWidth - width - x, so writing the BOTTOM inset into X
leaves exactly safeTopPx on the right, where the notch is. The reasoning,
including the pixel mapping, is now a comment at the site so the mapping
cannot be re-derived backwards.

Unit label. The Info tab's safe-area fields said "pt" for every device.
The catalog and the conversion are in points on iOS and dp on Android, so
an Android skin told the editor the wrong unit and invited converting a
documented dp inset as though it were typographic points. The label
follows device.platformName.

Verified on the rotated raster rather than asserted: for an iPhone 13 Pro
Max the landscape insets are left=102 (34pt bottom) and right=141 (47pt
top), all 86132 notch pixels land on the right, none on the left, and none
falls outside an inset. Restoring the old mapping makes the check report
notch pixels at x=2673 outside both insets, so it is not vacuous.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Painting the notch on the display, which is where it sits on the hardware,
also put it on the simulator's input surface. JavaSEPort's round-screen
path accepts every coordinate inside the display rect, so a click on the
opaque notch reached whatever app content was hidden under it. Before this
branch the notch lived in a frame extension outside the display and the
click fell out of bounds, so this is a regression this branch introduced
and not pre-existing behaviour.

The skin file had no way to say "these pixels are not display", so the
designer now writes a `cutouts` property: display-relative x,y,w,h
rectangles, one per cutout. JavaSEPort parses it and drops a pointer press
inside one, along with the drag and release that follow -- both of those
deliver on `mouseDown` alone, so suppressing only the press would hand the
app a gesture it never saw start.

skin_map.png deliberately does NOT stand in for the new property, which is
the obvious cheap alternative and is wrong: every shipped round skin
(iPhoneX, the watch skins) already carries a map marking only the screen,
so hit-testing against it would also reject the screen's rounded CORNERS.
Those are opaque skin material but real touch surface on the hardware, so
that trade swaps a small fidelity gap for a new one. The same argument
rules out testing the skin image's alpha. A skin that declares no cutouts
behaves exactly as before, which covers every skin written until now.

applyCutouts and buildProperties now share cutoutRectPx() so the painted
shape and the declared hit area are the same rectangle by construction.

JavaSEPortSkinCutoutTest covers the parse, the malformed-entry fallback,
the landscape rotation and the bounds test; breaking the rotation makes it
report "expected 2658 but was 0", so it is not vacuous. The generator-side
harness additionally confirms, per device, that each declared rectangle
contains every painted cutout pixel and none outside it, and lies inside
the safe area.

Also records in density_scale() why Android dp is converted from the
physical panel ppi rather than DisplayMetrics.densityDpi, which was the
other review question: the simulator has no densityDpi, it derives
pixelMilliRatio from the skin's own ppi, and one dp there is exactly
ppi/160 px -- so this is the conversion that keeps the safe area in step
with the UI drawn beside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
build-linux-jdk8 went red on the previous commit. The new test passed;
CodenameOneExtensionTest failed two unrelated assertions -- @LargerText
read back 1.0 instead of 1.6, @orientation(PORTRAIT) read back false.

JavaSEPort's constructor assigns the static `instance`, and the test built
one to reach an instance method. CodenameOneExtension applies @LargerText
and @orientation through JavaSEPort.instance while the assertions read
them back through Display, which still holds the original implementation,
so the extension configured the orphaned port and Display reported its
defaults.

isPointerOnCutout takes its rectangle list explicitly and is static, with
a one-line instance overload for the pointer path, so the test never needs
a port. The reason is a comment on the method, because "make this static"
is exactly the kind of thing a later refactor undoes.

Reproduced locally in one JVM in CI's order -- a Display-initialising test,
then a class that constructs a throwaway port, then CodenameOneExtensionTest
-- which fails those same two assertions; the committed test in that slot
gives 21/21. The earlier local run missed it because Display had not been
initialised yet when the throwaway port was built.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two more review findings.

The documented landscape example was reversed. skin_l.png is skin.png
rotated clockwise, so the portrait top inset lands on the landscape RIGHT
edge and safeLandscapeX carries the BOTTOM inset. The snippet still showed
185 there from before that fix, which with width 2335 hands the island side
a 102px margin and the other side 185 -- the wrong way round, and the
sample is what readers copy. It is 102 now, with a line saying why.

A hole escaped its own rectangle. applyCutouts drew a circle of diameter
cw centred in the box, ignoring c.h, so any hole a user made non-square
painted opaque pixels outside the rectangle written into the cutouts
property -- and that rectangle is what the simulator hit-tests, so those
pixels stayed clickable. It is an ellipse filling the box now, which is
also what DevicePreview has always drawn (g.fillArc with the cutout's w and
h), so the preview and the exported skin agree.

The safe-area floor now measures the same rectangles, rather than
recomputing the extent from viewbox units: round(y) + round(h) and
round(y + h) differ by a pixel often enough, and the difference would put a
cutout edge one pixel below the reserved band.

Hit testing stays rectangular, and there is a comment at the property
saying why: safeTopPx is floored at the lowest cutout edge, so every
declared rectangle lies inside the reserved band where app content does not
render, and the corners of a box that a pill or ellipse does not fill cost
nothing. Carrying the shape instead would put a second copy of the
rasteriser's geometry in JavaSEPort, free to drift from the code that
paints the pixels, which is what cutoutRectPx exists to prevent. What has
to hold is that the paint never leaves the box, and now it does not.

Checked with three deliberately non-square, off-centre cutouts across five
devices: every painted pixel falls inside some declared rectangle, every
rectangle has paint in it, and every rectangle sits inside the safe area.
Restoring the circle makes that report 10918 escaped pixels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two more review findings.

The persisted wizard state survives a reload, and SkinModel.load() read
safeTop/safeBottom back verbatim. Those numbers were entered as preview
viewbox units and are now read as points or dp, so a session left by the
older build silently regenerates a different skin: a hand-tuned 40 on a
1080-wide 400ppi Android device meant 135px then and would mean 100px now.
The persisted model carries a schema version, and a session below the
current one takes its insets fresh from the device catalog. There is
nothing to convert back to -- a value tuned by eye was tuned against the
scaling this branch fixes -- and an untouched session got the same numbers
from the catalog anyway. A session with no device selected goes back to the
device step, where resetForDevice refills both values on the way through.
The migration is written back once rather than repeated on every reload.

The documented property example omitted the `cutouts` key the wizard now
writes. A skin rebuilt from that example would paint the opaque island and
give JavaSEPort.parseCutouts() nothing, leaving pointer events free to
reach the content underneath -- exactly the bug the key exists to close.
The example carries the real value for its device (377,53,452,132, checked
against the generator), and the guide's property list and cutouts section
mention it.

Also drops this branch's own fix for the signup-funnel test: master fixed
it in #5628 with an attribute parser rather than a looser regex, which is
the better of the two, so the rebase takes master's version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@shai-almog
shai-almog force-pushed the fix/skin-designer-safe-area-units branch from 955e310 to 896ea3a Compare August 29, 2026 17:44
@shai-almog
shai-almog merged commit 885214a into master Aug 30, 2026
42 of 44 checks passed
@shai-almog
shai-almog deleted the fix/skin-designer-safe-area-units branch August 30, 2026 03:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant