Skip to content

Conversation

ShahzaibIbrahim
Copy link
Contributor

@ShahzaibIbrahim ShahzaibIbrahim commented Sep 16, 2025

Use OS.CreateIconIndirect for colored cursors with source and mask

Previously, the Cursor constructor that accepted both source and mask used OS.CreateCursor, which only supports monochrome cursors. As a result, any color information in the source was ignored and the cursor always appeared black.

This change updates the constructor to use OS.CreateIconIndirect, allowing full-color cursors while still respecting the mask for transparency. DPI scaling of the source and mask now works correctly with colored cursors.

How to Test

Run the following snippet:

Snippet386

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

public class Snippet386 {

	private static final int IMAGE_SIZE_IN_POINTS = 16;

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

		Combo combo = createConstructorCombo(shell);
		Label zoomLabel = createZoomLabel(shell);

		addZoomChangedListener(shell, zoomLabel);
		Group section = new Group(shell, SWT.NONE);
		section.setText("Scale");
		section.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
		section.setLayout(new FillLayout());
		addPaintTicks(section);

		CursorManager cursorManager = new CursorManager(display, shell, combo);
		combo.addListener(SWT.Selection, e -> cursorManager.updateCursor());
		cursorManager.updateCursor();

		shell.setSize(400, 400);
		shell.open();

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

	private static Shell createShell(Display display) {
		Shell shell = new Shell(display);
		shell.setText("Snippet 386");
		shell.setLayout(new GridLayout(1, false));
		return shell;
	}

	private static Combo createConstructorCombo(Composite parent) {
		Label label = new Label(parent, SWT.NONE);
		label.setText("Choose Cursor Constructor:");

		Combo combo = new Combo(parent, SWT.READ_ONLY);
		combo.setItems("Cursor(Device, int)", "Cursor(Device, ImageData, ImageData, int, int)",
				"Cursor(Device, ImageData, int, int)", "Cursor(Device, ImageDataProvider, int, int)");
		combo.select(0);
		return combo;
	}

	private static Label createZoomLabel(Shell parent) {
		Label zoomLabel = new Label(parent, SWT.NONE);
		zoomLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
		setZoomLabelText(parent, zoomLabel);
		return zoomLabel;
	}

	private static void setZoomLabelText(Shell shell, Label label) {
		int zoom = shell.getMonitor().getZoom();
		int expectedCursorSize = Math.round(IMAGE_SIZE_IN_POINTS * (zoom / 100f));
		label.setText("Current zoom: " + zoom + "% \nExpected Cursor Size = " + expectedCursorSize);
	}

	private static void addZoomChangedListener(Shell shell, Label zoomLabel) {
		shell.addListener(SWT.Resize, event -> {
			setZoomLabelText(shell, zoomLabel);
			shell.layout();
		});
	}

	private static void addPaintTicks(Composite composite) {
		composite.addPaintListener(event -> {
			drawTicks(composite, event.gc);
		});
	}

	private static void drawTicks(Composite shell, GC gc) {
		int deviceZoom = shell.getMonitor().getZoom();
		float devScale = deviceZoom / 100f;
		Point client = shell.getSize();
		int xPos = (int) ((client.x / 2) - (6 * (IMAGE_SIZE_IN_POINTS / devScale)));
		int tickHeight = 10;
		int yPos = 20;

		for (int tickIndex = 0; tickIndex < 6; tickIndex++) {
			xPos += (IMAGE_SIZE_IN_POINTS / devScale);
			int yOffset = (tickIndex % 3 == 1) ? 20 : (tickIndex % 3 == 2) ? 40 : 0;

			gc.drawLine(xPos, yPos, xPos, yPos + tickHeight);
			gc.drawText(Integer.toString(tickIndex * IMAGE_SIZE_IN_POINTS), xPos - 5, yPos + 12 + yOffset);
		}
	}

	private static void eventLoop(Display display, Shell shell) {
		while (!shell.isDisposed()) {
			if (!display.readAndDispatch())
				display.sleep();
		}
	}

	public static ImageData createSolidColorImageData(int size, RGB color) {
		PaletteData palette = new PaletteData(0xFF0000, 0x00FF00, 0x0000FF);
		ImageData imageData = new ImageData(size, size, 24, palette);

		int pixel = palette.getPixel(color);
		for (int y = 0; y < size; y++) {
			for (int x = 0; x < size; x++) {
				imageData.setPixel(x, y, pixel);
			}
		}
		return imageData;
	}

	private static class CursorManager {
		private final Display display;
		private final Shell shell;
		private final Combo combo;

		CursorManager(Display display, Shell shell, Combo combo) {
			this.display = display;
			this.shell = shell;
			this.combo = combo;
		}

