diff --git a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/ImagesWin32Tests.java b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/ImagesWin32Tests.java index cf48800de3..33f7e3fac2 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/ImagesWin32Tests.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/ImagesWin32Tests.java @@ -134,6 +134,85 @@ public void testHandlesAreDifferentForTwoZoomImageAtDifferentNearestZooms() { } } + /** + * Tests that a GC.drawImage() handle at the exact imageZoom is returned in + * preference to a handle at nearestAvailableZoom when both are present in the + * image's handle manager. + *

+ * Explicitly creating a persistent handle at 200% via + * {@link Image#win32_getHandle(Image, int)} places it in the handle manager. + * When GC.drawImage() then targets a pixel size that maps to imageZoom=200 + * (while nearestAvailableZoom stays 100% because the provider only has 100% + * data), the 200% handle must be found first via the imageZoom lookup and + * returned instead of the 100% one. + *

+ * See https://github.com/eclipse-platform/eclipse.platform.swt/issues/3419 + */ + @Test + public void testDrawImagePrefersExistingHandleAtExactImageZoom() { + PaletteData palette = new PaletteData(0xFF0000, 0xFF00, 0xFF); + ImageData imageData = new ImageData(10, 10, 24, palette); + Image image = new Image(Display.getDefault(), (ImageDataProvider) zoom -> zoom == 100 ? imageData : null); + try { + long handle100 = Image.win32_getHandle(image, 100); + long handle200 = Image.win32_getHandle(image, 200); + assertNotEquals(0L, handle100, "100% handle should be non-zero"); + assertNotEquals(0L, handle200, "200% handle should be non-zero"); + assertNotEquals(handle100, handle200, "Handles for different zooms should be distinct native objects"); + long[] drawHandle = {0}; + // 20x20 pixels → imageZoom=200 for a 10x10 base; nearestAvailableZoom=100 + // The 200% handle already in the handle manager must be found and preferred + image.executeOnImageHandleAtBestFittingSize(h -> drawHandle[0] = h.handle(), 20, 20); + assertEquals(handle200, drawHandle[0], + "GC.drawImage() should prefer the existing handle at imageZoom=200 " + + "over the nearestAvailableZoom=100 handle"); + } finally { + image.dispose(); + } + } + + /** + * Tests that when the pixel size requested by GC.drawImage() maps to a zoom + * equal to a current monitor's zoom, a persistent handle is created for that + * zoom level and is subsequently reusable via + * {@link Image#win32_getHandle(Image, int)}. + *

+ * A shell is created so that {@code Display.getShells()} is non-empty and + * the monitor zoom is visible to the handle-selection logic. Drawing at a + * pixel size whose imageZoom equals the shell's zoom triggers the + * monitor-zoom persistence path: the handle is stored in the image's handle + * manager and can be retrieved without allocating a second native object. + * On 100%-DPI machines the new {@code imageZoom == monitorZoom} branch + * overlaps with the existing {@code nearestAvailableZoom == 100} fallback; + * on HiDPI machines the new branch is exercised in isolation. + *

+ * See https://github.com/eclipse-platform/eclipse.platform.swt/issues/3419 + */ + @Test + public void testDrawImageCreatesAndReusesPersistentHandleForMonitorZoomImageZoom() { + PaletteData palette = new PaletteData(0xFF0000, 0xFF00, 0xFF); + ImageData imageData = new ImageData(10, 10, 24, palette); + Image image = new Image(Display.getDefault(), (ImageDataProvider) zoom -> zoom == 100 ? imageData : null); + Shell shell = new Shell(Display.getDefault()); + try { + int shellZoom = shell.getZoom(); + int pixelSize = 10 * shellZoom / 100; + long[] drawHandle = {0}; + // Drawing at shellZoom's pixel size → imageZoom == shellZoom == monitorZoom + // A persistent handle must be created and stored in the handle manager + image.executeOnImageHandleAtBestFittingSize(h -> drawHandle[0] = h.handle(), pixelSize, pixelSize); + // If persisted, win32_getHandle returns the same native handle without a new allocation + long persistedHandle = Image.win32_getHandle(image, shellZoom); + assertNotEquals(0L, drawHandle[0], "Draw handle should be non-zero"); + assertEquals(persistedHandle, drawHandle[0], + "GC.drawImage() at the monitor zoom should create a persistent handle " + + "so that win32_getHandle returns the same native object"); + } finally { + image.dispose(); + shell.dispose(); + } + } + /** * Tests that a persistent native handle already created via * {@link Image#win32_getHandle(Image, int)} is found and reused by diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java index 29e6fc94ae..962da6ee71 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java @@ -18,6 +18,7 @@ import java.io.*; import java.util.*; +import java.util.List; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import java.util.function.*; @@ -29,6 +30,7 @@ import org.eclipse.swt.internal.gdip.*; import org.eclipse.swt.internal.image.*; import org.eclipse.swt.internal.win32.*; +import org.eclipse.swt.widgets.*; /** * Instances of this class are graphics which have been prepared @@ -261,13 +263,10 @@ private InternalImageHandle getOrCreateImageHandleAtClosestSize(int widthHint, i int imageZoomForHeight = 100 * heightHint / bounds.height; int imageZoom = DPIUtil.getZoomForAutoscaleProperty(Math.max(imageZoomForWidth, imageZoomForHeight)); int nearestAvailableZoom = imageProvider.nearestAvailableZoom(imageZoom); - InternalImageHandle bestFittingHandle = imageHandleManager.get(nearestAvailableZoom); + InternalImageHandle bestFittingHandle = getPersistentHandle(imageZoom, nearestAvailableZoom); if (bestFittingHandle != null) { return bestFittingHandle; } - if (nearestAvailableZoom == 100) { - return getHandleInternal(100, 100); - } if (previousHandle != null && previousHandle.zoom() == nearestAvailableZoom) { temporaryHandleContainer = previousHandle; return previousHandle.handle(); @@ -277,6 +276,35 @@ private InternalImageHandle getOrCreateImageHandleAtClosestSize(int widthHint, i return temporaryHandleContainer.handle(); } + private InternalImageHandle getPersistentHandle(int imageZoom, int nearestAvailableZoom) { + InternalImageHandle bestFittingHandle = imageHandleManager.get(imageZoom); + if (bestFittingHandle != null) { + return bestFittingHandle; + } + if (getShellZooms().contains(imageZoom)) { + return getHandleInternal(imageZoom, imageZoom); + } + bestFittingHandle = imageHandleManager.get(nearestAvailableZoom); + if (bestFittingHandle != null) { + return bestFittingHandle; + } + if (nearestAvailableZoom == 100) { + return getHandleInternal(100, 100); + } + return null; + } + + private Set getShellZooms() { + if (getDevice() instanceof Display display) { + try { + return Arrays.stream(display.getShells()).map(Shell::getZoom).collect(Collectors.toSet()); + } catch (SWTException e) { + return Collections.emptySet(); + } + } + return Collections.emptySet(); + } + } private final HandleAtSize lastRequestedHandle = new HandleAtSize();