Skip to content

Commit 5584d36

Browse files
committed
Separate cursor icon from its decoration for the shared cursors.
The way cursors are rendered on Windows is that regardless of the zoom level, the stroke width of the cursor is always 1px wide. When using an SVG, this would normally be achieved by setting the "non-scaling-stroke" vector effect. Sadly, this effect is not supported by the jSVG rasterizer that is used by SWT and as a result, the stroke width is scaled proportionally to the zoom level. I.e. 1px at 100%, 2px at 200% and so on. To work around this limitation, the cursor is separated from the remaining components and constructed on-the-fly, The complete cursor is then a composite of the SVG and this generated cursor.
1 parent e2ba6cd commit 5584d36

File tree

6 files changed

+154
-33
lines changed

6 files changed

+154
-33
lines changed

org.eclipse.gef/src/org/eclipse/gef/SharedCursors.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
import org.eclipse.swt.graphics.Cursor;
1616

1717
import org.eclipse.jface.resource.ImageDescriptor;
18+
import org.eclipse.jface.viewers.DecorationOverlayIcon;
19+
import org.eclipse.jface.viewers.IDecoration;
1820

1921
import org.eclipse.draw2d.Cursors;
2022

23+
import org.eclipse.gef.internal.InternalCursor;
2124
import org.eclipse.gef.internal.InternalGEFPlugin;
2225
import org.eclipse.gef.internal.InternalImages;
2326

@@ -53,6 +56,12 @@ public class SharedCursors extends Cursors {
5356
}
5457

