Skip to content

Native desktop windows - #5556

Open
shai-almog wants to merge 265 commits into
masterfrom
feat-desktop-windows
Open

Native desktop windows#5556
shai-almog wants to merge 265 commits into
masterfrom
feat-desktop-windows

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Codename One has no windowing API. Even on JavaSE, Mac, Win32 and Linux, where the OS has real windows, an app gets exactly one, welded to a single global "current Form": CodenameOneImplementation holds one currentForm, Display.edtLoopImpl paints one surface per tick, paintDirty uses one global paint queue clipped to getDisplayWidth()/getDisplayHeight(), and handleEvent routes every input event to one form. Everything that looks like a second window today — Sheet, InteractionDialog, ToastBar, Dialog — is an overlay inside the current form's layered panes.

This adds real native windows, each rendering its own component tree, on all four desktop targets, without changing the single-form model mobile depends on.

API

TopLevelContainer is the shared contract Form and Window both implement. Its members were chosen by counting actual getComponentForm().<method>() chains in CodenameOne/src, and every one of them was already public on Form with an identical signature, so Form needed nothing beyond the implements clause and asContainer() — a Java interface cannot extend a class, so without that bridge a TopLevelContainer reference cannot go anywhere a Component is wanted.

Window extends Container implements TopLevelContainer. Inside a window getComponentForm() returns null, by design; Component.getTopLevelContainer() is the new resolution API, and core now uses it internally. Desktop and Monitor are the public parallel to Display for "what screens exist and what windows are open", including per-monitor DPI and backing scale; Display keeps meaning "the main app surface" exactly as before.

Modality is enforced in core rather than per port, so it behaves identically everywhere: Display keeps a modal stack and handleEvent drops input to blocked windows. showModal() parks the caller through invokeAndBlock the way Dialog already does, which re-enters the event loop — so every other window stays live and repainting while a modal is up.

Implementation

The impl SPI is a single WindowManager facade returned from CodenameOneImplementation.getWindowManager(). Returning null is the capability query, so there is no separate isMultiWindowSupported() that could drift from it. Only genuinely universal operations are abstract; anything a port might not offer has a no-op default, so adding a capability later never breaks a port.

Per-window paint state moves into a PaintSurface value object with the main window as instance zero; getCodenameOneGraphics(), repaint(Animation), cancelRepaint and hasPendingPaints() keep their signatures, so every existing port still compiles and behaves. paintDirty()'s body is parameterized rather than globally rebound — a global "active surface" was rejected because Display.getDisplayWidth() is public and callable off the EDT, so a live binding would change its answer re-entrantly across ~210 call sites.

Events pack the window id into the type word (type | (windowId << 8)). Window 0 is numerically identical to the previous wire format, so drag coalescing and the stack-swap logic are untouched. The port is handed the id at creation and echoes it back, so there is no peer-to-window map on the off-EDT input path.

