Skip to content

Conversation

@ShahzaibIbrahim
Copy link
Contributor

@ShahzaibIbrahim ShahzaibIbrahim commented Jul 31, 2025

Problem

Currently, we have a logic to always initialize the first handle in a handle field. Instead of using it, we could switch to lazy loading via using zoomLevelToHandle map. The handle will only created when win32_getHandle is called.


Proposed Solution

To fix this, the Cursor class should be refactored to manage zoom-level-specific handles more robustly and safely. The following changes are needed:

  1. Remove handle Field
    Eliminate the dedicated handle field from the class. All handle accesses should go through the zoomLevelToHandle map, which now acts as the single source of truth for all zoom levels.

  2. Update All Related Methods
    Refactor all methods that previously relied on the handle field to instead use zoomLevelToHandle. This includes:

    • hashCode()
    • equals(Object obj)
    • isDisposed()
    • destroy()

Benefits

  • Enables safe, lazy creation of per-zoom handles when needed.
  • Simplifies and centralizes handle management logic for better maintainability.

Example

import org.eclipse.swt.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.widgets.*;

public class CursorZoomTest {
    public static void main(String[] args) {
        Display display = new Display();
        Shell shell = new Shell(display);

        // 1. Constructor: Cursor(Device, int style)
        Cursor cursorFromStyle = new Cursor(display, SWT.CURSOR_HAND);

        // 2. Constructor: Cursor(Device, ImageData source, ImageData mask, int hotspotX, int hotspotY)
        ImageData source = new ImageData(16, 16, 1, new PaletteData(new RGB[]{new RGB(0, 0, 0)}));
        ImageData mask = new ImageData(16, 16, 1, new PaletteData(new RGB[]{new RGB(0, 0, 0)}));
        Cursor cursorFromImageAndMask = new Cursor(display, source, mask, 0, 0);

        // 3. Constructor: Cursor(Device, ImageData source, int hotspotX, int hotspotY)
        Cursor cursorFromImageOnly = new Cursor(display, source, 0, 0);

        // 4. Constructor: Cursor(Device, ImageDataProvider imageDataProvider, int hotspotX, int hotspotY)
        ImageDataProvider provider = zoom -> source;
        Cursor cursorFromProvider = new Cursor(display, provider, 0, 0);

        Cursor[] cursors = {
            cursorFromStyle,
            cursorFromImageAndMask,
            cursorFromImageOnly,
            cursorFromProvider
        };

        int[] zoomLevels = {100, 125, 150, 175, 200, 250};

        for (int i = 0; i < cursors.length; i++) {
            System.out.println("Cursor #" + (i + 1));
            for (int zoom : zoomLevels) {
                long handle = Cursor.win32_getHandle(cursors[i], zoom);
                System.out.println("  Zoom " + zoom + ": handle = " + handle);
            }
        }

        // Dispose resources
        for (Cursor cursor : cursors) {
            cursor.dispose();
        }

        shell.dispose();
        display.dispose();
    }
}

Output

Cursor #1
  Zoom 100: handle = 65567
  Zoom 125: handle = 65567
  Zoom 150: handle = 65567
  Zoom 175: handle = 65567
  Zoom 200: handle = 65567
  Zoom 250: handle = 65567
Cursor #2
  Zoom 100: handle = 75434631
  Zoom 125: handle = 53612289
  Zoom 150: handle = 43651639
  Zoom 175: handle = 61083667
  Zoom 200: handle = 30212277
  Zoom 250: handle = 141363963
Cursor #3
  Zoom 100: handle = 33559507
  Zoom 125: handle = 263197751
  Zoom 150: handle = 84413253
  Zoom 175: handle = 21237883
  Zoom 200: handle = 56361347
  Zoom 250: handle = 46928295
Cursor #4
  Zoom 100: handle = 20124285
  Zoom 125: handle = 63705063
  Zoom 150: handle = 46729749
  Zoom 175: handle = 68358127
  Zoom 200: handle = 79891305
  Zoom 250: handle = 14290407

Note that for the Cursor 1, we always get the same handle from OS but it's scaled correctly for all zoom levels.

Fixes: #2355