5558
private static Cursor createCursor(String sourceName) {
59+
if (InternalGEFPlugin.isSvgSupported()) {
60+
ImageDescriptor src1 = InternalImages.createDescriptor(sourceName);
61+
ImageDescriptor src2 = InternalCursor.getCursorDescriptor();
62+
ImageDescriptor src = new DecorationOverlayIcon(src1, src2, IDecoration.TOP_LEFT);
63+
return new Cursor(null, src.getImageData(100), 0, 0);
64+
}
5665
ImageDescriptor src = InternalImages.createDescriptor(sourceName);
5766
return InternalGEFPlugin.createCursor(src, 0, 0);
5867
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Patrick Ziegler and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* SPDX-License-Identifier: EPL-2.0
9+
*
10+
* Contributors:
11+
* Patrick Ziegler - initial API and implementation
12+
*******************************************************************************/
13+
14+
package org.eclipse.gef.internal;
15+
16+
import java.util.HashMap;
17+
import java.util.Map;
18+
import java.util.Objects;
19+
20+
import org.eclipse.swt.SWT;
21+
import org.eclipse.swt.graphics.GC;
22+
import org.eclipse.swt.graphics.Image;
23+
import org.eclipse.swt.graphics.ImageData;
24+
import org.eclipse.swt.graphics.PaletteData;
25+
import org.eclipse.swt.graphics.Path;
26+
import org.eclipse.swt.widgets.Display;
27+
28+
import org.eclipse.jface.resource.ImageDescriptor;
29+
30+
import org.eclipse.draw2d.ColorConstants;
31+
32+
/**
33+
* This class defines the shape of the default GEF-cursor used for the plug/tree
34+
* images. The cursor can't be part of the SVG itself, as it is a)
35+
* platform-specific and b) because SWT doesn't respect the
36+
* {@code non-scaling-stroke} vector effect, which ensures that the cursor
37+
* always has a stroke-width of 1px.
38+
*/
39+
public class InternalCursor {
40+
/**
41+
* Defines the shape of the cursor at 100% zoom.
42+
*/
43+
//@formatter:off
44+
private static final float[] CURSOR_POINTS = {
45+
0f, 0f,
46+
0f, 17f,
47+
4f, 13f,
48+
7f, 19f,
49+
9f, 18f,
50+
7f, 13f,
51+
7f, 12f,
52+
12f, 12f,
53+
0f, 0f
54+
};
55+
//@formatter:on
56+
/**
57+
* Local cache to store the cursor data for each zoom level.
58+
*/
59+
private static final Map<Integer, ImageData> CURSOR_AT_ZOOM = new HashMap<>();
60+
/**
61+
* The default cursor that is constructed using {link {@link #CURSOR_POINTS}.
62+
* May be replaced with a custom cursor by calling
63+
* {@link #setCursorDescriptor(ImageDescriptor)}.
64+
*/
65+
private static ImageDescriptor CURRENT_CURSOR_DESCRIPTOR = ImageDescriptor
66+
.createFromImageDataProvider(zoom -> CURSOR_AT_ZOOM.computeIfAbsent(zoom, InternalCursor::getCursorAtZoom));
67+
68+
/**
69+
* This method generates the image data for the cursor at the given zoom level.
70+
* The points defined with {@link #CURSOR_POINTS} are scaled by the given zoom
71+
* and painted onto an image.
72+
*
73+
* @param zoom The zoom level. e.g. 100, 125, 200
74+
* @return The cursor image data at the given zoom level.
75+
*/
76+
private static ImageData getCursorAtZoom(int zoom) {
77+
float maxWidth = 0f;
78+
float maxHeight = 0f;
79+
80+
for (int i = 0; i < CURSOR_POINTS.length; i += 2) {
81+
maxWidth = Math.max(maxWidth, CURSOR_POINTS[i]);
82+
maxHeight = Math.max(maxHeight, CURSOR_POINTS[i + 1]);
83+
}
84+
85+
float zoomFactor = zoom / 100.0f;
86+
87+
int width = 1 + (int) Math.ceil(zoomFactor * maxWidth);
88+
int height = 1 + (int) Math.ceil(zoomFactor * maxHeight);
89+
90+
//
91+
Display display = Display.getDefault();
92+
// Construct path
93+
Path path = new Path(display);
94+
for (int i = 0; i < CURSOR_POINTS.length; i += 2) {
95+
float x = zoomFactor * CURSOR_POINTS[i];
96+
float y = zoomFactor * CURSOR_POINTS[i + 1];
97+
if (i == 0) {
98+
path.moveTo(x, y);
99+
} else {
100+
path.lineTo(x, y);
101+
}
102+
}
103+
// Construct image
104+
ImageData imageData = new ImageData(width, height, 32, new PaletteData(0xFF0000, 0x00FF00, 0x0000FF));
105+
imageData.alphaData = new byte[width * height];
106+
Image image = new Image(display, imageData);
107+
GC gc = new GC(image);
108+
gc.setAlpha(0);
109+
gc.fillRectangle(0, 0, width, height);
110+
gc.setAlpha(255);
111+
gc.setAntialias(SWT.ON);
112+
gc.setLineWidth(1);
113+
gc.setBackground(ColorConstants.white);
114+
gc.fillPath(path);
115+
gc.setBackground(ColorConstants.black);
116+
gc.drawPath(path);
117+
gc.dispose();
118+
path.dispose();
119+
// Image is already scaled to expected zoom level
120+
imageData = image.getImageData(100);
121+
image.dispose();
122+
return imageData;
123+
}
124+
125+
/**
126+
* Returns the image descriptor for the GEF cursor. Never {@code null}.
127+
*
128+
* @return Either the default or a custom cursor descriptor.
129+
*/
130+
public static ImageDescriptor getCursorDescriptor() {
131+
return CURRENT_CURSOR_DESCRIPTOR;
132+
}
133+
134+
/**
135+
* Convenience method to allow replacing the default cursor descriptor. An
136+
* exception is thrown if {@code cursorDescriptor} is {@code null}.
137+
*
138+
* @param cursorDescriptor The new cursor descriptor.
139+
*/
140+
public static void setCursorDescriptor(ImageDescriptor cursorDescriptor) {
141+
Objects.requireNonNull(cursorDescriptor, "The new cursor descriptor must not be null!"); //$NON-NLS-1$
142+
CURRENT_CURSOR_DESCRIPTOR = cursorDescriptor;
143+
}
144+
}

org.eclipse.gef/src/org/eclipse/gef/internal/icons/plug-cursor.svg

Lines changed: 1 addition & 5 deletions
Loading

org.eclipse.gef/src/org/eclipse/gef/internal/icons/plugnot-cursor.svg

Lines changed: 0 additions & 8 deletions
Loading

org.eclipse.gef/src/org/eclipse/gef/internal/icons/tree_add-cursor.svg

Lines changed: 0 additions & 10 deletions
Loading

org.eclipse.gef/src/org/eclipse/gef/internal/icons/tree_move-cursor.svg

Lines changed: 0 additions & 10 deletions
Loading

0 commit comments

Comments
 (0)