Ports: JavaSE (per-canvas graphics de-singletonization — getNativeGraphics used to return one shared instance, and isScreenGraphics was an identity check against one buffer, so a second window would have drawn into the first window's pixels), native Windows (Direct2D per-window render targets, GWLP_USERDATA identity, WM_DPICHANGED), native Linux (per-window cairo back buffer, GTK closure data), and Mac Catalyst (UIWindowScene per window). Peer components and native text editing work in every window on every one of the four. iOS, Android and JavaScript need no port changes at all: they inherit the false capability and the throw lives in core.

Latent bug fixed on the way

handleEvent returned offset unchanged when the form was null, while the caller loops while (offset < actualTmpPointer) — an infinite EDT spin. It is unreachable today only because all nine entry points guard on getCurrentForm() != null; window disposal with events in flight makes it reachable. It is now a skipEvent that drains the packet so the rest of the batch still dispatches.

Testing

Core unit tests drive a scriptable fake WindowManager on TestCodenameOneImplementation — settable, defaulting to null, so the unsupported path is the default — covering lifecycle, paint isolation, event routing, modality including a modal window nested in a modal dialog, the TopLevelContainer contract, and a fake multi-monitor table at mixed DPI. JavaSE port tests cover the per-canvas graphics resolution, which is the riskiest edit here and had no coverage before.

The centrepiece is a windowed screenshot family in scripts/hellocodenameone: representative UI re-run inside a real window at several sizes and compared against its own goldens. A picture of a window proves nothing; layout, scrolling, graphics, layered overlays, native editing and modality rendering correctly on a non-primary surface is the actual claim. The three sizes, including a deliberately non-square one, are what prove content lays out to the window rather than to Display.getDisplayWidth(). This needed per-window capture on every port, since the existing pipeline can only see the main framebuffer.

Mac Catalyst was built and run on real hardware for this branch rather than left to CI, because it is the hardest of the four. That found four defects compiling never would have: capture() was unimplemented; the readiness probe was a false positive; captures were taken before the first paint; and the scene was never asked for the geometry the window was created with, so several captures came out at the main display size with the window's content in the corner.

Known scope limits, documented

HTMLComponent, accessibility on secondary windows, Dialog.show() from inside a window and form transitions into or out of one are out of scope for v1 and called out in the guide. Display.getDisplayWidth()/getDisplayHeight() keep reporting the main window; components inside a window use their top level's size.

🤖 Generated with Claude Code

shai-almog and others added 10 commits August 17, 2026 04:38
Adds a Desktop Windows chapter next to Desktop Integration, covering the whole
feature: where windows exist and where they throw, the Form/Window relationship
through TopLevelContainer, lifecycle and close vetoes, chrome and the two
coordinate systems, modality, monitors and per-monitor DPI, events, peers and
native editing, and the Mac Catalyst opt-in.

Two things are called out rather than buried, because they are what will
actually catch someone out. getComponentForm() returns null inside a Window, and
the failure mode is silence rather than an exception, since most code guards on
null and quietly does nothing -- so a component that will not scroll or focus in
a window has a named cause. And Catalyst multi-window needs the
macNative.multiWindow build hint, because a second window is a second scene and
that requires a process-wide Info.plist key.

Vale reports zero issues at suggestion level, LanguageTool zero matches across
the guide, and the paragraph capitalization check passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a port-level test for the riskiest edit in this work, which had no coverage
before and sits in the paint path where a regression shows up as wrong pixels
rather than an exception.

Two canvases must resolve to two distinct screen buffers -- sharing one is
exactly what would make a second window draw into the first window's pixels. And
isScreenGraphics has to answer true for a secondary window's buffer as well as
the primary one, but still false for a mutable image: drawNativePeerImpl uses
that answer to decide whether to undo the zoom scale, so a wrong answer
mis-scales a window's peer components.

222 JavaSE port tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Compiling CN1MacWindows.m against the actual Mac Catalyst SDK -- which the
earlier commit never did -- turned up three defects that would have shipped.

Scene-to-window matching was a race. Creation returns a slot immediately and
requests the scene asynchronously, and the arriving scene was handed to the
first unattached slot. Two windows opened in quick succession could therefore
swap identities. Scenes are delivered in request order, so the pending slots are
now a FIFO, enqueued on the same main-thread turn as the request; a window
destroyed before its scene arrives leaves the queue.

The presented frame was a use-after-free waiting to happen. flushGraphics
allocates a local Java int[], and the native side wrapped that pointer in a
CGBitmapContext, then used the resulting image on a later main-queue turn -- by
which time the array is garbage and the collector may have reclaimed or moved
it. The pixels are now copied, and handed to a CGDataProvider with a release
callback rather than a bitmap context: CGBitmapContextCreateImage is
copy-on-write, so it is not defined when the backing buffer becomes free to
release, whereas the provider makes that lifetime explicit.

The alpha format was wrong. getRGB returns straight ARGB and the image declared
kCGImageAlphaPremultipliedFirst, which would darken every pixel that is not
fully opaque. A window's content is opaque, so it now skips the alpha channel.

Also uses slotForScene, which was dead code, to reject a scene that was already
adopted.

Verified by compiling both CN1MacWindows.m and CodenameOne_GLSceneDelegate.m for
arm64-apple-ios-macabi against the real SDK: clean with -Wall. The same file
built for plain iOS exports zero CN1MacWindow symbols, confirming the whole
implementation compiles out and the iOS binary is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Compiling the native sources -- which the port commits never did -- turned up
three defects, one of them serious.

WM_CN1_DESKTOPWINDOW was defined as WM_APP + 24, which WM_CN1_WIDGET already
uses. Widget ops and desktop-window ops would have been delivered to each
other's handlers, both of them casting the same LPARAM to a different struct.
Moved to WM_APP + 25; the duplicate is now checked for rather than assumed
absent.

The two COM release calls in the Windows window layer did not resolve. This port
compiles its Direct2D translation units as C++ and resolves COBJMACROS-style
call sites through an explicit shim in cn1_windows_comc.h, which defines only the
methods the port actually uses -- and it had no Release entry for either the
HWND render target or the solid colour brush. Added both, in the shim's existing
style, rather than reaching around it.

On Linux, the GtkWidget-typed accessors were declared in cn1_linux.h. That header
is included by translation units that have no GTK on their include path, and
declaring a GtkWidget* there breaks them. Moved to cn1_linux_gfx.h, which is the
header that includes gtk and where the equivalent existing declarations already
live.

Verified with the real toolchains available here: cn1_linux_desktopwindow.c is
clean under -Wall against GTK 3, and every Windows translation unit including the
new one now reports zero errors of its own. The remaining diagnostics in both
ports reproduce identically on master and come from compiling Linux and Windows
sources on a Mac.

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

Findings from actually building and running the Catalyst app on a Mac, which no
earlier commit had done.

MacWindowManager never implemented capture(), so it inherited the base class's
null. Every windowed screenshot test failed with "Window capture returned null".
On this platform the window's content is already rendered into a mutable image,
so a capture is that raster.

The screenshot harness waited a fixed 1.2s on a UITimer bound to the current
form. Catalyst creates its window asynchronously -- it asks the system to
activate a scene and is handed one back later -- so a fixed delay is both too
long on the fast ports and too short here, and the timer's bound form is not the
window anyway. It now polls for the window actually being renderable, re-queuing
through callSerially rather than sleeping: the paint that makes it renderable
happens on that very thread, so blocking there would stop the condition ever
becoming true.

macNative.multiWindow now defaults to false for the sample as well. That is
measured, not cautious: with multiple scenes enabled, this suite's
OrientationLockScreenshotTest captures its landscape frame and then times out
after 20s trying to restore portrait. Catalyst treats a multiple-scene app's
windows more like Mac windows and honours orientation requests less, so the
regression belongs to the Info.plist key rather than to the window code. This
gives the warning already in IPhoneBuilder a concrete mechanism instead of
folklore.

What the run did confirm: the Info.plist key is emitted correctly,
CN1MacWindows.m compiles clean under Xcode's own flags, the app boots with
multiple scenes enabled and runs all 178 tests without the crash the older
comment described, and MultiWindowApiTest passes on the supported path -- so a
real Catalyst Window is created, registered, resolves getTopLevelContainer() to
itself, reports null from getComponentForm(), reports its monitor and scale, lays
out to its own size rather than the display's, and deregisters on dispose.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two corrections from further runs on real hardware.

The previous commit blamed multiple scenes for OrientationLockScreenshotTest
timing out while restoring portrait. That was wrong. With the key still enabled
the test passed in the following runs, so it was a slow-machine flake -- the
machine was compiling at the time -- not a consequence of the Info.plist key.
The hint stays off by default anyway, on the honest grounds that it changes
Catalyst windowing process-wide and an application should opt into that rather
than have it changed underneath it.

The screenshot harness was also asking the wrong question. It waited for the
window to report itself showing at its requested size, but a window reports the
size it was asked for before the platform has actually produced anything -- on
Catalyst the scene arrives asynchronously -- so both were true within
milliseconds and the capture then failed. Readiness is now "a capture succeeds",
which is exactly the condition the next line depends on and is correct on every
port.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Running the Catalyst suite showed every windowed screenshot emitting a blank
frame: the sizes differed correctly per window, but the content did not, and the
harness reported the captures as duplicates of each other.

The cause is that a window's raster exists from the moment it is shown, so a
capture taken before the first paint cycle returns an empty frame of the right
size rather than failing. The harness had no way to tell the two apart.

Window now records when a paint cycle has completed and exposes hasPaintedOnce(),
and the screenshot harness waits on that as well as on the capture succeeding.
This is useful beyond the tests: any tooling that wants a window's content rather
than its dimensions needs the same distinction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Built and ran the conformance suite as a Mac Catalyst app on real hardware with
multi-window enabled: 0 failures across all 178 tests, and all 14 windowed
screenshots captured with distinct hashes and no duplicates -- including the
modal case, whose background window is non-blank while a modal is up, which is
the property that proves the event loop keeps servicing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The extra macNative.multiWindow switch existed only because Mac Catalyst
scenes were unverified. They are verified now -- the whole conformance
suite runs as a Catalyst app with multiple scenes enabled -- so gating it
behind a second opt-in only meant CI never exercised the feature.

UIApplicationSupportsMultipleScenes is a process wide Info.plist key, so
it is still keyed off macNative.enabled rather than set unconditionally:
that key is true for the Mac Catalyst slice only and false for iPhone and
iPad builds, which keeps the iOS output byte for byte identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Inspecting the Mac Catalyst captures rather than only their hashes showed
three defects that distinct hashes had hidden.

A Catalyst scene was never asked for the geometry the window was created
with, so the system handed it the main scene's size. The window then laid
out into a raster that did not match the request: several captures came
out at the main display size with the window's content in the corner.
The scene now requests the pending geometry as soon as it connects, and
both that request and setBounds convert Codename One's pixels to UIKit's
points. getBounds reports pixels to match getWidth and getHeight.

The readiness probe accepted a window that had painted and could be
captured, neither of which implies the size settled -- which is how the
mismatch reached a golden in the first place. It now also requires the
window and the captured image to be exactly the requested size, so a
platform that cannot grant it fails loudly instead of baking a wrong
baseline.

A window used its own Window and WindowContentPane UIIDs, which no theme
written before desktop windows existed defines, so it painted nothing and
came up black. A window is a top level surface, so it now takes the Form,
ContentPane and TitleArea styles every theme already has.

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

@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

f.keyPressed(inputEventStackTmp[offset]);

P1 Badge Dispatch key events to the window's focused component

When f is a Window, this invokes the inherited Container.keyPressed(), because Window does not override the key handlers. That implementation only forwards to a container lead component, so ordinary focused controls receive no physical-key input, focus traversal never runs, and the listeners stored by Window.addKeyListener() are never fired. Window needs form-equivalent key pressed, released, repeated, and long-press dispatch.

ℹ️ 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 CodenameOne/src/com/codename1/ui/Window.java Outdated
Comment thread CodenameOne/src/com/codename1/ui/Window.java Outdated
Comment thread Ports/WindowsPort/src/com/codename1/impl/windows/WindowsWindowManager.java Outdated
Comment thread CodenameOne/src/com/codename1/ui/Window.java Outdated
Comment thread CodenameOne/src/com/codename1/ui/Window.java Outdated
Comment thread CodenameOne/src/com/codename1/ui/Window.java
Comment thread CodenameOne/src/com/codename1/ui/Window.java
@shai-almog

shai-almog commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

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

shai-almog and others added 2 commits August 17, 2026 04:56
The guide gate requires every source block to come from a tagged fixture
under a compiled source root, so the snippets are checked by javac rather
than only by eye. This chapter had them inline.

Two of them did not survive the move as written: one relied on an ellipsis
inside a switch and another on a call that has no declaration, so both are
now complete code. Also documents the styling a window starts out with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closing one window and opening another failed on Mac Catalyst with "scene
invalidated before create completion": the system does not hand out a
scene session while a previous destruction is still in flight, and the
window that asked was left without one. That is an ordinary sequence, so
a closed window now parks its scene for the next window to adopt rather
than destroying it.

The size query also answered with the size that was requested while the
scene did not exist yet, so a window looked correctly sized during exactly
the interval when nothing was known about it. It now answers zero until
there is something real to measure, and show() keeps the requested size
until a port delivers a real one instead of collapsing the window to
nothing.

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

github-actions Bot commented Aug 17, 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)
    • 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.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@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: 338ee1a6f1

