diff options
author | Tomi Virtanen <tltv@vaadin.com> | 2014-05-16 11:25:01 +0300 |
---|---|---|
committer | Sauli Tähkäpää <sauli@vaadin.com> | 2014-06-05 15:48:13 +0300 |
commit | 9e494f526cad2e6302cddd31200800450d3c857e (patch) | |
tree | 69f4c8b13097f6a5ce9d354dca60134f47174154 /client | |
parent | 94a051b54283230758280f9b7a609e27763bac93 (diff) | |
download | vaadin-framework-9e494f526cad2e6302cddd31200800450d3c857e.tar.gz vaadin-framework-9e494f526cad2e6302cddd31200800450d3c857e.zip |
Fix for 'Aborting layout after 100 passess' (#13359)
'Aborting layout after 100 passes.' is caused by LayoutManager falling
into a loop on rounding fractional layout slot sizes up and down while
trying to fit layout's content in the space available. LayoutManager
round always up, that causes this issue with IE9+ and Chrome. This
change helps LayoutManager to round fractional sizes down for browsers
that causes problems if rounded up.
Browsers may fall into the loop especially with a zoom level other than
100%. Not with any zoom level though. Problematic zoom level varies by
browser. OrderedLayoutExpandTest uses zoom levels other than 100%. Test
for Chrome is the only one that really is able to reproduce error
without the fix. IE9/10 would too, but the zoom level could not be set
exactly to the required 95% for IE. Test works best as a regression test
for other browsers.
Change-Id: Ie840b074df5fed5ea3b15fba9a6fd372a5c0b76a
Diffstat (limited to 'client')
4 files changed, 271 insertions, 23 deletions
diff --git a/client/src/com/vaadin/client/LayoutManager.java b/client/src/com/vaadin/client/LayoutManager.java index bf79009f4c..e17c0233b3 100644 --- a/client/src/com/vaadin/client/LayoutManager.java +++ b/client/src/com/vaadin/client/LayoutManager.java @@ -36,6 +36,7 @@ import com.vaadin.client.ui.VNotification; import com.vaadin.client.ui.layout.ElementResizeEvent; import com.vaadin.client.ui.layout.ElementResizeListener; import com.vaadin.client.ui.layout.LayoutDependencyTree; +import com.vaadin.client.ui.orderedlayout.AbstractOrderedLayoutConnector; public class LayoutManager { private static final String LOOP_ABORT_MESSAGE = "Aborting layout after 100 passes. This would probably be an infinite loop."; @@ -720,6 +721,16 @@ public class LayoutManager { Profiler.enter("LayoutManager.measureConnector"); Element element = connector.getWidget().getElement(); MeasuredSize measuredSize = getMeasuredSize(connector); + if (isBrowserOptimizedMeasuringNeeded() + && isWrappedInsideExpandBlock(connector)) { + // this fixes zoom/sub-pixel issues with ie9+ and Chrome + // (#13359) + // Update measuring logic to round up and/or down depending on + // browser. + measuredSize.setMeasurer(new MeasuredSize.RoundingMeasurer()); + } else { + measuredSize.setMeasurer(null);// resets to default measurer + } MeasureResult measureResult = measuredAndUpdate(element, measuredSize); if (measureResult.isChanged()) { @@ -729,6 +740,23 @@ public class LayoutManager { Profiler.leave("LayoutManager.measureConnector"); } + /** + * Return true if browser may need some optimized width and height measuring + * for MeasuredSize. + * <p> + * Usually optimization is needed to avoid unnecessary scroll bars appearing + * in layouts caused by sub-pixel rounding. And to avoid LayoutManager + * doLayout() going into a loop trying to fit content with fractional size + * (percentages) inside a parent element and stopping only after safety + * iteration limit exceeds, also caused by sub-pixel rounding. + * <p> + * For internal use only. May be removed or replaced in the future. + */ + private static boolean isBrowserOptimizedMeasuringNeeded() { + return BrowserInfo.get().isChrome() + || (BrowserInfo.get().isIE() && !BrowserInfo.get().isIE8()); + } + private void onConnectorChange(ComponentConnector connector, boolean widthChanged, boolean heightChanged) { Profiler.enter("LayoutManager.onConnectorChange"); @@ -810,6 +838,26 @@ public class LayoutManager { return connector instanceof ManagedLayout; } + /** + * Is the given connector wrapped inside a 'expanding' content. Detected by + * checking if the connector's parent is AbstractOrderedLayoutConnector that + * expands. 'Expand' means that some layout slots may expand its content + * width or height to some percentage fraction. + * + * @param connector + * The connector to check + * @return True if connector is wrapped inside a + * AbstractOrderedLayoutConnector that expands + */ + private static boolean isWrappedInsideExpandBlock( + ComponentConnector connector) { + ServerConnector parent = connector.getParent(); + if (parent instanceof AbstractOrderedLayoutConnector) { + return ((AbstractOrderedLayoutConnector) parent).needsExpand(); + } + return false; + } + public void forceLayout() { ConnectorMap connectorMap = connection.getConnectorMap(); JsArrayObject<ComponentConnector> componentConnectors = connectorMap diff --git a/client/src/com/vaadin/client/MeasuredSize.java b/client/src/com/vaadin/client/MeasuredSize.java index 2531ff9389..4005a2651d 100644 --- a/client/src/com/vaadin/client/MeasuredSize.java +++ b/client/src/com/vaadin/client/MeasuredSize.java @@ -43,6 +43,49 @@ public class MeasuredSize { } } + public interface Measurer { + int measureHeight(Element element); + + int measureWidth(Element element); + } + + /** + * Default measurer rounds always up. + */ + public static class DefaultMeasurer implements Measurer { + + @Override + public int measureHeight(Element element) { + int requiredHeight = Util.getRequiredHeight(element); + return requiredHeight; + } + + @Override + public int measureWidth(Element element) { + int requiredWidth = Util.getRequiredWidth(element); + return requiredWidth; + } + } + + /** + * RoundingMeasurer measurer rounds up and down, optimized for different + * browsers. + */ + public static class RoundingMeasurer implements Measurer { + + @Override + public int measureHeight(Element element) { + double requiredHeight = Util.getPreciseRequiredHeight(element); + return Util.roundPreciseSize(requiredHeight); + } + + @Override + public int measureWidth(Element element) { + double requiredWidth = Util.getPreciseRequiredWidth(element); + return Util.roundPreciseSize(requiredWidth); + } + } + private int width = -1; private int height = -1; @@ -52,6 +95,15 @@ public class MeasuredSize { private FastStringSet dependents = FastStringSet.create(); + private Measurer measurer = new DefaultMeasurer(); + + public void setMeasurer(Measurer measurer) { + if (measurer == null) { + measurer = new DefaultMeasurer(); + } + this.measurer = measurer; + } + public int getOuterHeight() { return height; } @@ -236,7 +288,7 @@ public class MeasuredSize { Profiler.leave("Measure borders"); Profiler.enter("Measure height"); - int requiredHeight = Util.getRequiredHeight(element); + int requiredHeight = measurer.measureHeight(element); int marginHeight = sumHeights(margins); int oldHeight = height; int oldWidth = width; @@ -247,7 +299,7 @@ public class MeasuredSize { Profiler.leave("Measure height"); Profiler.enter("Measure width"); - int requiredWidth = Util.getRequiredWidth(element); + int requiredWidth = measurer.measureWidth(element); int marginWidth = sumWidths(margins); if (setOuterWidth(requiredWidth + marginWidth)) { debugSizeChange(element, "Width (outer)", oldWidth, width); diff --git a/client/src/com/vaadin/client/Util.java b/client/src/com/vaadin/client/Util.java index e031b37422..bec88032bb 100644 --- a/client/src/com/vaadin/client/Util.java +++ b/client/src/com/vaadin/client/Util.java @@ -627,16 +627,48 @@ public class Util { return reqHeight; } - public static native int getRequiredWidthBoundingClientRect( - com.google.gwt.dom.client.Element element) - /*-{ - if (element.getBoundingClientRect) { - var rect = element.getBoundingClientRect(); - return Math.ceil(rect.right - rect.left); + /** + * Gets the border-box width for the given element, i.e. element width + + * border + padding. + * + * @param element + * The element to check + * @return The border-box width for the element + */ + public static double getPreciseRequiredWidth( + com.google.gwt.dom.client.Element element) { + double reqWidth; + if (browserUsesPreciseSizeByComputedStyle()) { + reqWidth = getPreciseWidthByComputedStyle(element); } else { - return element.offsetWidth; + reqWidth = getPreciseBoundingClientRectWidth(element); } - }-*/; + return reqWidth; + } + + /** + * Gets the border-box height for the given element, i.e. element height + + * border + padding. + * + * @param element + * The element to check + * @return The border-box height for the element + */ + public static double getPreciseRequiredHeight( + com.google.gwt.dom.client.Element element) { + double reqHeight; + if (browserUsesPreciseSizeByComputedStyle()) { + reqHeight = getPreciseHeightByComputedStyle(element); + } else { + reqHeight = getPreciseBoundingClientRectHeight(element); + } + return reqHeight; + } + + public static int getRequiredWidthBoundingClientRect( + com.google.gwt.dom.client.Element element) { + return (int) Math.ceil(getPreciseBoundingClientRectWidth(element)); + } public static native int getRequiredHeightComputedStyle( com.google.gwt.dom.client.Element element) @@ -678,18 +710,10 @@ public class Util { return Math.ceil(width+border+padding); }-*/; - public static native int getRequiredHeightBoundingClientRect( - com.google.gwt.dom.client.Element element) - /*-{ - var height; - if (element.getBoundingClientRect != null) { - var rect = element.getBoundingClientRect(); - height = Math.ceil(rect.bottom - rect.top); - } else { - height = element.offsetHeight; - } - return height; - }-*/; + public static int getRequiredHeightBoundingClientRect( + com.google.gwt.dom.client.Element element) { + return (int) Math.ceil(getPreciseBoundingClientRectHeight(element)); + } public static int getRequiredWidth(Widget widget) { return getRequiredWidth(widget.getElement()); @@ -700,6 +724,128 @@ public class Util { } /** + * Rounds pixel size up or down depending on the browser. + * <p> + * <li> + * IE9, IE10 - rounds always down to closest integer + * <li> + * Others - rounds half up to closest integer (1.49 -> 1.0, 1.5 -> 2.0) + * + * @param preciseSize + * Decimal number to round. + * @return Rounded integer + */ + public static int roundPreciseSize(double preciseSize) { + boolean roundAlwaysDown = BrowserInfo.get().isIE9() + || BrowserInfo.get().isIE10(); + if (roundAlwaysDown) { + return (int) preciseSize; + } else { + // round to closest integer + return (int) Math.round(preciseSize); + } + } + + /** + * Returns getBoundingClientRect.width, if supported. Otherwise return + * offsetWidth. Includes the padding, scrollbar, and the border. Excludes + * the margin. + * + * @param elem + * Target element to measure + */ + private static native double getPreciseBoundingClientRectWidth(Element elem) + /*-{ + try { + if (elem.getBoundingClientRect) { + return elem.getBoundingClientRect().width; + } else { + return (elem.offsetWidth) || 0; + } + } catch (e) { + // JS exception is thrown if the elem is not attached to the document. + return 0; + } + }-*/; + + /** + * Returns getBoundingClientRect.height, if supported. Otherwise return + * offsetHeight. Includes the padding, scrollbar, and the border. Excludes + * the margin. + * + * @param elem + * Target element to measure + */ + private static native double getPreciseBoundingClientRectHeight(Element elem) + /*-{ + try { + if (elem.getBoundingClientRect) { + return elem.getBoundingClientRect().height; + } else { + return (elem.offsetHeight) || 0; + } + } catch (e) { + // JS exception is thrown if the elem is not attached to the document. + return 0; + } + }-*/; + + /** + * Parse computed styles to get precise width for the given element. + * Excludes the margin. Fallback to offsetWidth if computed styles is not + * defined, or if width can't be read or it's not fractional. + * + * @param elem + * Target element to measure + */ + private static native double getPreciseWidthByComputedStyle( + com.google.gwt.dom.client.Element elem) + /*-{ + var cs = elem.ownerDocument.defaultView.getComputedStyle(elem); + var size = cs && cs.getPropertyValue('width') || ''; + if (size && size.indexOf('.') > -1) { + size = parseFloat(size) + + parseInt(cs.getPropertyValue('padding-left')) + + parseInt(cs.getPropertyValue('padding-right')) + + parseInt(cs.getPropertyValue('border-left-width')) + + parseInt(cs.getPropertyValue('border-right-width')); + } else { + size = elem.offsetWidth; + } + return size; + }-*/; + + /** + * Parse computed styles to get precise height for the given element. + * Excludes the margin. Fallback to offsetHeight if computed styles is not + * defined, or if height can't be read or it's not fractional. + * + * @param elem + * Target element to measure + */ + private static native double getPreciseHeightByComputedStyle( + com.google.gwt.dom.client.Element elem) + /*-{ + var cs = elem.ownerDocument.defaultView.getComputedStyle(elem); + var size = cs && cs.getPropertyValue('height') || ''; + if (size && size.indexOf('.') > -1) { + size = parseFloat(size) + + parseInt(cs.getPropertyValue('padding-top')) + + parseInt(cs.getPropertyValue('padding-bottom')) + + parseInt(cs.getPropertyValue('border-top-width')) + + parseInt(cs.getPropertyValue('border-bottom-width')); + } else { + size = elem.offsetHeight; + } + return size; + }-*/; + + /** For internal use only. May be removed or replaced in the future. */ + private static boolean browserUsesPreciseSizeByComputedStyle() { + return BrowserInfo.get().isIE9(); + } + + /** * Detects what is currently the overflow style attribute in given element. * * @param pe diff --git a/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java b/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java index 0c09ae49c6..c20eda71dc 100644 --- a/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java +++ b/client/src/com/vaadin/client/ui/orderedlayout/AbstractOrderedLayoutConnector.java @@ -551,8 +551,10 @@ public abstract class AbstractOrderedLayoutConnector extends /** * Does the layout need to expand? + * <p> + * For internal use only. May be removed or replaced in the future. */ - private boolean needsExpand() { + public boolean needsExpand() { return needsExpand; } |