@ShahzaibIbrahim ShahzaibIbrahim force-pushed the master-376 branch 2 times, most recently from aed2cb3 to 8b591c8 Compare July 31, 2025 11:08
@github-actions
Copy link
Contributor

github-actions bot commented Jul 31, 2025

Test Results

   546 files  ±0     546 suites  ±0   31m 3s ⏱️ - 1m 44s
 4 431 tests +1   4 414 ✅ +1   17 💤 ±0  0 ❌ ±0 
16 764 runs  +4  16 637 ✅ +4  127 💤 ±0  0 ❌ ±0 

Results for commit a3be4bd. ± Comparison against base commit ad53b13.

♻️ This comment has been updated with latest results.

@arunjose696
Copy link
Contributor

CI fails as Added tests uses windows only API, they should be probably moved to org.eclipse.swt.tests.win32

Copy link
Contributor

@HeikoKlare HeikoKlare left a comment

Choose a reason for hiding this comment

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

Can we please split to up into multiple commits or even PRs to make it easier to review and (hopefully not necessary) to find regressions? I see three changes combined here that we could split up:

  • Fixing the hotspot coordinate calculation
  • Refactoring the constructors (moving initialization logic to other methods)
  • Managing multiple handles for different zooms

@ShahzaibIbrahim ShahzaibIbrahim marked this pull request as draft August 1, 2025 09:30
@ShahzaibIbrahim ShahzaibIbrahim marked this pull request as ready for review August 1, 2025 10:59
@ShahzaibIbrahim
Copy link
Contributor Author

Can we please split to up into multiple commits or even PRs to make it easier to review and (hopefully not necessary) to find regressions? I see three changes combined here that we could split up:

  • Fixing the hotspot coordinate calculation
  • Refactoring the constructors (moving initialization logic to other methods)
  • Managing multiple handles for different zooms

As you proposed, I have 3 PRs for the following

@ShahzaibIbrahim ShahzaibIbrahim force-pushed the master-376 branch 2 times, most recently from 6eba22f to 4308424 Compare August 4, 2025 09:10
Copy link
Contributor

@HeikoKlare HeikoKlare left a comment

Choose a reason for hiding this comment

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

Thank you for splitting up this PR! Now with the reduced complexity, it sstill seems to be a combination of multiple changes:

  • Proper initialization of the handles for other zooms by not creating a new Cursor instance
  • Bug that caused the ImageDataProvider not to be used when requesting a handle for another zoom (as cursor.source is not null then, thus this will be used)
  • Proper handling of multiple zoom handles instead of having one default handle (probably including full on-demand handle creation, see below)

So it would again be good to split this up accordingly.
What I do not understand with the current change is why we need to create handles for default zoom upon cursor instantiation. All other resources create their handles only on demand and since consumers need to use win32_getHandle anyway, we could create the handle when first calling that method. That would even remove the duplication of calling initialization logic inside constructors and in win32_getHandle.

@ShahzaibIbrahim
Copy link
Contributor Author

  • Bug that caused the ImageDataProvider not to be used when requesting a handle for another zoom (as cursor.source is not null then, thus this will be used)

I am not sure what bug is it you're talking about. cursor.source is always going to be null when created with style attribute. From what I understand you mean the source will not be null when requesting handle for other zoom like this snippet?

ImageData source = new ImageData(16, 16, 1, new PaletteData(new RGB[]{new RGB(0, 0, 0)}));
		ImageDataProvider provider = zoom -> source;
Cursor cursorFromProvider = new Cursor(Display.getDefault(), provider, 0, 0);
Cursor.win32_getHandle(cursorFromProvider, 200);
Cursor.win32_getHandle(cursorFromProvider, 100);

@HeikoKlare

@ShahzaibIbrahim ShahzaibIbrahim force-pushed the master-376 branch 3 times, most recently from b66799b to 345aa4b Compare August 5, 2025 13:16
@ShahzaibIbrahim ShahzaibIbrahim changed the title Refactor Cursor handle management to eliminate leaks and support per-zoom handles Refactor Cursor to lazy load Cursor handle Aug 5, 2025
@ShahzaibIbrahim ShahzaibIbrahim marked this pull request as draft August 6, 2025 12:39
@ShahzaibIbrahim ShahzaibIbrahim force-pushed the master-376 branch 2 times, most recently from 09bfb02 to f5050ee Compare August 12, 2025 10:45
@ShahzaibIbrahim ShahzaibIbrahim marked this pull request as ready for review August 12, 2025 10:46
Copy link
Contributor