		void updateCursor() {
			int selection = combo.getSelectionIndex();
			Cursor oldCursor = shell.getCursor();
			if (oldCursor != null && !oldCursor.isDisposed()) {
				oldCursor.dispose();
			}
			Cursor cursor = createCursor(selection);
			if (cursor != null) {
				shell.setCursor(cursor);
			}
		}

		private Cursor createCursor(int selection) {
			switch (selection) {
			case 0:
				return new Cursor(display, SWT.CURSOR_HAND);
			case 1: {
				PaletteData rgbPalette = new PaletteData(0xFF0000, 0x00FF00, 0x0000FF);
				int bluePixel = rgbPalette.getPixel(new RGB(0, 0, 255));
				ImageData source = new ImageData(IMAGE_SIZE_IN_POINTS, IMAGE_SIZE_IN_POINTS, 24, new PaletteData(0xFF0000, 0x00FF00, 0x0000FF));

				for (int x = 0; x < IMAGE_SIZE_IN_POINTS; x++) {
					for (int y = 0; y < IMAGE_SIZE_IN_POINTS; y++) {
						source.setPixel(x, y, bluePixel);
					}
				}

				ImageData mask = new ImageData(IMAGE_SIZE_IN_POINTS, IMAGE_SIZE_IN_POINTS, 1, new PaletteData(new RGB[] {new RGB(0,0,0), new RGB(255,255,255)}));
				for (int x = 0; x < IMAGE_SIZE_IN_POINTS; x++) {
					for (int y = 0; y < IMAGE_SIZE_IN_POINTS; y++) {
						mask.setPixel(x, y, x % 2);
					}
				}

				return new Cursor(display, source, mask, IMAGE_SIZE_IN_POINTS / 2, IMAGE_SIZE_IN_POINTS / 2);
			}
			case 2: {
				RGB red = new RGB(255, 0, 0);
				return new Cursor(display, createSolidColorImageData(IMAGE_SIZE_IN_POINTS, red), 0, 0);
			}
			case 3: {
				RGB green = new RGB(0, 255, 0);
				ImageDataProvider provider = zoom -> {
					return createSolidColorImageData(IMAGE_SIZE_IN_POINTS * zoom / 100, green);
				};
				return new Cursor(display, provider, 0, 0);
			}
			default:
				return null;
			}
		}
	}
}

  • Select the Cursor(Device, ImageData, ImageData, int, int) value from drop down
  • See if the cursor is blue and transparency is also applied.

Result

Before:
20250916-1528-45 3125627

After:
20250916-1527-37 5966689

Requires

Copy link
Contributor

github-actions bot commented Sep 16, 2025

Test Results

  118 files  ±0    118 suites  ±0   9m 54s ⏱️ -28s
4 439 tests ±0  4 416 ✅  - 6  17 💤 ±0  6 ❌ +6 
  298 runs  ±0    288 ✅  - 6   4 💤 ±0  6 ❌ +6 

For more details on these failures, see this check.

Results for commit 433b5ba. ± Comparison against base commit 43f54cc.

♻️ This comment has been updated with latest results.

@ShahzaibIbrahim
Copy link
Contributor Author

Failing test are unrelated, see #2516

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.

I have some concerns about this change:

  • It introduces unnecessary complexity to setupCursorFromIamgeData as that internal utility method is made capable of handling cases that can not occur (or at least that were also not supported) by the existing API.
  • It change the return type of getPointerSizeScaleFactor() without any obvious need, increasing the complexity of consumer code.
  • The code is not properly formatted.

Copy link
Contributor

@fedejeanne fedejeanne left a comment

Choose a reason for hiding this comment

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

My comment was addressed 👍
See Heiko's comments

if (this.mask != null) {
int scaledMaskWidth = Math.round(this.mask.width * zoomFactor);
int scaledMaskHeight = Math.round(this.mask.height * zoomFactor);
scaledMask = this.mask.scaledTo(scaledMaskWidth, scaledMaskHeight);
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a behavior change. Previously DPIUtil.scaleImageData() was used for the mask and now ImageData::scaledTo is used producing a different kind of result. This either needs to be explained or reverted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We talked about that and in your words, "probably the issue is that when using "proper" scaling for the source it creates an image with alpha data, such that the mask is not properly taken into account. Using scaledTo will of course lead to worse scaling results, but maybe it's the best solution for the source+mask use case then"

Previously, the Cursor constructor that accepted both source and mask
used
OS.CreateCursor, which only supports monochrome cursors. As a result,
any color information in the source was ignored and the cursor always
appeared black.

This change updates the constructor to use OS.CreateIconIndirect,
allowing
full-color cursors while still respecting the mask for transparency.
DPI scaling of the source and mask now works correctly with colored
cursors.
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.

Unify Cursor Drawing for ImageDataWithMaskCursorHandleProvider and ImageDataCursorHandleProvider
3 participants