eclipse-platform / eclipse.platform.swt

Eclipse SWT
https://www.eclipse.org/swt/
Eclipse Public License 2.0
118 stars 139 forks source link

[Win32] ImageDataProvider: incorrectly reported zoom level #1579

Open tmssngr opened 2 weeks ago

tmssngr commented 2 weeks ago

Describe the bug An ImageDataProvider can be used to provide different ImageData instances for the passed zoom level. This does not work correctly - the passed zoom level is incorrect. It "snaps" to multiples of 100. For 150% zoom level on Windows 11, I'm getting the value 100, for 175% zoom level I'm getting 200.

To Reproduce Run this snippet on Windows 11 with different monitor zoom levels (I only have one 4k monitor attached):

import java.io.*;
import java.nio.file.*;

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

public class ImageDpiTest {

    public static void main(String[] args) throws IOException {
        final ImageData imageData1x;
        try (InputStream inputStream = Files.newInputStream(Paths.get("test1x.png"))) {
            imageData1x = new ImageData(inputStream);
        }

        final ImageData imageData2x;
        try (InputStream inputStream = Files.newInputStream(Paths.get("test2x.png"))) {
            imageData2x = new ImageData(inputStream);
        }

        final Display display = new Display();
        final Image image = new Image(display, (ImageDataProvider)zoom -> {
            System.out.println("zoom = " + zoom);
            return zoom >= 150 ? imageData2x : imageData1x;
        });

        final Shell shell = new Shell(display);
        shell.setLayout(new GridLayout(1, false));

        final Label label = new Label(shell, SWT.NORMAL);
        label.setImage(image);
        label.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));

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

        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }

        display.dispose();
    }
}

It will log the reported zoom level.

Expected behavior For 150% Windows/monitor zoom level, I expect 150 to be reported, for 175% I expect to get 175. Otherwise the image datas I report back are too small for 150% zoom level.

Screenshots For 150% zoom level:

zoom-150% - 100

For 175% zoom level:

zoom-175% - 200

Environment:

  1. Select the platform(s) on which the behavior is seen:

      • [ ] All OS
      • [x] Windows
      • [ ] Linux
      • [ ] macOS
  2. Additional OS info (e.g. OS version, Linux Desktop, etc) Tried on Windows 11 23H2 with one 32" 4k monitor attached.

Workaround (or) Additional context Set swt.autoScale to false, but this will cause other problems.

HeikoKlare commented 2 weeks ago

Expected behavior For 150% Windows/monitor zoom level, I expect 150 to be reported, for 175% I expect to get 175. Otherwise the image datas I report back are too small for 150% zoom level.

Can you please explain where this expectation comes from? The behavior should have never been like this. Actually, the behavior of having the device zoom being "rounded" to multiples of 100 is intended as being established several years ago: https://eclipse.dev/eclipse/news/4.6/platform.php#swt-autoscale-tweaks

If the application is supposed to scale everything according to the actual native zoom (including images), you should use swt.autoScale=quarter or swt.autoScale=exact.

tmssngr commented 2 weeks ago

My expectation comes from the fact that numbers like 100 or 200 are used. If the rounding to 100 would be the goal, I would have expected the use of 1 and 2 (factor).

tmssngr commented 2 weeks ago

Background: until now we were using swt.autoScale=none because this gave us access to the full resolution on Windows with 4k monitors.

Is there also a mode where my solely my ImageDataProvider decides what image to use (without any magic auto-zooming under the hood)?

BTW, how I can request the actual shell's zoom level now?

HeikoKlare commented 2 weeks ago

I have to admit that I do not understand the actual issue: is there any behavior that has changed? With swt.autoScale=none, the zoom value should have always been rounded to a multiple of 100. none is not a valid value for swt.autoScale, so the code always defaults back to this: https://github.com/eclipse-platform/eclipse.platform.swt/blob/d30571a8ec3a756f7b806574f15506e153b07254/bundles/org.eclipse.swt/Eclipse%20SWT/common/org/eclipse/swt/internal/DPIUtil.java#L651-L653

Also the image constructor retrieving the image data has always taken this device zoom, if I am not mistaken. So would be interesting to know where the behavior has actually changed in comparison to, e.g., the previous release of Eclipse SWT.