@HeikoKlare HeikoKlare left a comment

Choose a reason for hiding this comment

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

Please note that the simple removal of eager handle creation from the constructors will violates the existing contracts. There are exceptions specified, which (except for the ERROR_NO_HANDLES ones) must still be thrown in the constructor and not on a lazy handle request.

@ShahzaibIbrahim
Copy link
Contributor Author

Please note that the simple removal of eager handle creation from the constructors will violates the existing contracts. There are exceptions specified, which (except for the ERROR_NO_HANDLES ones) must still be thrown in the constructor and not on a lazy handle request.

Here's all the contracts tested on Cursor object creation

ImageData source = new ImageData(16, 16, 1, new PaletteData(new RGB[]{new RGB(0, 0, 0)}));
ImageData mask = new ImageData(16, 16, 1, new PaletteData(new RGB[]{new RGB(0, 0, 0)}));

// Should throw ERROR_INVALID_ARGUMENT
Cursor cursor = new Cursor(Display.getDefault(), -99);

// ERROR_NULL_ARGUMENT - Source is null
 Cursor cursorFromImageAndMask = new Cursor(Display.getDefault(), null, mask, 0, 0);

// ERROR_NULL_ARGUMENT - Mask is null and Source doesn't heve a mask
 Cursor cursorFromImageAndMask1 = new Cursor(Display.getDefault(), source, null, 0, 0);

// ERROR_INVALID_ARGUMENT - source and the mask are not the same size
ImageData source32 = new ImageData(32, 32, 1, new PaletteData(new RGB[]{new RGB(0, 0, 0)}));
ImageData mask16 = new ImageData(16, 16, 1, new PaletteData(new RGB[]{new RGB(0, 0, 0)}));
Cursor cursorFromImageAndMask2 = new Cursor(Display.getDefault(), source32, mask16, 0, 0);

// ERROR_INVALID_ARGUMENT - hotspot is outside the bounds of the image
Cursor cursorFromImageAndMask3 = new Cursor(Display.getDefault(), source, mask, 18, 18);

// ERROR_NULL_ARGUMENT - Source is null
ImageData nullImageData = null;
Cursor cursorFromSourceOnly= new Cursor(Display.getDefault(), nullImageData, 0, 0);

// ERROR_INVALID_ARGUMENT - hotspot is outside the bounds of the image
Cursor cursorFromSourceOnly1 = new Cursor(Display.getDefault(), source, 17, 17);

// ERROR_NULL_ARGUMENT - Source is null
ImageData nullSource = null;
ImageDataProvider provider = zoom -> nullSource;
Cursor cursorFromProvider = new Cursor(Display.getDefault(), provider, 0, 0);

Copy link
Member

@akurtakov akurtakov left a comment

Choose a reason for hiding this comment

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

I can't speak for the win32 specific change but the tests change look good.

@akurtakov akurtakov dismissed their stale review August 12, 2025 13:53

Unit tests are in good shape now.

Copy link
Contributor

@HeikoKlare HeikoKlare left a comment

Choose a reason for hiding this comment

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

In general, the change looks good. But there is at least one potential behavorial change beyond the lazy handle creation in there that we need to clarify or resolve.

@ShahzaibIbrahim ShahzaibIbrahim force-pushed the master-376 branch 2 times, most recently from 18bcf47 to e62283d Compare September 2, 2025 12:56
@eclipse-platform-bot
Copy link
Contributor

eclipse-platform-bot commented Sep 2, 2025

This pull request changes some projects for the first time in this development cycle.
Therefore the following files need a version increment:

tests/org.eclipse.swt.tests/META-INF/MANIFEST.MF
tests/org.eclipse.swt.tests/pom.xml

Warning

🚧 This PR cannot be modified by maintainers because edits are disabled or it is created from an organization repository. To obtain the required changes apply the git patch manually as an additional commit.

