From 9911405e543dbe07767808bad88534abbcc03c5a Mon Sep 17 00:00:00 2001 From: Rajat Mahajan Date: Tue, 1 Nov 2022 18:38:00 +0000 Subject: [PATCH] 8282958: Rendering issues of borders, TextFields on Windows High-DPI systems Co-authored-by: Alexey Ivanov Reviewed-by: aivanov, honkar, kizune --- .../javax/swing/border/LineBorder.java | 57 +++- .../LineBorder/ScaledLineBorderTest.java | 299 +++++++++++++++++ .../LineBorder/ScaledTextFieldBorderTest.java | 313 ++++++++++++++++++ 3 files changed, 663 insertions(+), 6 deletions(-) create mode 100644 test/jdk/javax/swing/border/LineBorder/ScaledLineBorderTest.java create mode 100644 test/jdk/javax/swing/border/LineBorder/ScaledTextFieldBorderTest.java diff --git a/src/java.desktop/share/classes/javax/swing/border/LineBorder.java b/src/java.desktop/share/classes/javax/swing/border/LineBorder.java index 6d4fa299ba4..8b4dee4eb8f 100644 --- a/src/java.desktop/share/classes/javax/swing/border/LineBorder.java +++ b/src/java.desktop/share/classes/javax/swing/border/LineBorder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2015, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2022, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,6 +34,9 @@ import java.awt.geom.Path2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RoundRectangle2D; import java.beans.ConstructorProperties; +import java.awt.geom.AffineTransform; + +import static sun.java2d.pipe.Region.clipRound; /** * A class which implements a line border of arbitrary thickness @@ -144,28 +147,70 @@ public class LineBorder extends AbstractBorder if ((this.thickness > 0) && (g instanceof Graphics2D)) { Graphics2D g2d = (Graphics2D) g; + AffineTransform at = g2d.getTransform(); + + // if m01 or m10 is non-zero, then there is a rotation or shear + // or if no Scaling enabled, + // skip resetting the transform + boolean resetTransform = ((at.getShearX() == 0) && (at.getShearY() == 0)) && + ((at.getScaleX() > 1) || (at.getScaleY() > 1)); + + int xtranslation; + int ytranslation; + int w; + int h; + int offs; + + if (resetTransform) { + /* Deactivate the HiDPI scaling transform, + * so we can do paint operations in the device + * pixel coordinate system instead of the logical coordinate system. + */ + g2d.setTransform(new AffineTransform()); + double xx = at.getScaleX() * x + at.getTranslateX(); + double yy = at.getScaleY() * y + at.getTranslateY(); + xtranslation = clipRound(xx); + ytranslation = clipRound(yy); + w = clipRound(at.getScaleX() * width + xx) - xtranslation; + h = clipRound(at.getScaleY() * height + yy) - ytranslation; + offs = this.thickness * (int) at.getScaleX(); + } else { + w = width; + h = height; + xtranslation = x; + ytranslation = y; + offs = this.thickness; + } + + g2d.translate(xtranslation, ytranslation); + Color oldColor = g2d.getColor(); g2d.setColor(this.lineColor); Shape outer; Shape inner; - int offs = this.thickness; int size = offs + offs; if (this.roundedCorners) { float arc = .2f * offs; - outer = new RoundRectangle2D.Float(x, y, width, height, offs, offs); - inner = new RoundRectangle2D.Float(x + offs, y + offs, width - size, height - size, arc, arc); + outer = new RoundRectangle2D.Float(0, 0, w, h, offs, offs); + inner = new RoundRectangle2D.Float(offs, offs, w - size, h - size, arc, arc); } else { - outer = new Rectangle2D.Float(x, y, width, height); - inner = new Rectangle2D.Float(x + offs, y + offs, width - size, height - size); + outer = new Rectangle2D.Float(0, 0, w, h); + inner = new Rectangle2D.Float(offs, offs, w - size, h - size); } Path2D path = new Path2D.Float(Path2D.WIND_EVEN_ODD); path.append(outer, false); path.append(inner, false); g2d.fill(path); g2d.setColor(oldColor); + + g2d.translate(-xtranslation, -ytranslation); + + if (resetTransform) { + g2d.setTransform(at); + } } } diff --git a/test/jdk/javax/swing/border/LineBorder/ScaledLineBorderTest.java b/test/jdk/javax/swing/border/LineBorder/ScaledLineBorderTest.java new file mode 100644 index 00000000000..2d858a8039b --- /dev/null +++ b/test/jdk/javax/swing/border/LineBorder/ScaledLineBorderTest.java @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +/* + * @test + * @bug 8282958 + * @summary Verify LineBorder edges have the same width + * @requires (os.family == "windows") + * @run main ScaledLineBorderTest + */ +public class ScaledLineBorderTest { + private static final Dimension SIZE = new Dimension(120, 25); + + private static final Color OUTER_COLOR = Color.BLACK; + private static final Color BORDER_COLOR = Color.RED; + private static final Color INSIDE_COLOR = Color.WHITE; + private static final Color TRANSPARENT_COLOR = new Color(0x00000000, true); + + private static final double[] scales = + {1.00, 1.25, 1.50, 1.75, 2.00, 2.50, 3.00}; + + private static final List images = + new ArrayList<>(scales.length); + + private static final List panelLocations = + new ArrayList<>(4); + + public static void main(String[] args) throws Exception { + Collection params = Arrays.asList(args); + final boolean showFrame = params.contains("-show"); + final boolean saveImages = params.contains("-save"); + SwingUtilities.invokeAndWait(() -> testScaling(showFrame, saveImages)); + } + + private static void testScaling(boolean showFrame, boolean saveImages) { + JComponent content = createUI(); + if (showFrame) { + showFrame(content); + } + + paintToImages(content, saveImages); + verifyBorderRendering(saveImages); + } + + private static void verifyBorderRendering(final boolean saveImages) { + String errorMessage = null; + int errorCount = 0; + for (int i = 0; i < images.size(); i++) { + BufferedImage img = images.get(i); + double scaling = scales[i]; + try { + int thickness = (int) Math.floor(scaling); + + checkVerticalBorders(SIZE.width / 2, thickness, img); + + for (Point p : panelLocations) { + int y = (int) (p.y * scaling) + SIZE.height / 2; + checkHorizontalBorder(y, thickness, img); + } + } catch (Error e) { + if (errorMessage == null) { + errorMessage = e.getMessage(); + } + errorCount++; + + System.err.printf("Scaling: %.2f\n", scaling); + e.printStackTrace(); + + // Save the image if it wasn't already saved + if (!saveImages) { + saveImage(img, getImageFileName(scaling)); + } + } + } + + if (errorCount > 0) { + throw new Error("Test failed: " + + errorCount + " error(s) detected - " + + errorMessage); + } + } + + private static void checkVerticalBorders(final int x, + final int thickness, + final BufferedImage img) { + checkBorder(x, 0, + 0, 1, + thickness, img); + } + + private static void checkHorizontalBorder(final int y, + final int thickness, + final BufferedImage img) { + checkBorder(0, y, + 1, 0, + thickness, img); + } + + private static void checkBorder(final int xStart, final int yStart, + final int xStep, final int yStep, + final int thickness, + final BufferedImage img) { + final int width = img.getWidth(); + final int height = img.getHeight(); + + State state = State.BACKGROUND; + int borderThickness = 0; + + int x = xStart; + int y = yStart; + do { + do { + final int color = img.getRGB(x, y); + switch (state) { + case BACKGROUND: + if (color == BORDER_COLOR.getRGB()) { + state = State.LEFT; + borderThickness = 1; + } else if (color != OUTER_COLOR.getRGB() + && color != TRANSPARENT_COLOR.getRGB()) { + throwUnexpectedColor(x, y, color); + } + break; + + case LEFT: + if (color == BORDER_COLOR.getRGB()) { + borderThickness++; + } else if (color == INSIDE_COLOR.getRGB()) { + if (borderThickness != thickness) { + throwWrongThickness(thickness, borderThickness, x, y); + } + state = State.INSIDE; + borderThickness = 0; + } else { + throwUnexpectedColor(x, y, color); + } + break; + + case INSIDE: + if (color == BORDER_COLOR.getRGB()) { + state = State.RIGHT; + borderThickness = 1; + } else if (color != INSIDE_COLOR.getRGB()) { + throwUnexpectedColor(x, y, color); + } + break; + + case RIGHT: + if (color == BORDER_COLOR.getRGB()) { + borderThickness++; + } else if (color == OUTER_COLOR.getRGB()) { + if (borderThickness != thickness) { + throwWrongThickness(thickness, borderThickness, x, y); + } + state = State.BACKGROUND; + borderThickness = 0; + } else { + throwUnexpectedColor(x, y, color); + } + } + } while (yStep > 0 && ((y += yStep) < height)); + } while (xStep > 0 && ((x += xStep) < width)); + } + + private enum State { + BACKGROUND, LEFT, INSIDE, RIGHT + } + + private static void throwWrongThickness(int thickness, int borderThickness, + int x, int y) { + throw new Error( + String.format("Wrong border thickness at %d, %d: %d vs %d", + x, y, borderThickness, thickness)); + } + + private static void throwUnexpectedColor(int x, int y, int color) { + throw new Error( + String.format("Unexpected color at %d, %d: %08x", + x, y, color)); + } + + private static JComponent createUI() { + Box contentPanel = Box.createVerticalBox(); + contentPanel.setBackground(OUTER_COLOR); + + Dimension childSize = null; + for (int i = 0; i < 4; i++) { + JComponent filler = new JPanel(null); + filler.setBackground(INSIDE_COLOR); + filler.setPreferredSize(SIZE); + filler.setBounds(i, 0, SIZE.width, SIZE.height); + filler.setBorder(BorderFactory.createLineBorder(BORDER_COLOR)); + + JPanel childPanel = new JPanel(new BorderLayout()); + childPanel.setBorder(BorderFactory.createEmptyBorder(0, i, 4, 4)); + childPanel.add(filler, BorderLayout.CENTER); + childPanel.setBackground(OUTER_COLOR); + + contentPanel.add(childPanel); + if (childSize == null) { + childSize = childPanel.getPreferredSize(); + } + childPanel.setBounds(0, childSize.height * i, childSize.width, childSize.height); + } + + contentPanel.setSize(childSize.width, childSize.height * 4); + + // Save coordinates of the panels + for (Component comp : contentPanel.getComponents()) { + panelLocations.add(comp.getLocation()); + } + + return contentPanel; + } + + private static void showFrame(JComponent content) { + JFrame frame = new JFrame("Scaled Line Border Test"); + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + frame.getContentPane().add(content, BorderLayout.CENTER); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + private static void paintToImages(final JComponent content, + final boolean saveImages) { + for (double scaling : scales) { + BufferedImage image = + new BufferedImage((int) Math.ceil(content.getWidth() * scaling), + (int) Math.ceil(content.getHeight() * scaling), + BufferedImage.TYPE_INT_ARGB); + + Graphics2D g2d = image.createGraphics(); + g2d.scale(scaling, scaling); + content.paint(g2d); + g2d.dispose(); + + if (saveImages) { + saveImage(image, getImageFileName(scaling)); + } + images.add(image); + } + } + + private static String getImageFileName(final double scaling) { + return String.format("test%.2f.png", scaling); + } + + private static void saveImage(BufferedImage image, String filename) { + try { + ImageIO.write(image, "png", new File(filename)); + } catch (IOException e) { + // Don't propagate the exception + e.printStackTrace(); + } + } +} diff --git a/test/jdk/javax/swing/border/LineBorder/ScaledTextFieldBorderTest.java b/test/jdk/javax/swing/border/LineBorder/ScaledTextFieldBorderTest.java new file mode 100644 index 00000000000..5e2868ed506 --- /dev/null +++ b/test/jdk/javax/swing/border/LineBorder/ScaledTextFieldBorderTest.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import javax.imageio.ImageIO; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; +import javax.swing.border.LineBorder; + +/* + * @test + * @bug 8282958 + * @summary Verify all the borders are rendered consistently for a JTextField + * in Windows LaF which uses LineBorder + * @requires (os.family == "windows") + * @run main ScaledTextFieldBorderTest + */ +public class ScaledTextFieldBorderTest { + + private static final double[] scales = { + 1.00, 1.25, 1.50, 1.75, + 2.00, 2.25, 2.50, 2.75, + 3.00 + }; + + private static final List images = + new ArrayList<>(scales.length); + + private static final List panelLocations = + new ArrayList<>(4); + + private static Dimension textFieldSize; + + public static void main(String[] args) throws Exception { + Collection params = Arrays.asList(args); + final boolean showFrame = params.contains("-show"); + final boolean saveImages = params.contains("-save"); + SwingUtilities.invokeAndWait(() -> testScaling(showFrame, saveImages)); + } + + private static void testScaling(boolean showFrame, boolean saveImages) { + JComponent content = createUI(); + if (showFrame) { + showFrame(content); + } + + paintToImages(content, saveImages); + verifyBorderRendering(saveImages); + } + + private static void verifyBorderRendering(final boolean saveImages) { + String errorMessage = null; + int errorCount = 0; + for (int i = 0; i < images.size(); i++) { + BufferedImage img = images.get(i); + double scaling = scales[i]; + try { + int thickness = (int) Math.floor(scaling); + + checkVerticalBorders(textFieldSize.width / 2, thickness, img); + + for (Point p : panelLocations) { + int y = (int) (p.y * scaling) + textFieldSize.height / 2; + checkHorizontalBorder(y, thickness, img); + } + } catch (Error | Exception e) { + if (errorMessage == null) { + errorMessage = e.getMessage(); + } + errorCount++; + + System.err.printf("Scaling: %.2f\n", scaling); + e.printStackTrace(); + + // Save the image if it wasn't already saved + if (!saveImages) { + saveImage(img, getImageFileName(scaling)); + } + } + } + + if (errorCount > 0) { + throw new Error("Test failed: " + + errorCount + " error(s) detected - " + + errorMessage); + } + } + + private static void checkVerticalBorders(final int x, + final int thickness, + final BufferedImage img) { + checkBorder(x, 0, + 0, 1, + thickness, img); + } + + private static void checkHorizontalBorder(final int y, + final int thickness, + final BufferedImage img) { + checkBorder(0, y, + 1, 0, + thickness, img); + } + + private enum State { + BACKGROUND, LEFT, INSIDE, RIGHT + } + + private static final int transparentColor = 0x00000000; + private static int panelColor; + private static int borderColor; + private static int insideColor; + + private static void checkBorder(final int xStart, final int yStart, + final int xStep, final int yStep, + final int thickness, + final BufferedImage img) { + final int width = img.getWidth(); + final int height = img.getHeight(); + + State state = State.BACKGROUND; + int borderThickness = -1; + + int x = xStart; + int y = yStart; + do { + do { + final int color = img.getRGB(x, y); + switch (state) { + case BACKGROUND: + if (color == borderColor) { + state = State.LEFT; + borderThickness = 1; + } else if (color != panelColor + && color != transparentColor) { + throwUnexpectedColor(x, y, color); + } + break; + + case LEFT: + if (color == borderColor) { + borderThickness++; + } else if (color == insideColor) { + if (borderThickness != thickness) { + throwWrongThickness(thickness, borderThickness, x, y); + } + state = State.INSIDE; + borderThickness = 0; + } else { + throwUnexpectedColor(x, y, color); + } + break; + + case INSIDE: + if (color == borderColor) { + state = State.RIGHT; + borderThickness = 1; + } else if (color != insideColor) { + throwUnexpectedColor(x, y, color); + } + break; + + case RIGHT: + if (color == borderColor) { + borderThickness++; + } else if (color == panelColor) { + if (borderThickness != thickness) { + throwWrongThickness(thickness, borderThickness, x, y); + } + state = State.BACKGROUND; + borderThickness = 0; + } else { + throwUnexpectedColor(x, y, color); + } + } + } while (yStep > 0 && ((y += yStep) < height)); + } while (xStep > 0 && ((x += xStep) < width)); + + if (state != State.BACKGROUND) { + throw new Error(String.format("Border is not rendered correctly at %d, %d", x, y)); + } + } + + private static void throwWrongThickness(int thickness, int borderThickness, int x, int y) { + throw new Error( + String.format("Wrong border thickness at %d, %d: %d vs %d", + x, y, borderThickness, thickness)); + } + + private static void throwUnexpectedColor(int x, int y, int color) { + throw new Error( + String.format("Unexpected color at %d, %d: %08x", + x, y, color)); + } + + private static JComponent createUI() { + JPanel contentPanel = new JPanel(); + contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); + + final LineBorder tfBorder = new LineBorder(Color.RED); + + for (int i = 0; i < 4; i++) { + JTextField textField = new JTextField(10); + textField.setBorder(tfBorder); + Box childPanel = Box.createHorizontalBox(); + childPanel.add(Box.createHorizontalStrut(i)); + childPanel.add(textField); + childPanel.add(Box.createHorizontalStrut(4)); + + contentPanel.add(childPanel); + if (textFieldSize == null) { + textFieldSize = textField.getPreferredSize(); + borderColor = tfBorder.getLineColor().getRGB(); + insideColor = textField.getBackground().getRGB(); + } + textField.setBounds(i, 0, textFieldSize.width, textFieldSize.height); + childPanel.setBounds(0, (textFieldSize.height + 4) * i, + textFieldSize.width + i + 4, textFieldSize.height); + } + + contentPanel.setSize(textFieldSize.width + 4, + (textFieldSize.height + 4) * 4); + + panelColor = contentPanel.getBackground().getRGB(); + + // Save coordinates of the panels + for (Component comp : contentPanel.getComponents()) { + panelLocations.add(comp.getLocation()); + } + + return contentPanel; + } + + private static void showFrame(JComponent content) { + JFrame frame = new JFrame("Text Field Border Test"); + frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + frame.getContentPane().add(content, BorderLayout.CENTER); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + private static void paintToImages(final JComponent content, + final boolean saveImages) { + for (double scaling : scales) { + BufferedImage image = + new BufferedImage((int) Math.ceil(content.getWidth() * scaling), + (int) Math.ceil(content.getHeight() * scaling), + BufferedImage.TYPE_INT_ARGB); + + Graphics2D g2d = image.createGraphics(); + g2d.scale(scaling, scaling); + content.paint(g2d); + g2d.dispose(); + + if (saveImages) { + saveImage(image, getImageFileName(scaling)); + } + images.add(image); + } + } + + private static String getImageFileName(final double scaling) { + return String.format("test%.2f.png", scaling); + } + + private static void saveImage(BufferedImage image, String filename) { + try { + ImageIO.write(image, "png", new File(filename)); + } catch (IOException e) { + // Don't propagate the exception + e.printStackTrace(); + } + } +}