]> source.dussan.org Git - vaadin-framework.git/commitdiff
Real fix for subpixels in Grid (#18213)
authorArtur Signell <artur@vaadin.com>
Tue, 9 Jun 2015 18:44:20 +0000 (21:44 +0300)
committerVaadin Code Review <review@vaadin.com>
Fri, 26 Jun 2015 07:30:55 +0000 (07:30 +0000)
Change-Id: I4246b8e5db528436fe355bdf57d2b3f88173474c

client/src/com/vaadin/client/ComputedStyle.java
client/src/com/vaadin/client/WidgetUtil.java
client/src/com/vaadin/client/widgets/Escalator.java

index e806e9e197df069f2d4cb1b5c67bf43baa0e6668..7a04296b4b3700eeaedbb439a4bb332ed0184799 100644 (file)
@@ -129,6 +129,15 @@ public class ComputedStyle {
         
     }-*/;
 
+    /**
+     * Retrieves the given computed property as an integer
+     *
+     * Returns 0 if the property cannot be converted to an integer
+     * 
+     * @param name
+     *            the property to retrieve
+     * @return the integer value of the property or 0
+     */
     public final int getIntProperty(String name) {
         Profiler.enter("ComputedStyle.getIntProperty");
         String value = getProperty(name);
@@ -137,6 +146,23 @@ public class ComputedStyle {
         return result;
     }
 
+    /**
+     * Retrieves the given computed property as a double
+     *
+     * Returns NaN if the property cannot be converted to a double
+     * 
+     * @param name
+     *            the property to retrieve
+     * @return the double value of the property
+     */
+    public final int getDoubleProperty(String name) {
+        Profiler.enter("ComputedStyle.getDoubleProperty");
+        String value = getProperty(name);
+        int result = parseDoubleNative(value);
+        Profiler.leave("ComputedStyle.getDoubleProperty");
+        return result;
+    }
+
     /**
      * Get current margin values from the DOM. The array order is the default
      * CSS order: top, right, bottom, left.
@@ -176,6 +202,26 @@ public class ComputedStyle {
         return border;
     }
 
+    /**
+     * Returns the current width from the DOM.
+     * 
+     * @since
+     * @return the computed width
+     */
+    public double getWidth() {
+        return getDoubleProperty("width");
+    }
+
+    /**
+     * Returns the current height from the DOM.
+     * 
+     * @since
+     * @return the computed height
+     */
+    public double getHeight() {
+        return getDoubleProperty("height");
+    }
+
     /**
      * Takes a String value e.g. "12px" and parses that to Integer 12.
      * 
@@ -221,4 +267,17 @@ public class ComputedStyle {
             return number;
     }-*/;
 
+    /**
+     * Takes a String value e.g. "12.3px" and parses that to a double, 12.3.
+     * 
+     * @param String
+     *            a value starting with a number
+     * @return the value from the string before any non-numeric characters or
+     *         NaN if the value cannot be parsed as a number
+     */
+    private static native int parseDoubleNative(final String value)
+    /*-{
+        return parseFloat(value);
+    }-*/;
+
 }
index 7d4f613e4ef5e1311cd9f09669df01fc24b76cf4..b9d22193deaa4997bbc70db8699ae182882eb44d 100644 (file)
@@ -389,6 +389,7 @@ public class WidgetUtil {
     }
 
     private static int detectedScrollbarSize = -1;
+    private static int detectedSubPixelRoundingFactor = -1;
 
     public static int getNativeScrollbarSize() {
         if (detectedScrollbarSize < 0) {
@@ -1630,4 +1631,134 @@ public class WidgetUtil {
             return heightWithBorder - heightWithoutBorder;
         }
     }-*/;
+
+    /**
+     * Rounds the given size up to a value which the browser will accept.
+     * 
+     * Safari/WebKit uses 1/64th of a pixel to enable using integer math
+     * (http://trac.webkit.org/wiki/LayoutUnit).
+     * 
+     * Firefox uses 1/60th of a pixel because it is divisible by three
+     * (https://bugzilla.mozilla.org/show_bug.cgi?id=1070940)
+     * 
+     * @since
+     * @param size
+     *            the value to round
+     * @return the rounded value
+     */
+    public static double roundSizeUp(double size) {
+        return roundSize(size, true);
+    }
+
+    /**
+     * Rounds the given size down to a value which the browser will accept.
+     * 
+     * Safari/WebKit uses 1/64th of a pixel to enable using integer math
+     * (http://trac.webkit.org/wiki/LayoutUnit).
+     * 
+     * Firefox uses 1/60th of a pixel because it is divisible by three
+     * (https://bugzilla.mozilla.org/show_bug.cgi?id=1070940)
+     * 
+     * IE9+ uses 1/100th of a pixel
+     * 
+     * @since
+     * @param size
+     *            the value to round
+     * @return the rounded value
+     */
+    public static double roundSizeDown(double size) {
+        return roundSize(size, false);
+    }
+
+    private static double roundSize(double size, boolean roundUp) {
+        if (BrowserInfo.get().isIE8()) {
+            if (roundUp) {
+                return Math.ceil(size);
+            } else {
+                return (int) size;
+            }
+        }
+
+        double factor = getSubPixelRoundingFactor();
+        if (factor < 0 || size < 0) {
+            return size;
+        }
+
+        if (roundUp) {
+            return roundSizeUp(size, factor);
+        } else {
+            return roundSizeDown(size, factor);
+        }
+    }
+
+    /**
+     * Returns the factor used by browsers to round subpixel values
+     * 
+     * @since
+     * @return the factor N used by the browser when storing subpixels as X+Y/N
+     */
+    private static double getSubPixelRoundingFactor() {
+        // Detects how the browser does subpixel rounding
+        // Currently Firefox uses 1/60th pixels
+        // and Safari uses 1/64th pixels
+        // IE 1/100th pixels
+        if (detectedSubPixelRoundingFactor != -1) {
+            return detectedSubPixelRoundingFactor;
+        }
+
+        double probeSize = 0.999999;
+        DivElement div = Document.get().createDivElement();
+        Document.get().getBody().appendChild(div);
+        div.getStyle().setHeight(probeSize, Unit.PX);
+        ComputedStyle computedStyle = new ComputedStyle(div);
+        double computedHeight = computedStyle.getHeight();
+
+        if (computedHeight < probeSize) {
+            // Rounded down by browser, all browsers but Firefox do this
+            // today
+            detectedSubPixelRoundingFactor = (int) Math
+                    .round(1.0 / (1.0 - computedHeight));
+        } else {
+            // Rounded up / to nearest by browser
+            probeSize = 1;
+
+            while (computedStyle.getHeight() != 0.0) {
+                computedHeight = computedStyle.getHeight();
+                probeSize /= 2.0;
+                div.getStyle().setHeight(probeSize, Unit.PX);
+            }
+
+            detectedSubPixelRoundingFactor = (int) Math
+                    .round(1.0 / computedHeight);
+        }
+
+        div.removeFromParent();
+        return detectedSubPixelRoundingFactor;
+    }
+
+    private static double roundSizeUp(double size, double divisor) {
+        // In: 12.51, 60.0
+
+        // 12
+        double integerPart = (int) size;
+
+        // (12.51 - 12) * 60 = 30.6
+        double nrFractions = (size - integerPart) * divisor;
+
+        // 12 + ceil(30.6) / 60 = 12 + 31/60 = 12.51666
+        return integerPart + (Math.ceil(nrFractions)) / divisor;
+    }
+
+    private static double roundSizeDown(double size, double divisor) {
+        // In: 12.51, 60.0
+
+        // 12
+        double integerPart = (int) size;
+
+        // (12.51 - 12) * 60 = 30.6
+        double nrFractions = (size - integerPart) * divisor;
+
+        // 12 + int(30.6) / 60 = 12 + 30/60 = 12.5
+        return integerPart + ((int) nrFractions) / divisor;
+    }
 }
index 514fce26dc83484f94fa56654aaf9c05de1e513a..3e08cc1f1fc434b9dc53c9524a8a8a839bd5c075 100644 (file)
@@ -821,7 +821,6 @@ public class Escalator extends Widget implements RequiresResize,
             double scrollContentHeight = body.calculateTotalRowHeight()
                     + body.spacerContainer.getSpacerHeightsSum();
             double scrollContentWidth = columnConfiguration.calculateRowWidth();
-
             double tableWrapperHeight = heightOfEscalator;
             double tableWrapperWidth = widthOfEscalator;
 
@@ -879,7 +878,6 @@ public class Escalator extends Widget implements RequiresResize,
                     .getCalculatedColumnsWidth(Range.between(
                             columnConfiguration.getFrozenColumnCount(),
                             columnConfiguration.getColumnCount()));
-            unfrozenPixels -= subpixelBrowserBugDetector.getActiveAdjustment();
             double frozenPixels = scrollContentWidth - unfrozenPixels;
             double hScrollOffsetWidth = tableWrapperWidth - frozenPixels;
             horizontalScrollbar.setOffsetSize(hScrollOffsetWidth);
@@ -2085,7 +2083,6 @@ public class Escalator extends Widget implements RequiresResize,
                 rowElement.insertBefore(cellClone, cellOriginal);
                 double requiredWidth = WidgetUtil
                         .getRequiredWidthBoundingClientRectDouble(cellClone);
-
                 if (BrowserInfo.get().isIE()) {
                     /*
                      * IE browsers have some issues with subpixels. Occasionally
@@ -4147,8 +4144,7 @@ public class Escalator extends Widget implements RequiresResize,
          * @return the width of a row, in pixels
          */
         public double calculateRowWidth() {
-            return getCalculatedColumnsWidth(Range.between(0, getColumnCount()))
-                    - subpixelBrowserBugDetector.getActiveAdjustment();
+            return getCalculatedColumnsWidth(Range.between(0, getColumnCount()));
         }
 
         private void assertArgumentsAreValidAndWithinRange(final int index,
@@ -4179,8 +4175,6 @@ public class Escalator extends Widget implements RequiresResize,
          */
         @Override
         public void insertColumns(final int index, final int numberOfColumns) {
-            subpixelBrowserBugDetector.invalidateFix();
-
             // Validate
             if (index < 0 || index > getColumnCount()) {
                 throw new IndexOutOfBoundsException("The given index(" + index
@@ -4333,11 +4327,10 @@ public class Escalator extends Widget implements RequiresResize,
                 int index = entry.getKey().intValue();
                 double width = entry.getValue().doubleValue();
 
-                if (index == getColumnCount() - 1) {
-                    subpixelBrowserBugDetector.invalidateFix();
-                }
-
                 checkValidColumnIndex(index);
+
+                // Not all browsers will accept any fractional size..
+                width = WidgetUtil.roundSizeDown(width);
                 columns.get(index).setWidth(width);
 
             }
@@ -4347,8 +4340,6 @@ public class Escalator extends Widget implements RequiresResize,
             body.reapplyColumnWidths();
             footer.reapplyColumnWidths();
 
-            subpixelBrowserBugDetector.checkAndFix();
-
             recalculateElementSizes();
         }
 
@@ -4443,145 +4434,6 @@ public class Escalator extends Widget implements RequiresResize,
         }
     }
 
-    private class SubpixelBrowserBugDetector {
-        private static final double SUBPIXEL_ADJUSTMENT = .1;
-        private boolean fixActive = false;
-
-        /**
-         * This is a fix essentially for Firefox and how it handles subpixels.
-         * <p>
-         * Even if an element has {@code style="width: 1000.12px"}, the bounding
-         * box's width in Firefox is usually nothing of that sort. It's actually
-         * 1000.11669921875 (in version 35.0.1). That's not even close, when
-         * talking about floating point precision. Other browsers handle the
-         * subpixels way better
-         * <p>
-         * In any case, we need to fix that. And that's fixed by simply checking
-         * if the sum of the width of all the cells is larger than the width of
-         * the row. If it is, we <i>hack</i> the last column
-         * {@value #SUBPIXEL_ADJUSTMENT}px narrower.
-         */
-        public void checkAndFix() {
-            if (!fixActive && hasSubpixelBrowserBug()) {
-                fixSubpixelBrowserBug();
-                fixActive = true;
-            }
-        }
-
-        private double getActiveAdjustment() {
-            if (fixActive) {
-                return -SUBPIXEL_ADJUSTMENT;
-            } else {
-                return 0.0;
-            }
-        }
-
-        public void invalidateFix() {
-            adjustBookkeepingPixels(SUBPIXEL_ADJUSTMENT);
-            fixActive = false;
-        }
-
-        private boolean hasSubpixelBrowserBug() {
-            final RowContainer rowContainer;
-            if (header.getRowCount() > 0) {
-                rowContainer = header;
-            } else if (body.getRowCount() > 0) {
-                rowContainer = body;
-            } else if (footer.getRowCount() > 0) {
-                rowContainer = footer;
-            } else {
-                return false;
-            }
-
-            double sumOfCellWidths = 0;
-            TableRowElement tr = rowContainer.getElement().getRows().getItem(0);
-
-            if (tr == null) {
-                /*
-                 * for some weird reason, the row might be null at this point in
-                 * (some?) webkit browsers.
-                 */
-                return false;
-            }
-
-            NodeList<TableCellElement> cells = tr.getCells();
-            assert cells != null : "cells was null, why is it null?";
-
-            for (int i = 0; i < cells.getLength(); i++) {
-                TableCellElement cell = cells.getItem(i);
-                if (!cell.getStyle().getDisplay()
-                        .equals(Display.NONE.getCssName())) {
-                    sumOfCellWidths += WidgetUtil.getBoundingClientRect(cell)
-                            .getWidth();
-                }
-            }
-
-            double rowWidth = WidgetUtil.getBoundingClientRect(tr).getWidth();
-            return sumOfCellWidths >= rowWidth;
-        }
-
-        private void fixSubpixelBrowserBug() {
-            assert columnConfiguration.getColumnCount() > 0 : "Why are we running this code if there are no columns?";
-
-            adjustBookkeepingPixels(-SUBPIXEL_ADJUSTMENT);
-
-            fixSubpixelBrowserBugFor(header);
-            fixSubpixelBrowserBugFor(body);
-            fixSubpixelBrowserBugFor(footer);
-        }
-
-        private void adjustBookkeepingPixels(double adjustment) {
-            int lastColumnIndex = columnConfiguration.columns.size() - 1;
-            if (lastColumnIndex < 0) {
-                return;
-            }
-
-            columnConfiguration.columns.get(lastColumnIndex).calculatedWidth += adjustment;
-            if (columnConfiguration.widthsArray != null) {
-                columnConfiguration.widthsArray[lastColumnIndex] += adjustment;
-            }
-        }
-
-        /**
-         * Adjust the last non-spanned cell by {@link #SUBPIXEL_ADJUSTMENT} (
-         * {@value #SUBPIXEL_ADJUSTMENT}px).
-         * <p>
-         * We'll do this brute-force, by individually measuring and shrinking
-         * the last non-spanned cell. Brute-force, since each row might be
-         * spanned differently - we can't simply pick one index and one width,
-         * and mass-apply that to everything :(
-         */
-        private void fixSubpixelBrowserBugFor(RowContainer rowContainer) {
-            if (rowContainer.getRowCount() == 0) {
-                return;
-            }
-
-            NodeList<TableRowElement> rows = rowContainer.getElement()
-                    .getRows();
-            for (int i = 0; i < rows.getLength(); i++) {
-
-                NodeList<TableCellElement> cells = rows.getItem(i).getCells();
-                TableCellElement lastNonspannedCell = null;
-                for (int j = cells.getLength() - 1; j >= 0; j--) {
-                    TableCellElement cell = cells.getItem(j);
-                    if (!cell.getStyle().getDisplay()
-                            .equals(Display.NONE.getCssName())) {
-                        lastNonspannedCell = cell;
-                        break;
-                    }
-                }
-
-                assert lastNonspannedCell != null : "all cells were \"display: none\" on row "
-                        + i + " in " + rowContainer.getElement().getTagName();
-
-                double cellWidth = WidgetUtil.getBoundingClientRect(
-                        lastNonspannedCell).getWidth();
-                double newWidth = cellWidth - SUBPIXEL_ADJUSTMENT;
-                lastNonspannedCell.getStyle().setWidth(newWidth, Unit.PX);
-            }
-        }
-    }
-
     /**
      * A decision on how to measure a spacer when it is partially within a
      * designated range.
@@ -5647,8 +5499,6 @@ public class Escalator extends Widget implements RequiresResize,
         }
     };
 
-    private final SubpixelBrowserBugDetector subpixelBrowserBugDetector = new SubpixelBrowserBugDetector();
-
     private final ElementPositionBookkeeper positions = new ElementPositionBookkeeper();
 
     /**