Git patch
From ee29f1e93c10591781b92dc7ae04684a86078120 Mon Sep 17 00:00:00 2001
From: Eclipse Platform Bot <[email protected]>
Date: Fri, 5 Sep 2025 09:09:27 +0000
Subject: [PATCH] Version bump(s) for 4.38 stream


diff --git a/tests/org.eclipse.swt.tests/META-INF/MANIFEST.MF b/tests/org.eclipse.swt.tests/META-INF/MANIFEST.MF
index cf91c273eb..92d095fc15 100644
--- a/tests/org.eclipse.swt.tests/META-INF/MANIFEST.MF
+++ b/tests/org.eclipse.swt.tests/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
 Bundle-ManifestVersion: 2
 Bundle-Name: Eclipse SWT Tests
 Bundle-SymbolicName: org.eclipse.swt.tests
-Bundle-Version: 3.107.900.qualifier
+Bundle-Version: 3.107.1000.qualifier
 Bundle-Vendor: Eclipse.org
 Export-Package: org.eclipse.swt.tests.junit,
  org.eclipse.swt.tests.junit.performance
diff --git a/tests/org.eclipse.swt.tests/pom.xml b/tests/org.eclipse.swt.tests/pom.xml
index 59df1ad54e..f8a6eb1865 100644
--- a/tests/org.eclipse.swt.tests/pom.xml
+++ b/tests/org.eclipse.swt.tests/pom.xml
@@ -20,7 +20,7 @@
     <relativePath>../../local-build/local-build-parent/</relativePath>
   </parent>
   <artifactId>org.eclipse.swt.tests</artifactId>
-  <version>3.107.900-SNAPSHOT</version>
+  <version>3.107.1000-SNAPSHOT</version>
   <packaging>eclipse-test-plugin</packaging>
   <properties>
     <tycho.testArgLine></tycho.testArgLine>
-- 
2.51.0

Further information are available in Common Build Issues - Missing version increments.

Copy link
Contributor

@HeikoKlare HeikoKlare left a comment

Choose a reason for hiding this comment

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

The latest change introduced a regression that will make the cursor setup fail when passing a null mask.

@ShahzaibIbrahim ShahzaibIbrahim force-pushed the master-376 branch 4 times, most recently from 0f1e6d1 to 690dcd8 Compare September 4, 2025 10:28
Copy link
Contributor

@HeikoKlare HeikoKlare left a comment

Choose a reason for hiding this comment

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

Change is almost fine now. Please also adapt the commit message as it currently states that it adds per-zoom handle support, but that was part of a previous change and this is about lazy instantiation. Please also extend the description with some explanation on such an essential change.

@HeikoKlare HeikoKlare dismissed their stale review September 5, 2025 06:59

Major concerns have been addressed

@ShahzaibIbrahim ShahzaibIbrahim force-pushed the master-376 branch 2 times, most recently from 79c5417 to af030ab Compare September 5, 2025 09:03
This change updates the Win32 implementation of the Cursor class to
defer creation of the native cursor handle until first use, enabling
lazy loading instead of initializing the handle at construction time.

Key changes:
- Move initialization of the first handle creation from the constructors
to the first handle retrieval.
- Update equals(), hashCode(), isDisposed(), and toString() to work with
zoom-aware handles and a new isDestroyed flag.
- Add comprehensive unit tests for invalid constructor arguments to
improve coverage and guard against improper usage.

This change reduces unnecessary resource creation.
@HeikoKlare HeikoKlare changed the title Refactor Cursor to lazy load Cursor handle [Win32] Lazy load Cursor handles Sep 6, 2025
Copy link
Contributor

@HeikoKlare HeikoKlare left a comment

Choose a reason for hiding this comment

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

Looks good now. With the latest push I have just adapted the faulty commit message, as it still referred to adding multi-handle support which is not part of this change but was implemented already before.

@HeikoKlare HeikoKlare merged commit de3812d into eclipse-platform:master Sep 6, 2025
17 checks passed
@HeikoKlare HeikoKlare deleted the master-376 branch September 6, 2025 09:41
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.

Refactor Cursor to lazy load Cursor handle [Win32] Retrieving handle for cursor leads to non-disposed resource error

5 participants