JFormDesigner / FlatLaf

FlatLaf - Swing Look and Feel (with Darcula/IntelliJ themes support)
https://www.formdev.com/flatlaf/
Apache License 2.0
3.42k stars 272 forks source link

HiDPI: fix incomplete component repainting at 125% or 175% scaling on Windows #864

Closed DevCharly closed 4 months ago

DevCharly commented 4 months ago

There is a problem in Swing, when using scale factors that end on .25 or .75 (e.g. 1.25, 1.75, 2.25, etc) and repainting single components. Sometimes, under special conditions, the right and/or bottom 1px edge of the component is not repainted.

Following screenshot shows examples where the right and/or bottom 1px edge of a component was not repainted:

grafik

This PR introduces new repaint methods that fix/workaround the issue:

HiDPIUtils.repaint( Component c );
HiDPIUtils.repaint( Component c, Rectangle r );
HiDPIUtils.repaint( Component c, int x, int y, int width, int height );

fixes issues #860 and #582

Repaint Manager

There is also a repaint manager, that also fixes/workarounds the issue, but does not need any change to existing code (no need to use HiDPIUtils.repaint()). This can be useful for custom or 3rd party components. Invoke following on application startup to use it:

HiDPIUtils.installHiDPIRepaintManager();

Be careful if already using a custom repaint manager, or if using a library that may use a custom repaint manager (e.g. SwingX for translucent JXPanel).

In-depth analysis of the Swing issue

When repainting a component using Component.repaint(), the component is first painted to an in-memory image, and then that image is copied to the screen. See javax.swing.RepaintManager.PaintManager.paintDoubleBufferedFPScales().

There are two clipping rectangles involved when copying the image to the screen: sun.java2d.SunGraphics2D.devClip and sun.java2d.SunGraphics2D.usrClip.

devClip is the device clipping in physical pixels. It gets the bounds of the painting component, which is either the passed component, or if it is non-opaque, then the first opaque ancestor of the passed component. It is calculated in sun.java2d.SunGraphics2D.constrain() while getting a graphics context via JComponent.getGraphics().

usrClip is the user clipping, which is set via Graphics clipping methods. This is done in javax.swing.RepaintManager.PaintManager.paintDoubleBufferedFPScales().

The intersection of devClip and usrClip (computed in sun.java2d.SunGraphics2D.validateCompClip()) is used to copy the image to the screen.

Unfortunately different scaling/rounding strategies are used to calculate the two clipping rectangles, which is the reason of the issue.

devClip (see sun.java2d.SunGraphics2D.constrain()):

int devX = (int) (x * scale);
int devWidth = Math.round( width * scale )

usrClip (see javax.swing.RepaintManager.PaintManager.paintDoubleBufferedFPScales()):

int usrX = (int) Math.ceil( (x * scale) - 0.5 );
int usrWidth = ((int) Math.ceil( ((x + width) * scale) - 0.5 )) - usrX;

X/Y coordinates are always rounded down for devClip, but rounded up for usrClip. Width/height calculation is also different.

Conclusion

devClip is sometimes 1px too small.

Workaround

Do repaint on an (larger) ancestor. In this case, devClip is larger because it gets the bounds of the ancestor.

DevCharly commented 4 months ago

Update: added "Repaint Manager" section (see above)

DevCharly commented 3 months ago

Update: there is a bug in HiDPIUtils.installHiDPIRepaintManager() in 3.5. Upgrading to 3.5.1 is recommended.