ℹ️ 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/LinuxPort/nativeSources/cn1_linux_desktopwindow.c Outdated
Comment thread Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java Outdated
Comment thread CodenameOne/src/com/codename1/ui/Window.java Outdated
Comment thread CodenameOne/src/com/codename1/ui/Window.java Outdated
Comment thread CodenameOne/src/com/codename1/ui/Window.java
@github-actions

github-actions Bot commented Aug 17, 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 17, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 9.04% (8948/99009 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.84% (46262/523333), branch 3.46% (1713/49575), complexity 3.46% (1830/52883), method 5.31% (1478/27827), class 10.70% (398/3720)
    • 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.04% (8948/99009 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.84% (46262/523333), branch 3.46% (1713/49575), complexity 3.46% (1830/52883), method 5.31% (1478/27827), class 10.70% (398/3720)
    • 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 194ms / native 142ms = 1.3x speedup
SIMD float-mul (64K x300) java 132ms / native 188ms = 0.7x 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 74.000 ms
Base64 CN1 decode 84.000 ms
Base64 native encode 365.000 ms
Base64 encode ratio (CN1/native) 0.203x (79.7% faster)
Base64 native decode 283.000 ms
Base64 decode ratio (CN1/native) 0.297x (70.3% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog

shai-almog commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 163 screenshots: 163 matched.
Native Windows port (x64 / Intel-AMD): full hellocodenameone screenshot suite rendered offscreen with Direct2D/DirectWrite, plus the real benchmarks (base64 native/CN1/SIMD, image createMask/applyMask/modifyAlpha/PNG/JPEG, SSE2 SIMD kernels). Compared against the in-repo baseline in scripts/windows/screenshots.

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 62ms / native 5ms = 12.4x speedup
SIMD float-mul (64K x300) java 63ms / native 5ms = 12.6x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
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 195.000 ms
Base64 CN1 decode 135.000 ms
Base64 SIMD encode 102.000 ms
Base64 encode ratio (SIMD/CN1) 0.523x (47.7% faster)
Base64 SIMD decode 98.000 ms
Base64 decode ratio (SIMD/CN1) 0.726x (27.4% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 27.000 ms
Image createMask (SIMD on) 18.000 ms
Image createMask ratio (SIMD on/off) 0.667x (33.3% faster)
Image applyMask (SIMD off) 52.000 ms
Image applyMask (SIMD on) 213.000 ms
Image applyMask ratio (SIMD on/off) 4.096x (309.6% slower)
Image modifyAlpha (SIMD off) 58.000 ms
Image modifyAlpha (SIMD on) 50.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.862x (13.8% faster)
Image modifyAlpha removeColor (SIMD off) 58.000 ms
Image modifyAlpha removeColor (SIMD on) 53.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.914x (8.6% faster)

Every one of these was a real defect.

show() never initialized the hierarchy, unlike the setCurrent() path a Form
goes through, so initComponent() never ran, the look and feel was never
bound and native peers were never attached for anything added before the
window was shown.

Modality was only registered by showModal(), so a window given a modality
type and then shown normally set the platform's modal flag while the
framework kept delivering input to the windows behind it. The framework
blocker and the native flag now move together, exactly once, through
acquireModal/releaseModal -- which also fixes a much worse leak: the
Windows port implements the native flag by disabling the main HWND, and
nothing ever re-enabled it, so the application was unusable after any
modal window closed. That is counted rather than flagged, so a modal
nested inside another does not release the outer one's block.

hide() left the component tree visible, so its components kept queuing
paints onto a surface that is never painted; hasPendingPaints() then kept
the event dispatch thread awake spinning on work that would never drain.

The whole window overlay had no sized layers, unlike Form's, so Sheet,
InteractionDialog and ToastBar had nothing to render into -- and it was
made the hit testing root unconditionally, so once anything installed a
layer every click landed on the overlay and the content and title stopped
responding. Both now match Form.

A failed native window creation produced a phantom window that painted
through null graphics; it throws now. An owned window was never disposed
with its owner despite the API promising it. The vetoable close listeners
were fired a second time from dispose(), so one user close ran a save
twice; dispose() now reports the new WindowEvent.Type.Disposed instead.
setMinimumWindowSize() only stored the value: it reaches the port now and
is clamped when a resize is delivered, for ports that cannot express it.

The mouse wheel had no window-aware path at all -- Linux requested scroll
events without connecting a handler, the Windows window procedure had no
wheel case, and JavaSE resolved the target from the current form -- so
rolling the wheel over a window either did nothing or scrolled the main
form. And a JavaSE peer inside a window had only its attachment redirected:
positioning still converted from the main canvas and removal still went to
the original frame, so it was offset by the two frames' distance and left
attached after removal.

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

shai-almog commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 163 screenshots: 163 matched.
Native Windows port (arm64 / Apple Silicon - Arm): full hellocodenameone screenshot suite rendered offscreen with Direct2D/DirectWrite, plus the real benchmarks (base64 native/CN1/SIMD, image createMask/applyMask/modifyAlpha/PNG/JPEG, NEON SIMD kernels). Compared against the in-repo baseline in scripts/windows/screenshots.

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 57ms / native 4ms = 14.2x speedup
SIMD float-mul (64K x300) java 56ms / native 3ms = 18.6x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 native bridge unavailable (CN1 + SIMD + image benchmarks only)
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 266.000 ms
Base64 CN1 decode 154.000 ms
Base64 SIMD encode 65.000 ms
Base64 encode ratio (SIMD/CN1) 0.244x (75.6% faster)
Base64 SIMD decode 63.000 ms
Base64 decode ratio (SIMD/CN1) 0.409x (59.1% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 12.000 ms
Image createMask (SIMD on) 8.000 ms
Image createMask ratio (SIMD on/off) 0.667x (33.3% faster)
Image applyMask (SIMD off) 25.000 ms
Image applyMask (SIMD on) 20.000 ms
Image applyMask ratio (SIMD on/off) 0.800x (20.0% faster)
Image modifyAlpha (SIMD off) 18.000 ms
Image modifyAlpha (SIMD on) 12.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.667x (33.3% faster)
Image modifyAlpha removeColor (SIMD off) 21.000 ms
Image modifyAlpha removeColor (SIMD on) 145.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 6.905x (590.5% slower)

shai-almog and others added 2 commits August 17, 2026 05:34
Each one fails against the code as it was: the hierarchy left
uninitialized by show(), a phantom window from a failed native create,
a modal flag set once and never cleared, close listeners fired twice for
one close, an owned window surviving its owner, a minimum size that
reached nothing and clamped nothing, and a hidden window leaving paint
work behind that keeps the event dispatch thread awake.

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

@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: 13311a9bed

ℹ️ 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 CodenameOne/src/com/codename1/ui/Display.java
Comment thread CodenameOne/src/com/codename1/ui/Window.java
Comment thread Ports/WindowsPort/src/com/codename1/impl/windows/WindowsWindowManager.java Outdated
Comment thread Ports/WindowsPort/src/com/codename1/impl/windows/WindowsWindowManager.java Outdated
Comment thread CodenameOne/src/com/codename1/ui/Window.java
Comment thread Ports/JavaSE/src/com/codename1/impl/javase/JavaSEWindowManager.java
Two independent reasons the capture disagreed with the window it came
from, both visible as a screenshot at the wrong size.

The content view was created while the window still had zero bounds and
relied on an autoresizing mask to grow. The mask distributes a resize
*delta*, so a view that starts at zero stays at zero -- and since the size
query read that view, the window reported nothing at all. The content view
is now pinned to the controller's view on every layout, and the size query
reads the controller's view, which is what the window manager lays out and
what viewDidLayoutSubviews reports back.

The raster was sized from the scene's drawable. The scene arrives and
resizes asynchronously, so a raster allocated mid-resize was left holding
an intermediate size that nothing later reconciled: the framework then
painted into it at its own size and the capture disagreed with the window.
It is sized from the framework's window now, which is what was laid out
and painted; the drawable is only the fallback until a window exists.

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

@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: 1670ca0579

ℹ️ 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 CodenameOne/src/com/codename1/ui/Display.java Outdated
Comment thread CodenameOne/src/com/codename1/ui/Window.java
Comment thread docs/developer-guide/Desktop-Windows.asciidoc Outdated
Key events never reached a window's focused component. Window inherited
Container's handler, which only forwards to a lead component, so focused
buttons saw nothing, arrow traversal did not work and addKeyListener never
fired in a secondary window. It dispatches keys itself now, the same way
Form does minus the menu bar it has no equivalent of.

Hiding a modal window left it blocking. HIDE_ON_CLOSE, or a plain hide(),
cleared visibility while the window stayed on the modal stack with the
platform's own modal flag still set, so what it covered kept rejecting
input for a window nobody could reach any more. showModal() also treats
hidden as finished, since parking the caller for a window that will never
be disposed is a hang.

Modality told the port nothing about its scope. A port implements it by
disabling the window that is blocked, so window scoped modality was
disabling the main window -- an unrelated part of the application. The
SPI now carries the scope and the blocked window's peer.

An owned window's native ownership was never established: every port
ignored parentPeer, so no platform knew the child had to stay above its
owner. Windows passes the owner HWND, Linux sets the transient parent
(which is also what scopes GTK's modality), and JavaSE creates an owned
window as a JDialog, since Swing expresses ownership only through the
owner passed at construction and JFrame has no owned form.

setUtilityWindow() only set a field. It reaches the port now:
WS_EX_TOOLWINDOW on Windows, GDK_WINDOW_TYPE_HINT_UTILITY on Linux and
Window.Type.UTILITY on JavaSE.

A minimized window went on being treated as displayed, because Window
inherited Container's inert hideNotify: it kept being painted and an
animation in it would keep the event dispatch thread awake indefinitely.

Finally, a resize kept paint work that had been computed against the old
geometry. A port that reallocates its buffer on resize painted those
stale rectangles into the fresh, larger one and left the rest unpainted --
which is what the Mac Catalyst captures showed as content in the corner of
a bigger frame. The queue is dropped and the whole window repainted, and
hasPaintedOnce() resets so nothing captures the half painted surface.

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

@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: bea352c1ee

ℹ️ 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
Comment thread Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java
Comment thread CodenameOne/src/com/codename1/ui/Window.java

@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: eb4ca866ef

ℹ️ 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 CodenameOne/src/com/codename1/ui/Desktop.java
Comment on lines +756 to +757
gtk_window_move(GTK_WINDOW(w->window), op->a, op->b);
gtk_window_resize(GTK_WINDOW(w->window), op->c > 0 ? op->c : 1, op->d > 0 ? op->d : 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Translate Linux outer bounds before calling GTK resize

For decorated GTK windows using server-side window-manager chrome, gtk_window_resize() sizes the client window rather than the outer frame, while Window.setWindowBounds() defines its dimensions as including native chrome. Consequently, restoring or centering a saved rectangle creates a window larger than requested, and the gtk_window_get_size() round-trip below reports the client size rather than the actual desktop extent. Convert between outer and client dimensions using the frame extents so Linux honors the same bounds contract as the other ports.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The finding is correct, and checking it turned up something wider than the report, so I want to lay out the evidence before changing anything.

Window.setWindowBounds() documents its dimensions as native geometry including chrome. Comparing what each port actually produces for the same 600x450 request, from the committed baselines:

port requested captured
Windows 600x450 584x411
Linux x64 600x450 600x450
Linux arm64 600x450 600x450
Mac Catalyst 600x450 600x450

Windows honours the contract -- 16 wide and 39 high of chrome come off the client area. Linux does not, exactly as you describe: gtk_window_resize() sizes the client window and gtk_window_get_size() reads it back. Catalyst does not either, which your comment does not mention and which matters for the same reason.

So this is one contract honoured by one port out of three, not a Linux bug in isolation.

What I have not done is fix it, and I would rather say why than quietly leave it or quietly break something:

  • Correcting Linux and Catalyst changes every windowed capture on those ports from the requested size to the requested size minus chrome. That is 14 baselines on Linux x64, 14 on Linux arm64 and 14 on Catalyst -- 42 images that would all have to be re-recorded.
  • I cannot run GTK or Catalyst here, so I could neither verify the frame-extent arithmetic nor look at the re-recorded images for anything beyond their dimensions. Re-baselining 42 screenshots I cannot inspect is precisely the move that would have buried the double-composited title earlier in this PR.
  • There is a timing trap in the obvious implementation: gdk_window_get_frame_extents() returns the client rectangle before the window is realized, so insets read as zero at creation -- when setWindowBounds is most often called -- and the compensation silently does nothing.

My recommendation is to fix all three ports together in a change of its own, with the baselines re-recorded in the same commit and looked at by someone who can run those platforms. If you would rather I do it inside this PR I will, but I did not want to spend a green Linux port and 42 unverifiable baselines on a judgement I can make but cannot check.

Leaving this thread open deliberately, since it is not fixed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Now fixed in c1e1a7680e, on both ports, per the decision to do it here rather than in a follow-up.

Linux. Every request and readback is converted through the frame extents. The conversion cannot happen where the request arrives, for the reason flagged earlier: gdk_window_get_frame_extents() reports the client rectangle until the window is realized and the window manager has attached a frame. So the requested outer size is held and applied from configure-event once the chrome is measurable, with the pending flag cleared before the resize that re-enters that handler.

The zero-inset case needed care, because "realized but not yet framed" and "genuinely borderless" are the same reading. It is disambiguated by asking whether decorations were requested, not by the numbers -- answering "no chrome" for a decorated window would bake it in permanently.

Catalyst turned out to disagree with itself, which is worth recording. CN1MacWindowSetBounds already requested a system frame and carried a comment saying so, while creation converged on a content size -- so the same window reported a different size depending on which call had placed it. Creation now requests the system frame too, and the settler compares system frame against system frame via effectiveGeometry.systemFrame (same macCatalyst 16.0 availability as initWithSystemFrame:). Because request and readback are finally the same quantity, the computed chrome correction is gone: the settler only ever re-asks for the frame it first asked for, which structurally removes the 120x120-minimum and 1700x400-overshoot failures the old capped correction existed to contain.

Size restrictions had to move with it. UISceneSizeRestrictions is in content points, so pinning a non-resizable window to the requested outer size would demand a content area the size of the whole window and the frame could never be granted. Restrictions are relaxed for the request, and a fixed-size window is pinned once its geometry settles, to the content it actually got.

Correcting my own estimate: it is not 42 baselines. Linux CI runs bare Xvfb with no window manager, so nothing attaches a frame, the insets are genuinely zero, and the code defers rather than deducting a phantom chrome -- there the client area really is the outer size. Linux goldens should be unchanged; only mac-native's 14 are expected to move.

Verification, and its limits. I have no Linux machine and the local Catalyst build is currently blocked by an unrelated ParparVM issue (Window/Desktop/TopLevelContainer are not translated, so the generated test includes headers that were never emitted -- the translator is reading the current core, since recent classes like CSSColor translate fine). So both are verified through CI, not by hand here.

What I could test directly is the inset arithmetic, which I extracted into a pure function for exactly that reason. All ten decision cases pass, including the deferral trap, and the end-to-end result is the part worth reading:

requested outer -> client, with 16x39 of chrome:
     400x300  ->  384x261
     600x450  ->  584x411
     900x700  ->  884x661
    1000x400  ->  984x361

Those are byte-for-byte the sizes the Windows goldens already contain. The three ports now compute the same thing.

…ture the list host

Three review findings.

A live resize of the main surface queued every notification. 400 of them consume
999 of the stack's 1000 slots, after which the final size is dropped -- the
hierarchy stays laid out for a size the surface no longer has -- and the releases
behind it go with it, since a size change may use the termination reserve. That
last part is mine: making size changes non-droppable is what let them exhaust the
reserve. A queued size packet is now overwritten by a newer size instead of
followed, the way drags already coalesce there, which keeps the ordering the
packed stack gives.

Guarding addMonitorListener() against a null implementation stopped the throw but
not the silence: the ports start watching for display changes when their window
manager is first created, so a listener registered before init and never followed
by anything touching the desktop heard nothing. Display.init() now starts the
watch if anything is listening.

GenericListCellRenderer registered its monitor on a resolved top level and
deregistered from a freshly resolved one, so a list removed or reparented while a
ticker ran left the monitor on the original for good.

That last one is the same class I said was swept, and it escaped a third distinct
flaw in the sweep: the pattern required "x = getTopLevelContainer()" and this is
"x = parentList.getTopLevelContainer()". Re-run allowing any receiver, it is the
only risky site left; the other eight re-resolve inside deinitialize(), where the
component is still attached.

5354 core tests; PMD, SpotBugs and Checkstyle at zero. The resize test grows the
stack by 999 slots without the coalescing.

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

@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: 78e6d90152

ℹ️ 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".

Both scene-manifest checks scanned the entire injection, so an unrelated
dictionary carrying UIApplicationSupportsMultipleScenes as true, or something
shaped like a window role, answered for a manifest that enables and configures
neither. The build passed and Window was still unsupported on the device, which
is the exact failure this guard exists to prevent.

They now run against the manifest's value element. A nested UISceneConfigurations
dictionary inside it is still in scope, since the element walk honours nesting.

The test asserts both sides: the unscoped lookup answers true for the bad
manifest, the scoped one answers false. 25 tests here, 889 in the plugin.

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

@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: 1ad4acaeb8

ℹ️ 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 CodenameOne/src/com/codename1/ui/plaf/DefaultLookAndFeel.java Outdated
…plist value

The modern pull-to-refresh spinner allocated a fresh Animation on every paint and
registered it. registerAnimated de-duplicates by identity, so a new instance is
never the one already there: the animation list grew by one per frame and nothing
ever came off it, leaving the event dispatch thread awake for good once a refresh
had run. One animation is kept for the spinner now, registered each frame as the
same instance, and released from the top level that took it when spinning ends.

The plist value walk searched for its closing tag with a raw indexOf, so a comment
containing "</dict>" closed the element early. Validation then read a prefix of
the manifest and rejected a build that is correctly configured. It skips comments
now, which is the rule every other search in that guard already followed and the
one I failed to carry into the nesting scan when I wrote it.

26 tests there, 890 in the plugin, 5354 core; PMD, SpotBugs and Checkstyle at
zero. The commented-closing-tag case fails against the raw search.

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

@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: 519efc38ac

ℹ️ 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 CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java Outdated
The activation filter I added for window pointers kept a single set of state, so
two windows with a contact down at the same time shared it -- a supported path on
Linux, where each window tracks its own touch sequence. A press or release in one
window reset the other's filter, and a drag activated in one carried the other's
first jitter straight past its threshold.

It is keyed by window now, in slots claimed on press and released on release, the
way the framework already keys press targets, long presses and drag histories.
More simultaneous window drags than there are slots lets the gesture through
rather than swallowing it: the filter is a refinement, and suppressing real input
is the worse failure.

The threshold state was the only part of this that I left global, having made the
region and the surface size window-aware in the same change -- so the review is
right that it was the outlier.

148 tests in WindowTest, 5355 core; PMD, SpotBugs and Checkstyle at zero. The new
case drags window B on one pixel of jitter when the state is shared.

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

@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: 540a283d11

ℹ️ 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 CodenameOne/src/com/codename1/components/InteractionDialog.java Outdated
setTimeout() scheduled the timer immediately, against whatever resolveHost()
answered at that moment. Called before the dialog is shown -- which is the
natural order -- that is the current form, so a popup later anchored in a window
had its timeout bound to the wrong surface, and if that form was replaced its
animations stopped and the dialog never timed out. In an application with no form
at all, resolveHost() answers null and UITimer.schedule() threw.

The timeout is recorded and started when the dialog is shown, from both show
paths, once there is a host to bind it to. Setting one while already showing
still starts it immediately.

Two tests: a timeout set before an anchored popup ends up bound after the window
is resolved, and setting one with no form at all no longer throws. The second
throws a NullPointerException against the old code.

5357 core tests; PMD, SpotBugs and Checkstyle at zero.

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

@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: be0898d763

ℹ️ 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 Outdated
shai-almog and others added 2 commits August 23, 2026 15:24
# Conflicts:
#	docs/website/data/port_status_reports/android.json
#	docs/website/data/port_status_reports/ios-gl.json
#	docs/website/data/port_status_reports/ios-metal.json
#	docs/website/data/port_status_reports/javascript.json
#	docs/website/data/port_status_reports/linux-arm64.json
#	docs/website/data/port_status_reports/linux-x64.json
#	docs/website/data/port_status_reports/mac-native.json
#	docs/website/data/port_status_reports/tvos.json
#	docs/website/data/port_status_reports/watchos.json
#	docs/website/data/port_status_reports/windows-arm64.json
#	docs/website/data/port_status_reports/windows-x64.json
#	scripts/hellocodenameone/conformance/test_port_status.py
getWindowManager() lazily created the manager with an unsynchronized null
check, and both of its entry points are reachable off the EDT --
Desktop.isSupported() and the Window constructor are callable from any
thread. Two threads could each see a null field and each construct one.

The cost is not a wasted allocation. The constructor starts the
monitor-topology poller, a daemon timer that wakes every two seconds. Only
the last manager stays reachable through the field, so only that one can
ever be stopped: the loser's poller keeps running for the life of the
process, reporting every topology change a second time and outliving the
deinitialize() meant to end it -- across a later Display restart included.

Both the lazy init and the matching teardown in deinitialize() now run
under one lock.

JavaSEWindowManagerLazyInitTest races 12 threads through getWindowManager()
for 25 rounds and asserts they all get the same instance, since a second
instance is precisely a second poller. Without the fix it fails on round 0
with 5 managers built and 4 unstoppable pollers left behind.

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

@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: a2f952b974

ℹ️ 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/LinuxPort/nativeSources/cn1_linux_desktopwindow.c
shai-almog and others added 2 commits August 23, 2026 15:52
Two-finger touchpad scrolling did nothing on the Linux port -- in secondary
windows and, identically, in the main window, which had the same gap from
the start.

A touchpad reports no discrete steps, so GDK emits only a GDK_SCROLL_SMOOTH
event for it: gdk/wayland/gdkdevice-wayland.c emits a discrete event only
when the device reports discrete_x/discrete_y, and the XI2 backend turns
scroll valuators straight into a smooth event. Neither widget selected
GDK_SMOOTH_SCROLL_MASK, and gdkwindow.c drops a smooth event before delivery
when the window has not selected that mask, so the scroll callbacks were
never invoked at all for a touchpad.

Both widgets now select the mask and translate the smooth deltas. Selecting
it also makes GDK drop the pointer-emulated discrete events a real wheel
produces, so the discrete branches and the smooth branch cannot both fire
for one physical movement.

The deltas cannot be forwarded one for one: they are fractions of a notch
and arrive continuously, while wheelUnits() on the Java side floors any
sub-notch delta to a whole notch -- forwarding each one would turn a gentle
drag into a page-per-frame stampede. cn1LinuxTakeWholeNotches() dispatches
whole notches and carries the remainder, per window so two windows cannot
consume each other's partial notches, and an is_stop marker clears it so a
gesture cannot inherit a partial notch from the previous one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Window.setWindowBounds() defines its dimensions as native geometry including
chrome. Windows honoured that -- a 600x450 request yields a 584x411 client
area -- while Linux and Mac Catalyst both sized the client area to the
requested figure, so the same request produced a different window on each of
the three ports and a bounds round trip disagreed with itself by the chrome.

Linux: gtk_window_resize() and gtk_window_get_size() both speak the client
area, so every request and readback is now converted through the frame
extents. The conversion cannot happen where the request arrives:
gdk_window_get_frame_extents() reports the client rectangle until the window
is realized and the window manager has attached a frame, which is normally
after setWindowBounds has been called -- and zero insets are
indistinguishable from a genuinely borderless window except by asking
whether decorations were requested. So the requested outer size is held and
applied from configure-event once the chrome is measurable, with the pending
flag cleared before the resize that re-enters that handler.

Catalyst: the port already disagreed with itself. CN1MacWindowSetBounds
requested a system frame and said so, while creation converged on a *content*
size, so a window reported a different size depending on which call had
placed it. Creation now requests the system frame too, and the settler
compares system frame against system frame. Because request and readback are
finally the same quantity, the computed chrome correction is gone -- the
settler only ever re-asks for the frame it first asked for, which removes the
overshoot the old computed correction was capped to contain.

Size restrictions had to move with it: UISceneSizeRestrictions is in content
points, so pinning a non-resizable window to the requested outer size would
demand a content area the size of the whole window and the frame could never
be granted. Restrictions are relaxed for the request and a fixed-size window
is pinned once its geometry settles, to the content it actually got.

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

@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: 10f627222f

ℹ️ 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 CodenameOne/src/com/codename1/ui/Window.java
Comment thread CodenameOne/src/com/codename1/ui/Display.java Outdated
shai-almog and others added 2 commits August 23, 2026 16:53
In pureTouch mode a component shows its selection only while a contact is
down on it, which shouldRenderSelection(Component) answers from a single
flag and the global pointerX/pointerY. With a contact down in two windows
whichever window's packet ran last owned both.

Two distinct failures came out of that. A press/release cycle in one window
cleared the flag while another window was still held, so the held window's
component silently dropped its selection. And a component was tested with
c.contains(pointerX, pointerY) against whatever window moved the pointer
last -- window coordinates are window relative, so that is not merely the
wrong point, it is a point in a different coordinate space.

The flag and the coordinates it is tested with are now per window, tracked
in the same slot table as the other per-window pointer state. Window zero
keeps the singleton and the global coordinates, so the single-window path is
unchanged. The component-less shouldRenderSelection() answers "any window
has a press down", which is what it meant when there was only one.

hideNotify() clears every window rather than one, because losing input is
not a per-window event.

WindowSelectionStateTest covers both halves and each fails without the fix
on its own assertion. UITestBase clears the new table between tests: a
window left held would otherwise carry into the next test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
WindowsWindowManager never overrode WindowManager.capture(), so it returned
the base null and Window.capture() took its documented fallback: re-rendering
the component hierarchy into a fresh image. That fallback draws what the
window *should* be showing, so it cannot tell a correct window from one whose
raster and hierarchy disagree, and it contains no native peer or editor at
all -- which is most of what the windowed screenshot suite exists to check.
The Windows windowed goldens were therefore passing without ever exercising
the Direct2D secondary-window surface.

The main-surface capture could not simply be reused. It encodes the render
target's WIC bitmap, and a secondary window draws into an
ID2D1HwndRenderTarget, which Direct2D offers no readback for -- there is no
bitmap to hand the encoder, which is what its own "window target is not
WIC-backed" branch already said. So the window is asked to render itself into
a DC through PrintWindow instead. PW_RENDERFULLCONTENT is the load-bearing
flag: without it a Direct2D or DirectComposition surface comes back blank.
PW_CLIENTONLY keeps the frame out, so the result is the rectangle the
framework laid out and the goldens are sized to.

The flag is #defined defensively rather than assumed, because an older SDK
header omits it and losing it silently would produce empty goldens rather
than a compile error.

Verified with scripts/check-native-signatures.sh, which is the check that
matters for a new native: a wrong symbol name still compiles and links, and
the dead-code pass then drops the Java method, leaving the feature inert.
Windows reports 480 native methods all resolving, 0 fatal.

Expect the windows-x64 and windows-arm64 windowed goldens to move: they were
recorded from the re-render fallback and will now be real readbacks.

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

@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: 1c7a7cd216

ℹ️ 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/iOSPort/src/com/codename1/impl/ios/MacWindowManager.java
Each time a Mac Catalyst window's content size changed, getNativeGraphics
allocated a new mutable image and overwrote the previous one without
releasing it. IOSImplementation.NativeImage frees its native peer only from
finalize(), and the Java wrapper is a few dozen bytes in front of a
width*height*4 native allocation -- so the collector sees almost no pressure
while a live resize hands out a new multi-megabyte raster per size step. The
dispose path had the same shape: it nulled the field and left the raster to
a finalizer, and that is the largest single allocation a window owns.

Both now release explicitly. Nothing outside holds the raster across the
swap: capture() deliberately copies the pixels into an independent image
rather than handing the live one out, and the paint loop re-fetches this
graphics every frame rather than caching it -- so the released image has no
remaining reader.

No test: the iOS port has no test module, and this is not reachable from
core-unittests. Verified by compiling the port and by SpotBugs over the ios
module, which reports zero findings.

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

@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: 1c5ee65734

ℹ️ 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 CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
The window drag activation filter claims one of eight slots on press and
releases it on release. A window disposed -- or losing the native pointer --
while a press is still down never delivers that release, so the slot stayed
claimed. Window ids are never reused, so it was held by a dead id for the
life of the process.

After eight such windows there are no slots left. windowDragSlot() then
answers -1 and the drag path takes its deliberate passthrough, which exists
so that more simultaneous drags than slots are let through rather than
swallowed. From that point on every window loses the filter entirely: a
pixel of jitter after a press is delivered as a real drag, which activates
drag and drop and moves a draggable component on what was meant as a click.

Display's windowInputCancelled() and windowDisposed() already cleared the
framework's own per-window input records, and the comment in Window.dispose()
noted that they only forget "the framework's records" -- the implementation's
slot was exactly the state neither of them could reach. Both now call
releaseWindowInputState(), which is a no-op for the main surface.

WindowTest.disposingWindowsMidPressDoesNotExhaustTheDragSlots presses in nine
windows and disposes each without a release, then checks a fresh window still
filters jitter and still passes real movement. Without the fix it reports the
jitter as a drag.

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

@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: 9f01f76ff0

ℹ️ 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".

plistValueElementEnd tracked nesting depth with literal "<array>" and
"</array>" strings while the rest of this parser matches elements
structurally through plistElementIndex, which already accepts "<array >" and
"<dict custom=\"x\">" as the same elements. So valid XML formatting on a
container tag was invisible to the nesting scan.

Both directions were wrong. A nested opening written "<array >" was missed,
so the first inner "</array>" closed the outer element and the role was
truncated before its delegate -- a correctly configured manifest failing the
build. A closing tag written "</array >" was missed entirely, the scan found
no close, and the caller fell back to the rest of the fragment -- which is
the worse one: a later role's delegate then vouches for a window role that
names somebody else, exactly the hole the CarPlay case exists to close,
reopened by a space.

Going structural needs one thing the literal matching got right by accident:
"<array/>" is an element but not a nesting level, and plistElementIndex
matches it. Counting it would leave the depth permanently ahead and swallow
the real closing tag. plistNestedElementIndex skips self-closing tags on
purpose, and plistCloseElementIndex is the closing-tag counterpart of
plistElementIndex, allowing only whitespace between the name and the ">".

The whitespace test asserts the accepting direction is refused: without the
fix it reports true for a manifest whose window role names another delegate.
The self-closing and close-tag-prefix tests pass either way by construction
-- they guard the new structural matching rather than reproduce the old bug,
which is why they are written against the CarPlay shape.

910 plugin tests pass; SpotBugs zero.

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

@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: d5dc9c1430

ℹ️ 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 CodenameOne/src/com/codename1/ui/Window.java Outdated
Comment thread CodenameOne/src/com/codename1/ui/Window.java Outdated
shai-almog and others added 2 commits August 23, 2026 18:25
…roll over

Two gaps against the Form path, both in Window.pointerPressed.

The press handle was created after the window's pointer-pressed listeners
ran. A listener can enter a nested event loop -- showModal() does -- and the
matching physical release is then processed inside it. With the handle
created afterwards that nested release found no gesture to clear, and the
method went on to install a fresh press whose release had already happened,
leaving the component latched until some later gesture freed it. Form
creates its handle before firing listeners; so does the framework's own
press record, for exactly this reason. Moved.

A press landing on a still-gliding container stopped the motion, cleared
pressedCmp and returned. Stopping the glide is right, but it was only half
of what Form does: Form re-enters the drag path so the same physical gesture
takes the scroll over, while here every following drag packet had no target
and the user had to lift and press again. The press now cancels the glide,
primes drag and drop, and re-enters through this window's drag path -- not
Display.pointerDragged(), which is the main surface's and would deliver to
the current Form instead.

It hands pressedCmp to the component the scroll was taken over from. Form
reaches the same place differently, by re-resolving the component under the
pointer whenever it has no pressed one; giving this window's drag path that
same fallback was tried and rejected -- it changed routing for every gesture
and broke five unrelated tests.

While here, drag events now carry setPointerPressedDuringDrag as Form's do,
read and cleared in the scalar path and reported without clearing in the
multi-pointer one, matching Form on both counts.

Both tests fail without their own fix. Each disposes its window in a finally,
because an assertion that throws before dispose leaves a window showing and
times out the next test's setup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Window.capture() falls back to re-rendering the component hierarchy when the
window manager returns no pixels. That fallback produces a plausible image of
the right size, so a capture path that never reads the real surface looks
exactly like one that does -- which is how the missing Windows override went
unnoticed in the first place.

The Windows manager now says so, once per process, when the native capture
comes back empty. Without it the only way to tell a live readback from a
silent fallback is to find a pixel that differs, and the windowed goldens are
byte-identical either way: the harness stops editing before capturing, so no
native editor is in frame to give it away.

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

@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: 150a2f2890

ℹ️ 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 CodenameOne/src/com/codename1/ui/Window.java
Comment thread CodenameOne/src/com/codename1/ui/Window.java
setWindowLocation already carries a comment about this trap: the read has to
happen on the event dispatch thread with the write, because setWindowBounds
marshals itself and reading beforehand queues a move carrying the old size.
centerOnDesktop(), centerOn() and restore() had the same shape and were
missed.

Both centring methods read the window's bounds, compute a position and only
then call something that marshals. From a background thread that computes
against geometry a queued resize is about to replace, so a caller that
resized and then centred got a window centred for the size it no longer has.

restore() is an ordering problem rather than an arithmetic one.
showOwnerChain() may queue the owner's show(), while the native restore ran
immediately -- so the child could reach the platform ahead of its owner, and
a WindowManager call happened off the event dispatch thread, which is the
only context that SPI is defined in.

All three now marshal the whole method.

Checked the rest of the class by enumerating every public method that touches
the window manager or reads bounds rather than grepping for the pattern:
setTitle, setResizable, setDecorated, setAlwaysOnTop, setUtilityWindow,
setWindowIcon, setMinimumWindowSize, minimize, toggleMaximize and
requestWindowFocus are each a single call with no read to go stale, and every
port marshals internally, so they are left alone deliberately.

Both tests fail without their own fix: the centring one is out by exactly
half the size difference, and the restore one sees the port called before the
queue drains.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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