Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*******************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.swt.graphics;

import static org.junit.jupiter.api.Assertions.*;

import java.util.*;

import org.eclipse.swt.*;
import org.eclipse.swt.internal.*;
import org.eclipse.swt.widgets.*;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.*;

@ExtendWith(PlatformSpecificExecutionExtension.class)
@ExtendWith(WithMonitorSpecificScalingExtension.class)
class CursorWin32Tests {

private Display display;

@BeforeEach
void setUp() {
display = Display.getDefault();
}

@Test
void testDisposedCursorReturnsZeroHandle() {
Cursor cursor = new Cursor(display, SWT.CURSOR_ARROW);
cursor.dispose();
assertEquals(0L, Cursor.win32_getHandle(cursor, 100),
"A disposed cursor must return a zero handle");
}

@Test
void testHandleIsCachedForSameZoomLevel() {
ImageData source = new ImageData(16, 16, 32,
new PaletteData(0xFF00, 0xFF0000, 0xFF000000));
source.alpha = 255;

Cursor cursor = new Cursor(display, source, 0, 0);
try {
long first = Cursor.win32_getHandle(cursor, 100);
long second = Cursor.win32_getHandle(cursor, 100);
assertEquals(first, second,
"Repeated calls with the same zoom must return the cached handle");
} finally {
cursor.dispose();
}
}

@Test
void testImageDataCursorProducesDifferentHandlesForDifferentZoomLevels() {
// 32bpp image with uniform alpha — takes the ARGB path in setupCursorFromImageData
ImageData source = new ImageData(16, 16, 32,
new PaletteData(0xFF00, 0xFF0000, 0xFF000000));
source.alpha = 255;

Cursor cursor = new Cursor(display, source, 0, 0);
try {
long handle100 = Cursor.win32_getHandle(cursor, 100);
long handle200 = Cursor.win32_getHandle(cursor, 200);

assertNotEquals(0L, handle100, "Handle at 100% zoom must be non-zero");
assertNotEquals(0L, handle200, "Handle at 200% zoom must be non-zero");
assertNotEquals(handle100, handle200,
"Different zoom levels must produce distinct OS cursor handles (different physical sizes)");
} finally {
cursor.dispose();
}
}

@Test
void testDestroyHandlesExceptPreservesRetainedHandle() {
// 32bpp ARGB source so we get a unique, non-shared OS handle per zoom level
ImageData source = new ImageData(16, 16, 32,
new PaletteData(0xFF00, 0xFF0000, 0xFF000000));
source.alpha = 255;

Cursor cursor = new Cursor(display, source, 0, 0);
try {
long handle100 = Cursor.win32_getHandle(cursor, 100);
Cursor.win32_getHandle(cursor, 200); // populate a second zoom level

cursor.destroyHandlesExcept(Set.of(DPIUtil.getZoomForAutoscaleProperty(100)));

// The cursor itself must still be alive and the retained handle unchanged
assertFalse(cursor.isDisposed(), "Cursor must not be disposed after destroyHandlesExcept");
assertEquals(handle100, Cursor.win32_getHandle(cursor, 100),
"The handle for the retained zoom level must be unchanged after destroyHandlesExcept");
} finally {
cursor.dispose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -316,25 +316,14 @@ public Cursor(Device device, ImageDataProvider imageDataProvider, int hotspotX,
*
* @noreference This method is not intended to be referenced by clients.
*/
public static Long win32_getHandle (Cursor cursor, int zoom) {
public static long win32_getHandle (Cursor cursor, int zoom) {
if (cursor.isDisposed()) {
return 0L;
}
int zoomWithPointerSizeScaleFactor = (int) (zoom * getPointerSizeScaleFactor());
if (cursor.zoomLevelToHandle.get(zoomWithPointerSizeScaleFactor) != null) {
return cursor.zoomLevelToHandle.get(zoomWithPointerSizeScaleFactor).getHandle();
}

CursorHandle handle = cursor.cursorHandleProvider.createHandle(cursor.device, zoomWithPointerSizeScaleFactor);
cursor.setHandleForZoomLevel(handle, zoomWithPointerSizeScaleFactor);

return cursor.zoomLevelToHandle.get(zoomWithPointerSizeScaleFactor).getHandle();
}

private void setHandleForZoomLevel(CursorHandle handle, Integer zoom) {
if (zoom != null && !zoomLevelToHandle.containsKey(zoom)) {
zoomLevelToHandle.put(zoom, handle);
}
int scaledZoom = (int) (zoom * getPointerSizeScaleFactor());
return cursor.zoomLevelToHandle
.computeIfAbsent(scaledZoom, z -> cursor.cursorHandleProvider.createHandle(cursor.device, z))
.getHandle();
}

/**
Expand Down Expand Up @@ -389,8 +378,7 @@ void destroy () {
@Override
public boolean equals (Object object) {
if (object == this) return true;
if (!(object instanceof Cursor)) return false;
Cursor cursor = (Cursor) object;
if (!(object instanceof Cursor cursor)) return false;
return device == cursor.device && win32_getHandle(this, DEFAULT_ZOOM) == win32_getHandle(cursor, DEFAULT_ZOOM);
}

Expand All @@ -406,7 +394,7 @@ public boolean equals (Object object) {
*/
@Override
public int hashCode () {
return win32_getHandle(this, DEFAULT_ZOOM).intValue();
return (int) win32_getHandle(this, DEFAULT_ZOOM);
}

/**
Expand Down Expand Up @@ -439,7 +427,7 @@ public String toString () {
@Override
void destroyHandlesExcept(Set<Integer> zoomLevels) {
zoomLevelToHandle.entrySet().removeIf(entry -> {
final Integer zoom = entry.getKey();
Integer zoom = entry.getKey();
if (!zoomLevels.contains(zoom) && zoom != DPIUtil.getZoomForAutoscaleProperty(DEFAULT_ZOOM)) {
entry.getValue().destroy();
return true;
Expand Down Expand Up @@ -492,7 +480,7 @@ void destroy() {
}
}

private static interface CursorHandleProvider {
private interface CursorHandleProvider {
CursorHandle createHandle(Device device, int zoom);
}

Expand All @@ -513,7 +501,7 @@ public CursorHandle createHandle(Device device, int zoom) {
return new CustomCursorHandle(handle);
}

private static final long getOSCursorIdFromStyle(int style) {
private static long getOSCursorIdFromStyle(int style) {
long lpCursorName = 0;
switch (style) {
case SWT.CURSOR_HAND:
Expand Down Expand Up @@ -663,15 +651,15 @@ public ImageDataWithMaskCursorHandleProvider(ImageData source, ImageData mask, i
}

private void validateMask(ImageData source, ImageData mask) {
ImageData testMask = mask == null ? null : (ImageData) mask.clone();
if (testMask == null) {
ImageData effectiveMask = mask;
if (effectiveMask == null) {
if (source.getTransparencyType() != SWT.TRANSPARENCY_MASK) {
SWT.error(SWT.ERROR_NULL_ARGUMENT);
}
testMask = source.getTransparencyMask();
effectiveMask = source.getTransparencyMask();
}
/* Check the bounds. Mask must be the same size as source */
if (testMask.width != source.width || testMask.height != source.height) {
if (effectiveMask.width != source.width || effectiveMask.height != source.height) {
SWT.error(SWT.ERROR_INVALID_ARGUMENT);
}
}
Expand Down
Loading