From 7b3544dc0cdc2b12eb2337abc37c63b47a1b0470 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Tue, 9 Jun 2015 21:44:20 +0300 Subject: [PATCH] Real fix for subpixels in Grid (#18213) Change-Id: I26cea74541629988c2ea8be8db54bd62442ecf59 --- .../src/com/vaadin/client/ComputedStyle.java | 59 +++++++ client/src/com/vaadin/client/WidgetUtil.java | 131 +++++++++++++++ .../com/vaadin/client/widgets/Escalator.java | 158 +----------------- 3 files changed, 194 insertions(+), 154 deletions(-) diff --git a/client/src/com/vaadin/client/ComputedStyle.java b/client/src/com/vaadin/client/ComputedStyle.java index e806e9e197..7a04296b4b 100644 --- a/client/src/com/vaadin/client/ComputedStyle.java +++ b/client/src/com/vaadin/client/ComputedStyle.java @@ -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); + }-*/; + } diff --git a/client/src/com/vaadin/client/WidgetUtil.java b/client/src/com/vaadin/client/WidgetUtil.java index 7d4f613e4e..b9d22193de 100644 --- a/client/src/com/vaadin/client/WidgetUtil.java +++ b/client/src/com/vaadin/client/WidgetUtil.java @@ -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; + } } diff --git a/client/src/com/vaadin/client/widgets/Escalator.java b/client/src/com/vaadin/client/widgets/Escalator.java index 514fce26dc..3e08cc1f1f 100644 --- a/client/src/com/vaadin/client/widgets/Escalator.java +++ b/client/src/com/vaadin/client/widgets/Escalator.java @@ -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. - *

- * 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 - *

- * 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 hack 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 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). - *

- * 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 rows = rowContainer.getElement() - .getRows(); - for (int i = 0; i < rows.getLength(); i++) { - - NodeList 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(); /** -- 2.39.5