Is there also a mode where my solely my ImageDataProvider decides what image to use (without any magic auto-zooming under the hood)?

In the ImageDataProvider, you can do whatever you want. No one forces you to consider the zoom value passed to it. However, the question is how you want to provide properly scaled image data if you do not consider the context and its scaling. With the improved HiDPI support coming up (https://eclipse.dev/eclipse/news/4.34/platform.php#rescale-on-runtime-preference), you need to consider the context in which an image is used to adapt it to the zoom level of the monitor currently working on. If you do not want to support this kind of functionality in the future, you could fall back to, e.g., the non-API DPIUtil#getNativeDeviceZoom() to retrieve the original zoom value of the OS.

BTW, how I can request the actual shell's zoom level now?

A shell does not have any public API to retrieve its zoom. It has the getZoom() method to retrieve the zoom used by SWT (which in your scenario is 100/200/...) and it has nativeZoom with the original native zoom of the application, but none of them is accessible outside SWT. In which scenario do you need to retrieve the zoom of a shell? See comment below

laeubi commented 2 weeks ago

A shell does not have any public API to retrieve its zoom.

Shell.getMonitor().getZoom() ?

tmssngr commented 2 weeks ago

In the ImageDataProvider, you can do whatever you want. No one forces you to consider the zoom value passed to it. However, the question is how you want to provide properly scaled image data if you do not consider the context and its scaling.

I've tried our application with the new SWT library with swt.autoScale set to quarter or exact. It caused auto-scaling for some images (e.g. in buttons), because there were some scaling artefacts visible, e.g. duplicate pixels where in the 100% and 200% images were just 1-pixel width.

tmssngr commented 1 week ago

What can I do to use our 200% zoom images for a system zoom level of 150%? How can I avoid any auto-scaling for the image datas provided by my ImageDataProvider?

tmssngr commented 1 week ago

Also the image constructor retrieving the image data has always taken this device zoom, if I am not mistaken. So would be interesting to know where the behavior has actually changed in comparison to, e.g., the previous release of Eclipse SWT.

We were using the swt.autoScale=false (sorry, my above mentioned none was the wrong value) on Windows since SWT began to supported HiDPI, because it looked better. Now I assume that the new monitor-specific zoom handling seems to require that we avoid this hack.

HeikoKlare commented 1 week ago

What can I do to use our 200% zoom images for a system zoom level of 150%? How can I avoid any auto-scaling for the image datas provided by my ImageDataProvider?

From my understanding, for a system zoom of 150%, the 200% images will only be used when using quarter or exact mode (and then scaled down from 200% to 150%). If you are using integer200 or false, the effective application zoom is 100% and thus the 100% images are requested by SWT. But it should have always been like this. Auto-scaling of images is only performed if the image is not provided in the same zoom as required by the consumer, e.g., if you provide a 200% image while the application requests a 150% one. But if you are running your application with swt.autoScale=false, it should have never used the 200% images, has it?

We were using the swt.autoScale=false (sorry, my above mentioned none was the wrong value) on Windows since SWT began to supported HiDPI, because it looked better. Now I assume that the new monitor-specific zoom handling seems to require that we avoid this hack.

The new monitor-specific scaling should not affect the existing behavior in any way. It is supposed to be an opt-in feature (for now). So if anything works different for you then it did before, there is probably some regression that we missed so far. We actively test integer200 (as the default) and quarter (as the "reasonable") mode, so we may have missed some changes that unintentionally affected the false mode.

tmssngr commented 1 week ago

So SWT always performs auto-scaling for images (e.g. for zoom levels like 150% or 175%) and there is no way to avoid it?

HeikoKlare commented 1 week ago

So SWT always performs auto-scaling for images (e.g. for zoom levels like 150% or 175%) and there is no way to avoid it?

It depends:

But as said: this is existing behavior of SWT for years, and none of the recent changes should have affected that.

tmssngr commented 1 week ago

How do I get the exact bounds of the loaded image (with swt.autoScale unset)? Note, that getBoundsInPixels() is deprecated and hence no allowed answer. ;)

Background: we are loading an image containing multiple sub-images: directories The code that extracts the subimages knows the subimage counts per row/column and hence asserts that bounds.width % columnCount == 0 and bounds.height % rowCount == 0.