Initial implementation of layout system, along with the first two layout widgets. Review: http://gwt-code-reviews.appspot.com/51830 (TBR, until jlabanca has time to finish reviewing) git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5877 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/javadoc/com/google/gwt/examples/LayoutPanelExample.java b/user/javadoc/com/google/gwt/examples/LayoutPanelExample.java new file mode 100644 index 0000000..13372d7 --- /dev/null +++ b/user/javadoc/com/google/gwt/examples/LayoutPanelExample.java
@@ -0,0 +1,55 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.examples; + +import static com.google.gwt.dom.client.Style.Unit.PCT; + +import com.google.gwt.core.client.EntryPoint; +import com.google.gwt.layout.client.Layout.Layer; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.LayoutPanel; +import com.google.gwt.user.client.ui.RootLayoutPanel; +import com.google.gwt.user.client.ui.Widget; + +public class LayoutPanelExample implements EntryPoint { + + public void onModuleLoad() { + // Attach two child widgets to a LayoutPanel, laying them out horizontally, + // splitting at 50%. + Widget childOne = new HTML(), childTwo = new HTML(); + LayoutPanel p = new LayoutPanel(); + p.add(childOne); + p.add(childTwo); + + Layer layerOne = p.getLayer(childOne), layerTwo = p.getLayer(childTwo); + layerOne.setLeftWidth(0, PCT, 50, PCT); + layerTwo.setRightWidth(0, PCT, 50, PCT); + + // Note the explicit call to layout(). This is required for the layout to + // take effect. + p.layout(); + + // Attach the LayoutPanel to the RootLayoutPanel. The latter will listen for + // resize events on the window to ensure that its children are informed of + // possible size changes. + RootLayoutPanel rp = RootLayoutPanel.get(); + rp.add(p); + + // The RootLayoutPanel also requires that its layout() method be explicitly + // called for the initial layout to take effect. + rp.layout(); + } +} \ No newline at end of file
diff --git a/user/src/com/google/gwt/layout/Layout.gwt.xml b/user/src/com/google/gwt/layout/Layout.gwt.xml new file mode 100644 index 0000000..6619b69 --- /dev/null +++ b/user/src/com/google/gwt/layout/Layout.gwt.xml
@@ -0,0 +1,25 @@ +<!-- --> +<!-- Copyright 2009 Google Inc. --> +<!-- Licensed under the Apache License, Version 2.0 (the "License"); you --> +<!-- may not use this file except in compliance with the License. You may --> +<!-- may obtain a copy of the License at --> +<!-- --> +<!-- http://www.apache.org/licenses/LICENSE-2.0 --> +<!-- --> +<!-- Unless required by applicable law or agreed to in writing, software --> +<!-- distributed under the License is distributed on an "AS IS" BASIS, --> +<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or --> +<!-- implied. License for the specific language governing permissions and --> +<!-- limitations under the License. --> + +<module> + <inherits name="com.google.gwt.core.Core"/> + <inherits name="com.google.gwt.user.UserAgent"/> + <inherits name="com.google.gwt.dom.DOM"/> + <inherits name="com.google.gwt.animation.Animation"/> + + <replace-with class="com.google.gwt.layout.client.LayoutImplIE6"> + <when-type-is class="com.google.gwt.layout.client.LayoutImpl"/> + <when-property-is name="user.agent" value="ie6"/> + </replace-with> +</module>
diff --git a/user/src/com/google/gwt/layout/client/Layout.java b/user/src/com/google/gwt/layout/client/Layout.java new file mode 100644 index 0000000..6df50e1 --- /dev/null +++ b/user/src/com/google/gwt/layout/client/Layout.java
@@ -0,0 +1,640 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.layout.client; + +import static com.google.gwt.dom.client.Style.Unit.PX; + +import com.google.gwt.animation.client.Animation; +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.Unit; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class for laying out a container element and its children. + * + * <p> + * This class is typically used by higher-level widgets to implement layout on + * their behalf. It is intended to wrap an element (usually a <div>), and + * lay its children out in a predictable fashion, automatically accounting for + * changes to the parent's size, and for all elements' margins, borders, and + * padding. + * </p> + * + * <p> + * To use this class, create a container element (again, usually a <div>) + * and pass it to {@link #Layout(Element)}. Rather than attaching child elements + * directly to the element managed by this {@link Layout}, use the + * {@link Layout#attachChild(Element)} method. This will attach the child + * element and return a {@link Layout.Layer} object which is used to manage the + * child. + * </p> + * + * <p> + * A separate {@link Layout.Layer} instance is associated with each child + * element. There is a set of methods available on this class to manipulate the + * child element's position and size. In order for changes to a layer to take + * effect, you must finally call one of {@link #layout()} or + * {@link #animate(int)}. This allows many changes to different layers to be + * applied efficiently, and to be animated. + * </p> + * + * <p> + * On most browsers, this is implemented using absolute positioning. It also + * contains extra logic to make IE6 work properly. + * </p> + * + * <p> + * <h3>Example</h3> + * {@example com.google.gwt.examples.LayoutExample} + * </p> + * + * <p> + * NOTE: This class will <em>only</em> work in standards mode, which requires + * that the HTML page in which it is run have an explicit <!DOCTYPE> + * declaration. + * </p> + * + * <p> + * NOTE: This class is still very new, and its interface may change without + * warning. Use at your own risk. + * </p> + */ +public class Layout { + + /** + * Callback interface used by {@link Layout#animate(int, AnimationCallback)} + * to provide updates on animation progress. + */ + public interface AnimationCallback { + + /** + * Called immediately after the animation is complete, and the entire layout + * is in its final state. + */ + void onAnimationComplete(); + + /** + * Called at each step of the animation, for each layer being laid out. + * + * @param layer the layer being laid out + */ + void onLayout(Layer layer, double progress); + } + + /** + * This class is used to set the position and size of child elements. + * + * <p> + * Each child element has three values associated with each axis: {left, + * right, width} on the horizontal axis, and {top, bottom, height} on the + * vertical axis. Precisely two of three values may be set at a time, or the + * system will be over- or under-contrained. For this reason, the following + * methods are provided for setting these values: + * <ul> + * <li>{@link #setLeftRight(double, Unit, double, Unit)}</li> + * <li>{@link #setLeftWidth(double, Unit, double, Unit)}</li> + * <li>{@link #setRightWidth(double, Unit, double, Unit)}</li> + * <li>{@link #setTopBottom(double, Unit, double, Unit)}</li> + * <li>{@link #setTopHeight(double, Unit, double, Unit)}</li> + * <li>{@link #setBottomHeight(double, Unit, double, Unit)}</li> + * </ul> + * </p> + * + * <p> + * By default, each layer is set to fill the entire parent (i.e., {left, top, + * right, bottom} = {0, 0, 0, 0}). + * </p> + */ + public class Layer { + final Element container, child; + + Object userObject; + + boolean setLeft, setRight, setTop, setBottom, setWidth, setHeight; + boolean setTargetLeft = true, setTargetRight = true, setTargetTop = true, + setTargetBottom = true, setTargetWidth, setTargetHeight; + Unit leftUnit, topUnit, rightUnit, bottomUnit, widthUnit, heightUnit; + Unit targetLeftUnit = PX, targetTopUnit = PX, targetRightUnit = PX, + targetBottomUnit = PX, targetWidthUnit, targetHeightUnit; + double left, top, right, bottom, width, height; + double sourceLeft, sourceTop, sourceRight, sourceBottom, sourceWidth, + sourceHeight; + double targetLeft, targetTop, targetRight, targetBottom, targetWidth, + targetHeight; + + Layer(Element container, Element child, Object userObject) { + this.container = container; + this.child = child; + this.userObject = userObject; + } + + /** + * Gets the container element associated with this layer. + * + * <p> + * This is the element that sits between the parent and child elements. It + * is normally necessary to operate on this element only when you need to + * modify certain CSS properties, such as visibility. + * </p> + * + * @return the container element + */ + public Element getContainerElement() { + return container; + } + + /** + * Gets the user-data associated with this layer. + * + * @return the layer's user-data object + */ + public Object getUserObject() { + return this.userObject; + } + + /** + * Sets the layer's bottom and height values. + * + * @param bottom + * @param bottomUnit + * @param height + * @param heightUnit + */ + public void setBottomHeight(double bottom, Unit bottomUnit, double height, + Unit heightUnit) { + this.setTargetBottom = this.setTargetHeight = true; + this.setTargetTop = false; + this.targetBottom = bottom; + this.targetHeight = height; + this.targetBottomUnit = bottomUnit; + this.targetHeightUnit = heightUnit; + } + + /** + * Sets the layer's left and right values. + * + * @param left + * @param leftUnit + * @param right + * @param rightUnit + */ + public void setLeftRight(double left, Unit leftUnit, double right, + Unit rightUnit) { + this.setTargetLeft = this.setTargetRight = true; + this.setTargetWidth = false; + this.targetLeft = left; + this.targetRight = right; + this.targetLeftUnit = leftUnit; + this.targetRightUnit = rightUnit; + } + + /** + * Sets the layer's left and width values. + * + * @param left + * @param leftUnit + * @param width + * @param widthUnit + */ + public void setLeftWidth(double left, Unit leftUnit, double width, + Unit widthUnit) { + this.setTargetLeft = this.setTargetWidth = true; + this.setTargetRight = false; + this.targetLeft = left; + this.targetWidth = width; + this.targetLeftUnit = leftUnit; + this.targetWidthUnit = widthUnit; + } + + /** + * Sets the layer's right and width values. + * + * @param right + * @param rightUnit + * @param width + * @param widthUnit + */ + public void setRightWidth(double right, Unit rightUnit, double width, + Unit widthUnit) { + this.setTargetRight = this.setTargetWidth = true; + this.setTargetLeft = false; + this.targetRight = right; + this.targetWidth = width; + this.targetRightUnit = rightUnit; + this.targetWidthUnit = widthUnit; + } + + /** + * Sets the layer's top and bottom values. + * + * @param top + * @param topUnit + * @param bottom + * @param bottomUnit + */ + public void setTopBottom(double top, Unit topUnit, double bottom, + Unit bottomUnit) { + this.setTargetTop = this.setTargetBottom = true; + this.setTargetHeight = false; + this.targetTop = top; + this.targetBottom = bottom; + this.targetTopUnit = topUnit; + this.targetBottomUnit = bottomUnit; + } + + /** + * Sets the layer's top and height values. + * + * @param top + * @param topUnit + * @param height + * @param heightUnit + */ + public void setTopHeight(double top, Unit topUnit, double height, + Unit heightUnit) { + this.setTargetTop = this.setTargetHeight = true; + this.setTargetBottom = false; + this.targetTop = top; + this.targetHeight = height; + this.targetTopUnit = topUnit; + this.targetHeightUnit = heightUnit; + } + } + + private static LayoutImpl impl = GWT.create(LayoutImpl.class); + + private List<Layer> layers = new ArrayList<Layer>(); + private final Element parentElem; + private Animation animation; + + /** + * Constructs a new layout associated with the given parent element. + * + * @param parent the element to serve as the layout parent + */ + public Layout(Element parent) { + this.parentElem = parent; + impl.initParent(parent); + } + + /** + * Updates the layout by animating it over time. + * + * @param duration the duration of the animation + * @see #animate(int, AnimationCallback) + */ + public void animate(int duration) { + animate(duration, null); + } + + /** + * Updates the layout by animating it over time, with a callback on each frame + * of the animation, and upon completion. + * + * @param duration the duration of the animation + * @param callback the animation callback + */ + public void animate(int duration, final AnimationCallback callback) { + // Deal with constraint changes (e.g. left-width => right-width, etc) + int parentWidth = parentElem.getClientWidth(); + int parentHeight = parentElem.getClientHeight(); + for (Layer l : layers) { + adjustHorizontalConstraints(parentWidth, l); + adjustVerticalConstraints(parentHeight, l); + } + + // Cancel the old animation, if there is one. + if (animation != null) { + animation.cancel(); + } + + animation = new Animation() { + @Override + protected void onCancel() { + onComplete(); + } + + @Override + protected void onComplete() { + layout(); + if (callback != null) { + callback.onAnimationComplete(); + } + animation = null; + } + + @Override + protected void onUpdate(double progress) { + for (Layer l : layers) { + if (l.setTargetLeft) { + l.left = l.sourceLeft + (l.targetLeft - l.sourceLeft) * progress; + } + if (l.setTargetRight) { + l.right = l.sourceRight + (l.targetRight - l.sourceRight) + * progress; + } + if (l.setTargetTop) { + l.top = l.sourceTop + (l.targetTop - l.sourceTop) * progress; + } + if (l.setTargetBottom) { + l.bottom = l.sourceBottom + (l.targetBottom - l.sourceBottom) + * progress; + } + if (l.setTargetWidth) { + l.width = l.sourceWidth + (l.targetWidth - l.sourceWidth) + * progress; + } + if (l.setTargetHeight) { + l.height = l.sourceHeight + (l.targetHeight - l.sourceHeight) + * progress; + } + + impl.layout(l); + if (callback != null) { + callback.onLayout(l, progress); + } + } + impl.finalizeLayout(parentElem); + } + }; + + animation.run(duration); + } + + /** + * Asserts that the given child element is managed by this layout + * + * @param elem the element to be tested + */ + public void assertIsChild(Element elem) { + assert elem.getParentElement().getParentElement() == this.parentElem : "Element is not a child of this layout"; + } + + /** + * This method must be called when the parent element becomes attached to the + * document. + * + * @see #detach() + */ + public void attach() { + impl.attach(parentElem); + } + + /** + * Attaches a child element to this layout. + * + * <p> + * This method will attach the child to the layout, removing it from its + * current parent element. Use the {@link Layer} it returns to manipulate the + * child. + * </p> + * + * @param child the child to be attached + * @return the {@link Layer} associated with the element + */ + public Layer attachChild(Element child) { + return attachChild(child, null); + } + + /** + * Attaches a child element to this layout. + * + * <p> + * This method will attach the child to the layout, removing it from its + * current parent element. Use the {@link Layer} it returns to manipulate the + * child. + * </p> + * + * @param child the child to be attached + * @param userObject an arbitrary object to be associated with this layer + * @return the {@link Layer} associated with the element + */ + public Layer attachChild(Element child, Object userObject) { + Element container = impl.attachChild(parentElem, child); + Layer layer = new Layer(container, child, userObject); + layers.add(layer); + return layer; + } + + /** + * This method must be called when the parent element becomes detached from + * the document. + * + * @see #attach() + */ + public void detach() { + impl.detach(parentElem); + } + + /** + * Causes the parent element to fill its own parent. + * + * <p> + * This is most useful for top-level layouts that need to follow the size of + * another element, such as the <body>. + * </p> + */ + public void fillParent() { + impl.fillParent(parentElem); + } + + /** + * Returns the size of one unit, in pixels, in the context of this layout. + * + * <p> + * This will work for any unit type, but be aware that certain unit types, + * such as {@link Unit#EM}, and {@link Unit#EX}, will return different values + * based upon the parent's associated font size. {@link Unit#PCT} is dependent + * upon the parent's actual size, and the axis to be measured. + * </p> + * + * @param unit the unit type to be measured + * @param vertical whether the unit to be measured is on the vertical or + * horizontal axis (this matters only for {@link Unit#PCT}) + * @return the unit size, in pixels + */ + public double getUnitSize(Unit unit, boolean vertical) { + return impl.getUnitSizeInPixels(parentElem, unit, vertical); + } + + /** + * Updates this layout's children immediately. This method <em>must</em> be + * called after updating any of its children's {@link Layer layers}. + */ + public void layout() { + for (Layer l : layers) { + l.left = l.sourceLeft = l.targetLeft; + l.top = l.sourceTop = l.targetTop; + l.right = l.sourceRight = l.targetRight; + l.bottom = l.sourceBottom = l.targetBottom; + l.width = l.sourceWidth = l.targetWidth; + l.height = l.sourceHeight = l.targetHeight; + + l.setLeft = l.setTargetLeft; + l.setTop = l.setTargetTop; + l.setRight = l.setTargetRight; + l.setBottom = l.setTargetBottom; + l.setWidth = l.setTargetWidth; + l.setHeight = l.setTargetHeight; + + l.leftUnit = l.targetLeftUnit; + l.topUnit = l.targetTopUnit; + l.rightUnit = l.targetRightUnit; + l.bottomUnit = l.targetBottomUnit; + l.widthUnit = l.targetWidthUnit; + l.heightUnit = l.targetHeightUnit; + + impl.layout(l); + } + + impl.finalizeLayout(parentElem); + } + + /** + * Removes a child element from this layout. + * + * @param layer the layer associated with the child to be removed + */ + public void removeChild(Layer layer) { + impl.removeChild(layer.container, layer.child); + layers.remove(layer); + } + + private void adjustHorizontalConstraints(int parentWidth, Layer l) { + double leftPx = l.left * getUnitSize(l.leftUnit, false); + double rightPx = l.right * getUnitSize(l.rightUnit, false); + double widthPx = l.width * getUnitSize(l.widthUnit, false); + + if (l.setLeft && !l.setTargetLeft) { + // -left + l.setLeft = false; + + if (!l.setWidth) { + // +width + l.setTargetWidth = true; + l.sourceWidth = (parentWidth - (leftPx + rightPx)) + / getUnitSize(l.targetWidthUnit, false); + } else { + // +right + l.setTargetRight = true; + l.sourceRight = (parentWidth - (leftPx + widthPx)) + / getUnitSize(l.targetRightUnit, false); + } + } else if (l.setWidth && !l.setTargetWidth) { + // -width + l.setWidth = false; + + if (!l.setLeft) { + // +left + l.setTargetLeft = true; + l.sourceLeft = (parentWidth - (rightPx + widthPx)) + / getUnitSize(l.targetLeftUnit, false); + } else { + // +right + l.setTargetRight = true; + l.sourceRight = (parentWidth - (leftPx + widthPx)) + / getUnitSize(l.targetRightUnit, false); + } + } else if (l.setRight && !l.setTargetRight) { + // -right + l.setRight = false; + + if (!l.setWidth) { + // +width + l.setTargetWidth = true; + l.sourceWidth = (parentWidth - (leftPx + rightPx)) + / getUnitSize(l.targetWidthUnit, false); + } else { + // +left + l.setTargetLeft = true; + l.sourceLeft = (parentWidth - (rightPx + widthPx)) + / getUnitSize(l.targetLeftUnit, false); + } + } + + l.setLeft = l.setTargetLeft; + l.setRight = l.setTargetRight; + l.setWidth = l.setTargetWidth; + + l.leftUnit = l.targetLeftUnit; + l.rightUnit = l.targetRightUnit; + l.widthUnit = l.targetWidthUnit; + } + + private void adjustVerticalConstraints(int parentHeight, Layer l) { + double topPx = l.top * getUnitSize(l.topUnit, true); + double bottomPx = l.bottom * getUnitSize(l.bottomUnit, true); + double heightPx = l.height * getUnitSize(l.heightUnit, true); + + if (l.setTop && !l.setTargetTop) { + // -top + l.setTop = false; + + if (!l.setHeight) { + // +height + l.setTargetHeight = true; + l.sourceHeight = (parentHeight - (topPx + bottomPx)) + / getUnitSize(l.targetHeightUnit, true); + } else { + // +bottom + l.setTargetBottom = true; + l.sourceBottom = (parentHeight - (topPx + heightPx)) + / getUnitSize(l.targetBottomUnit, true); + } + } else if (l.setHeight && !l.setTargetHeight) { + // -height + l.setHeight = false; + + if (!l.setTop) { + // +top + l.setTargetTop = true; + l.sourceTop = (parentHeight - (bottomPx + heightPx)) + / getUnitSize(l.targetTopUnit, true); + } else { + // +bottom + l.setTargetBottom = true; + l.sourceBottom = (parentHeight - (topPx + heightPx)) + / getUnitSize(l.targetBottomUnit, true); + } + } else if (l.setBottom && !l.setTargetBottom) { + // -bottom + l.setBottom = false; + + if (!l.setHeight) { + // +height + l.setTargetHeight = true; + l.sourceHeight = (parentHeight - (topPx + bottomPx)) + / getUnitSize(l.targetHeightUnit, true); + } else { + // +top + l.setTargetTop = true; + l.sourceTop = (parentHeight - (bottomPx + heightPx)) + / getUnitSize(l.targetTopUnit, true); + } + } + + l.setTop = l.setTargetTop; + l.setBottom = l.setTargetBottom; + l.setHeight = l.setTargetHeight; + + l.topUnit = l.targetTopUnit; + l.bottomUnit = l.targetBottomUnit; + l.heightUnit = l.targetHeightUnit; + } +}
diff --git a/user/src/com/google/gwt/layout/client/LayoutImpl.java b/user/src/com/google/gwt/layout/client/LayoutImpl.java new file mode 100644 index 0000000..a5e9730 --- /dev/null +++ b/user/src/com/google/gwt/layout/client/LayoutImpl.java
@@ -0,0 +1,167 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.layout.client; + +import static com.google.gwt.dom.client.Style.Unit.EM; +import static com.google.gwt.dom.client.Style.Unit.EX; +import static com.google.gwt.dom.client.Style.Unit.IN; +import static com.google.gwt.dom.client.Style.Unit.PX; + +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.layout.client.Layout.Layer; + +/** + * Default implementation, which works with all browsers except for IE6. It uses + * only the "top", "left", "bottom", "right", "width", and "height" CSS + * properties. + * + * Note: This implementation class has state, so {@link Layout} must create a + * new instance for each layout-parent. + */ +class LayoutImpl { + + private static DivElement fixedRuler; + + static { + Document doc = Document.get(); + fixedRuler = createRuler(IN, IN); + doc.getBody().appendChild(fixedRuler); + } + + protected static DivElement createRuler(Unit widthUnit, Unit heightUnit) { + DivElement ruler = Document.get().createDivElement(); + ruler.setInnerHTML(" "); + Style style = ruler.getStyle(); + style.setPosition(Position.ABSOLUTE); + style.setZIndex(-32767); + style.setLeft(-10000, PX); + style.setWidth(1, widthUnit); + style.setHeight(1, heightUnit); + return ruler; + } + + protected DivElement relativeRuler; + + public Element attachChild(Element parent, Element child) { + DivElement container = Document.get().createDivElement(); + container.appendChild(child); + + container.getStyle().setProperty("position", "absolute"); + container.getStyle().setProperty("overflow", "hidden"); + + fillParent(child); + + Style style = child.getStyle(); + style.setWidth(100, Unit.PCT); + style.setHeight(100, Unit.PCT); + + parent.appendChild(container); + return container; + } + + public void fillParent(Element elem) { + Style style = elem.getStyle(); + style.setPosition(Position.ABSOLUTE); + style.setLeft(0, PX); + style.setTop(0, PX); + style.setRight(0, PX); + style.setBottom(0, PX); + } + + public void finalizeLayout(Element parent) { + } + + public double getUnitSizeInPixels(Element parent, Unit unit, boolean vertical) { + if (unit == null) { + return 1; + } + + switch (unit) { + default: + case PX: + return 1; + case PCT: + return (vertical ? parent.getClientHeight() : parent.getClientWidth()) / 100.0; + case EM: + return relativeRuler.getOffsetWidth(); + case EX: + return relativeRuler.getOffsetHeight(); + case CM: + return fixedRuler.getOffsetWidth(); + case MM: + return fixedRuler.getOffsetWidth() / 10.0; + case IN: + return fixedRuler.getOffsetWidth() / 2.54; + case PT: + return fixedRuler.getOffsetWidth() / 28.4; + case PC: + return fixedRuler.getOffsetWidth() / 2.36; + } + } + + public void initParent(Element parent) { + parent.getStyle().setProperty("position", "relative"); + parent.appendChild(relativeRuler = createRuler(EM, EX)); + } + + public void layout(Layer layer) { + Style style = layer.container.getStyle(); + + style.setProperty("left", layer.setLeft + ? (layer.left + layer.leftUnit.getType()) : ""); + style.setProperty("top", layer.setTop + ? (layer.top + layer.topUnit.getType()) : ""); + style.setProperty("right", layer.setRight + ? (layer.right + layer.rightUnit.getType()) : ""); + style.setProperty("bottom", layer.setBottom + ? (layer.bottom + layer.bottomUnit.getType()) : ""); + style.setProperty("width", layer.setWidth + ? (layer.width + layer.widthUnit.getType()) : ""); + style.setProperty("height", layer.setHeight + ? (layer.height + layer.heightUnit.getType()) : ""); + } + + public void removeChild(Element container, Element child) { + container.removeFromParent(); + + // We want this code to be resilient to the child having already been + // removed from its container (perhaps by widget code). + if (child.getParentElement() == container) { + child.removeFromParent(); + } + + // Cleanup child styles set by fillParent(). + Style style = child.getStyle(); + style.clearPosition(); + style.clearLeft(); + style.clearTop(); + style.clearWidth(); + style.clearHeight(); + } + + public void detach(Element parent) { + // Do nothing. This exists only to help LayoutImplIE6 avoid memory leaks. + } + + public void attach(Element parent) { + // Do nothing. This exists only to help LayoutImplIE6 avoid memory leaks. + } +}
diff --git a/user/src/com/google/gwt/layout/client/LayoutImplIE6.java b/user/src/com/google/gwt/layout/client/LayoutImplIE6.java new file mode 100644 index 0000000..38231a5 --- /dev/null +++ b/user/src/com/google/gwt/layout/client/LayoutImplIE6.java
@@ -0,0 +1,352 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.layout.client; + +import static com.google.gwt.dom.client.Style.Unit.PX; + +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.Overflow; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.event.logical.shared.ResizeEvent; +import com.google.gwt.event.logical.shared.ResizeHandler; +import com.google.gwt.layout.client.Layout.Layer; +import com.google.gwt.user.client.Window; + +/** + * IE6-specific implementation, which uses the "onresize" event, along with a + * series of measurement tools, to deal with several IE6 bugs. Specifically, + * IE6 doesn't support simultaneous left-right and top-bottom values, and it + * misplaces by one pixel elements whose right/bottom properties are set. + * + * Because this implementation gets compiled in for both IE6 and 7, it + * dynamically detects IE7 and punts to the super implementation. + */ +class LayoutImplIE6 extends LayoutImpl { + + private static Element createStyleRuler(Element parent) { + DivElement styleRuler = Document.get().createDivElement(); + DivElement styleInner = Document.get().createDivElement(); + + styleRuler.getStyle().setPosition(Position.ABSOLUTE); + styleRuler.getStyle().setLeft(-10000, PX); + + parent.appendChild(styleRuler); + styleRuler.appendChild(styleInner); + return styleRuler; + } + + private static void hookWindowResize(final Element elem) { + Window.addResizeHandler(new ResizeHandler() { + public void onResize(ResizeEvent event) { + resizeRelativeToParent(elem); + } + }); + } + + private static native void measureDecoration(Element elem) /*-{ + var ruler = elem.__styleRuler; + var inner = ruler.children[0]; + var s = inner.style, cs = elem.currentStyle; + + s.borderLeftStyle = cs.borderLeftStyle; + s.borderRightStyle = cs.borderRightStyle; + s.borderTopStyle = cs.borderTopStyle; + s.borderBottomStyle = cs.borderBottomStyle; + + s.borderLeftWidth = cs.borderLeftWidth; + s.borderRightWidth = cs.borderRightWidth; + s.borderTopWidth = cs.borderTopWidth; + s.borderBottomWidth = cs.borderBottomWidth; + + // Oddly enough, allowing the word 'auto' to creep into the style + // ruler's margin causes it to take up all of its parent's space. + s.marginLeft = (cs.marginLeft == 'auto') ? '' : cs.marginLeft; + s.marginRight = (cs.marginRight == 'auto') ? '' : cs.marginRight; + s.marginTop = (cs.marginTop == 'auto') ? '' : cs.marginTop; + s.marginBottom = (cs.marginBottom == 'auto') ? '' : cs.marginBottom; + + s.paddingLeft = cs.paddingLeft; + s.paddingRight = cs.paddingRight; + s.paddingTop = cs.paddingTop; + s.paddingBottom = cs.paddingBottom; + + s.width = s.height = 32; + elem.__decoWidth = ruler.offsetWidth - 32; + elem.__decoHeight = ruler.offsetHeight - 32; + }-*/; + + private static native void resizeRelativeToParent(Element elem) /*-{ + var parent = elem.__resizeParent; + if (parent) { + @com.google.gwt.layout.client.LayoutImplIE6::measureDecoration(Lcom/google/gwt/dom/client/Element;)(elem); + elem.style.left = 0; + elem.style.top = 0; + elem.style.width = parent.clientWidth - elem.__decoWidth; + elem.style.height = parent.clientHeight - elem.__decoHeight; + } + }-*/; + + private static native void setLayer(Element container, Layer layer) /*-{ + // Potential leak: This is cleaned up in detach(). + container.__layer = layer; + }-*/; + + private static native void setPropertyElement(Element elem, String name, + Element value) /*-{ + elem[name] = value; + }-*/; + + @Override + public void attach(Element parent) { + if (UserAgent.isIE6()) { + // No need to re-connect layer refs. This will be taken care of + // automatically in layout(). + initResizeHandler(parent); + initUnitChangeHandler(parent, relativeRuler); + } + } + + public Element attachChild(Element parent, Element child) { + if (!UserAgent.isIE6()) { + return super.attachChild(parent, child); + } + + DivElement container = Document.get().createDivElement(); + container.appendChild(child); + + container.getStyle().setPosition(Position.ABSOLUTE); + container.getStyle().setOverflow(Overflow.HIDDEN); + child.getStyle().setPosition(Position.ABSOLUTE); + + // Hang the style ruler from the container element, but associate it with + // the child element, so that measureDecoration(child) will work. + setPropertyElement(child, "__styleRuler", createStyleRuler(container)); + + parent.appendChild(container); + return container; + } + + @Override + public void detach(Element parent) { + if (UserAgent.isIE6()) { + removeLayerRefs(parent); + removeResizeHandler(parent); + removeUnitChangeHandler(relativeRuler); + } + } + + @Override + public void fillParent(Element elem) { + if (!UserAgent.isIE6()) { + super.fillParent(elem); + return; + } + + fillParentImpl(elem); + } + + @Override + public void finalizeLayout(Element parent) { + if (!UserAgent.isIE6()) { + super.finalizeLayout(parent); + return; + } + + resizeRelativeToParent(parent); + resizeHandler(parent, true); + } + + @Override + public void initParent(Element parent) { + super.initParent(parent); + + if (UserAgent.isIE6()) { + setPropertyElement(parent, "__styleRuler", createStyleRuler(parent)); + } + } + + public void layout(Layer layer) { + if (!UserAgent.isIE6()) { + super.layout(layer); + return; + } + + Element elem = layer.getContainerElement(); + setLayer(elem, layer); + } + + private native void fillParentImpl(Element elem) /*-{ + // Hook the parent element's onresize event. If the parent is the <body>, + // then we have to go through the Window class to get the resize event, + // because IE6 requires a bunch of foul hackery to safely hook it. + var parent = elem.parentElement; + if (parent.tagName.toLowerCase() == 'body') { + elem.style.position = 'absolute'; + var docElem = parent.parentElement; + elem.__resizeParent = docElem; + @com.google.gwt.layout.client.LayoutImplIE6::hookWindowResize(Lcom/google/gwt/dom/client/Element;)(elem); + return; + } + + function resize() { + @com.google.gwt.layout.client.LayoutImplIE6::resizeRelativeToParent(Lcom/google/gwt/dom/client/Element;)(elem); + } + + elem.__resizeParent = parent; + parent.onresize = resize; + resize(); + }-*/; + + private native void initResizeHandler(Element parent) /*-{ + // Potential leak: This is cleaned up in detach(). + var self = this; + parent.onresize = function() { + self.@com.google.gwt.layout.client.LayoutImplIE6::resizeHandler(Lcom/google/gwt/dom/client/Element;)(parent); + }; + }-*/; + + private native void initUnitChangeHandler(Element parent, Element ruler) /*-{ + // Potential leak: This is cleaned up in detach(). + var self = this; + ruler.onresize = function() { + self.@com.google.gwt.layout.client.LayoutImplIE6::resizeHandler(Lcom/google/gwt/dom/client/Element;Z)(parent, true); + }; + }-*/; + + private native void removeLayerRefs(Element parent) /*-{ + for (var i = 0; i < parent.childNodes.length; ++i) { + var container = parent.childNodes[i]; + if (container.__layer) { + container.__layer = null; + } + } + }-*/; + + private native void removeResizeHandler(Element parent) /*-{ + parent.onresize = null; + }-*/; + + private native void removeUnitChangeHandler(Element ruler) /*-{ + ruler.onresize = null; + }-*/; + + private void resizeHandler(Element parent) { + resizeHandler(parent, false); + } + + private native void resizeHandler(Element parent, boolean force) /*-{ + if (!force && + ((parent.offsetWidth == parent.__oldWidth) && + (parent.offsetHeight == parent.__oldHeight))) { + return; + } + parent.__oldWidth = parent.offsetWidth; + parent.__oldHeight = parent.offsetHeight; + + var parentWidth = parent.clientWidth; + var parentHeight = parent.clientHeight; + + // Iterate over the children, assuming each of them is an unstyled + // container element. + for (var i = 0; i < parent.childNodes.length; ++i) { + // Don't attempt to layout the rulers. + var container = parent.childNodes[i]; + var layer = container.__layer; + if (!layer) { + continue; + } + + // This is ugly, but it's less ugly than writing all these JSNI refs inline. + var _setLeft = layer.@com.google.gwt.layout.client.Layout.Layer::setLeft; + var _setTop = layer.@com.google.gwt.layout.client.Layout.Layer::setTop; + var _setWidth = layer.@com.google.gwt.layout.client.Layout.Layer::setWidth; + var _setHeight = layer.@com.google.gwt.layout.client.Layout.Layer::setHeight; + var _setRight = layer.@com.google.gwt.layout.client.Layout.Layer::setRight; + var _setBottom = layer.@com.google.gwt.layout.client.Layout.Layer::setBottom; + + var _left = layer.@com.google.gwt.layout.client.Layout.Layer::left; + var _top = layer.@com.google.gwt.layout.client.Layout.Layer::top; + var _width = layer.@com.google.gwt.layout.client.Layout.Layer::width; + var _height = layer.@com.google.gwt.layout.client.Layout.Layer::height; + var _right = layer.@com.google.gwt.layout.client.Layout.Layer::right; + var _bottom = layer.@com.google.gwt.layout.client.Layout.Layer::bottom; + + var _leftUnit = layer.@com.google.gwt.layout.client.Layout.Layer::leftUnit; + var _topUnit = layer.@com.google.gwt.layout.client.Layout.Layer::topUnit; + var _widthUnit = layer.@com.google.gwt.layout.client.Layout.Layer::widthUnit; + var _heightUnit = layer.@com.google.gwt.layout.client.Layout.Layer::heightUnit; + var _rightUnit = layer.@com.google.gwt.layout.client.Layout.Layer::rightUnit; + var _bottomUnit = layer.@com.google.gwt.layout.client.Layout.Layer::bottomUnit; + + // Apply the requested position & size values to the element's style. + var style = container.style; + style.left = _setLeft ? (_left + _leftUnit.@com.google.gwt.dom.client.Style.Unit::getType()()) : ""; + style.top = _setTop ? (_top + _topUnit.@com.google.gwt.dom.client.Style.Unit::getType()()) : ""; + style.width = _setWidth ? (_width + _widthUnit.@com.google.gwt.dom.client.Style.Unit::getType()()) : ""; + style.height = _setHeight ? (_height + _heightUnit.@com.google.gwt.dom.client.Style.Unit::getType()()) : ""; + + // If right is defined, reposition/size the container horizontally. + if (_setRight) { + var ratio = this.@com.google.gwt.layout.client.LayoutImplIE6::getUnitSizeInPixels(Lcom/google/gwt/dom/client/Element;Lcom/google/gwt/dom/client/Style$Unit;Z)(parent, _rightUnit, false); + var right = parentWidth - (_right * ratio); + + if (!_setLeft) { + // There's no left defined; adjust the left position move the element to the right edge. + container.style.left = (right - container.offsetWidth) + 'px'; + } else { + // Both left and right are defined; calculate the width and set it. + var leftRatio = this.@com.google.gwt.layout.client.LayoutImplIE6::getUnitSizeInPixels(Lcom/google/gwt/dom/client/Element;Lcom/google/gwt/dom/client/Style$Unit;Z)(parent, _leftUnit, false); + var left = (_left * leftRatio); + if (right > left) { + container.style.width = (right - left) + 'px'; + } + } + } + + // If bottom is defined, reposition/size the container vertically. + if (_setBottom) { + var ratio = this.@com.google.gwt.layout.client.LayoutImplIE6::getUnitSizeInPixels(Lcom/google/gwt/dom/client/Element;Lcom/google/gwt/dom/client/Style$Unit;Z)(parent, _bottomUnit, true); + var bottom = parentHeight - (_bottom * ratio); + + if (!_setTop) { + // There's no top defined; adjust the left position move the element to the bottom edge. + container.style.top = (bottom - container.offsetHeight) + 'px'; + } else { + // Both top and bottom are defined; calculate the height and set it. + var topRatio = this.@com.google.gwt.layout.client.LayoutImplIE6::getUnitSizeInPixels(Lcom/google/gwt/dom/client/Element;Lcom/google/gwt/dom/client/Style$Unit;Z)(parent, _topUnit, true); + var top = (_top * topRatio); + if (bottom > top) { + container.style.height = (bottom - top) + 'px'; + } + } + } + + // Resize the child to take up all of its container's width and height. + var child = container.firstChild; + @com.google.gwt.layout.client.LayoutImplIE6::measureDecoration(Lcom/google/gwt/dom/client/Element;)(child); + var childDecoWidth = child.__decoWidth; + var childDecoHeight = child.__decoHeight; + if (container.offsetWidth > childDecoWidth) { + child.style.width = (container.offsetWidth - childDecoWidth) + 'px'; + } + if (container.offsetHeight > childDecoHeight) { + child.style.height = (container.offsetHeight - childDecoHeight) + 'px'; + } + } + }-*/; +}
diff --git a/user/src/com/google/gwt/layout/client/UserAgent.java b/user/src/com/google/gwt/layout/client/UserAgent.java new file mode 100644 index 0000000..2127a59 --- /dev/null +++ b/user/src/com/google/gwt/layout/client/UserAgent.java
@@ -0,0 +1,45 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.layout.client; + +/** + * User-Agent utility methods used by {@link LayoutImplIE6}. + * + * TODO: Generalize this, move it into a common place, and make it available for + * use by other classes. + */ +class UserAgent { + + // Stolen and modified from UserAgent.gwt.xml. + public static native boolean isIE6() /*-{ + function makeVersion(result) { + return (parseInt(result[1]) * 1000) + parseInt(result[2]); + } + + var ua = navigator.userAgent.toLowerCase(); + if (ua.indexOf("msie") != -1) { + var result = /msie ([0-9]+)\.([0-9]+)/.exec(ua); + if (result && result.length == 3) { + var v = makeVersion(result); + if (v < 7000) { + return true; + } + } + } + + return false; + }-*/; +}
diff --git a/user/src/com/google/gwt/user/client/ui/LayoutComposite.java b/user/src/com/google/gwt/user/client/ui/LayoutComposite.java new file mode 100644 index 0000000..f4f5067 --- /dev/null +++ b/user/src/com/google/gwt/user/client/ui/LayoutComposite.java
@@ -0,0 +1,35 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.user.client.ui; + +/** + * A {@link Composite} implementation that implements {@link RequiresLayout} and + * automatically delegates that interface's methods to its wrapped widget, which + * must itself implement {@link RequiresLayout}. + */ +public abstract class LayoutComposite extends Composite implements RequiresLayout { + + @Override + protected void initWidget(Widget widget) { + assert widget instanceof RequiresLayout : + "LayoutComposite requires that its wrapped widget implement HasLayout"; + super.initWidget(widget); + } + + public void onLayout() { + ((RequiresLayout) getWidget()).onLayout(); + } +}
diff --git a/user/src/com/google/gwt/user/client/ui/LayoutPanel.java b/user/src/com/google/gwt/user/client/ui/LayoutPanel.java new file mode 100644 index 0000000..880b8ae --- /dev/null +++ b/user/src/com/google/gwt/user/client/ui/LayoutPanel.java
@@ -0,0 +1,205 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.user.client.ui; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.layout.client.Layout; +import com.google.gwt.layout.client.Layout.Layer; + +/** + * A panel that lays its children out in arbitrary {@link Layout.Layer layers} + * using the {@link Layout} class. + * + * <p> + * NOTE: This widget will <em>only</em> work in standards mode, which requires + * that the HTML page in which it is run have an explicit <!DOCTYPE> + * declaration. + * </p> + * + * <p> + * NOTE: This class is still very new, and its interface may change without + * warning. Use at your own risk. + * </p> + * + * <p> + * <h3>Example</h3> + * {@example com.google.gwt.examples.LayoutPanelExample} + * </p> + */ +public class LayoutPanel extends ComplexPanel implements RequiresLayout, + ProvidesLayout { + + private final Layout layout; + + /** + * Creates an empty layout panel. + */ + public LayoutPanel() { + setElement(Document.get().createDivElement()); + layout = new Layout(getElement()); + } + + /** + * Adds a widget to this panel. + * + * <p> + * By default, each child will fill the panel. To build more interesting + * layouts, use {@link #getLayer(Widget)} to get the {@link Layout.Layer} + * associated with each child, and set its layout constraints as desired. + * </p> + * + * @param widget the widget to be added + */ + public void add(Widget widget) { + // Detach new child. + widget.removeFromParent(); + + // Logical attach. + getChildren().add(widget); + + // Physical attach. + Layer layer = layout.attachChild(widget.getElement(), widget); + widget.setLayoutData(layer); + + // Adopt. + adopt(widget); + } + + /** + * This method, or one of its overloads, must be called whenever any of the + * {@link Layout.Layer layers} associated with its children is modified. + * + * @see #layout(int) + * @see #layout(int, com.google.gwt.layout.client.Layout.AnimationCallback) + */ + public void layout() { + layout.layout(); + } + + /** + * This method, or one of its overloads, must be called whenever any of the + * {@link Layout.Layer layers} associated with its children is modified. + * + * <p> + * This overload will cause the layout to be updated by animating over a + * specified period of time. + * </p> + * + * @param duration the animation duration, in milliseconds + * + * @see #layout() + * @see #layout(int, com.google.gwt.layout.client.Layout.AnimationCallback) + */ + public void layout(int duration) { + layout.animate(duration); + } + + /** + * This method, or one of its overloads, must be called whenever any of the + * {@link Layout.Layer layers} associated with its children is modified. + * + * <p> + * This overload will cause the layout to be updated by animating over a + * specified period of time. In addition, it provides a callback that will be + * informed of updates to the layers. This can be used to create more complex + * animation effects. + * </p> + * + * @param duration the animation duration, in milliseconds + * @param callback the animation callback + * + * @see #layout() + * @see #layout(int, com.google.gwt.layout.client.Layout.AnimationCallback) + */ + public void layout(int duration, final Layout.AnimationCallback callback) { + layout.animate(duration, new Layout.AnimationCallback() { + public void onAnimationComplete() { + // Chain to the passed callback. + if (callback != null) { + callback.onAnimationComplete(); + } + } + + public void onLayout(Layer layer, double progress) { + // Inform the child associated with this layer that its size may have + // changed. + Widget child = (Widget) layer.getUserObject(); + if (child instanceof RequiresLayout) { + ((RequiresLayout) child).onLayout(); + } + + // Chain to the passed callback. + if (callback != null) { + callback.onLayout(layer, progress); + } + } + }); + } + + public void onLayout() { + for (Widget child : getChildren()) { + if (child instanceof RequiresLayout) { + ((RequiresLayout) child).onLayout(); + } + } + } + + /** + * Gets the {@link Layer} associated with the given widget. This layer may be + * used to manipulate the child widget's layout constraints. + * + * <p> + * After you have made changes to any of the child widgets' constraints, you + * must call one of the {@link HasAnimatedLayout} methods for those changes to + * be reflected visually. + * </p> + * + * @param child the child widget whose layer is to be retrieved + * @return the associated layer + */ + public Layout.Layer getLayer(Widget child) { + assert child.getParent() == this : "The requested widget is not a child of this panel"; + return (Layout.Layer) child.getLayoutData(); + } + + @Override + public boolean remove(Widget w) { + boolean removed = super.remove(w); + if (removed) { + layout.removeChild((Layer) w.getLayoutData()); + } + return removed; + } + + /** + * Gets the {@link Layout} instance associated with this widget. + * + * @return this widget's layout instance + */ + protected Layout getLayout() { + return layout; + } + + @Override + protected void onLoad() { + layout.attach(); + } + + @Override + protected void onUnload() { + layout.detach(); + } +}
diff --git a/user/src/com/google/gwt/user/client/ui/ProvidesLayout.java b/user/src/com/google/gwt/user/client/ui/ProvidesLayout.java new file mode 100644 index 0000000..f6e6d07 --- /dev/null +++ b/user/src/com/google/gwt/user/client/ui/ProvidesLayout.java
@@ -0,0 +1,40 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.user.client.ui; + +/** + * This tag interface specifies that the implementing widget will call + * {@link RequiresLayout#onLayout()} on its children whenever their size may + * have changed. + * + * <p> + * With limited exceptions (such as {@link RootLayoutPanel}), widgets that + * implement this interface will also implement {@link RequiresLayout}. A typical + * widget will implement {@link RequiresLayout#onLayout()} like this: + * + * <code> + * public void onLayout() { + * for (Widget child : getChildren()) { + * if (child instanceof RequiresLayout) { + * ((RequiresLayout) child).onLayout(); + * } + * } + * } + * </code> + * </p> + */ +public interface ProvidesLayout { +}
diff --git a/user/src/com/google/gwt/user/client/ui/RequiresLayout.java b/user/src/com/google/gwt/user/client/ui/RequiresLayout.java new file mode 100644 index 0000000..be81be6 --- /dev/null +++ b/user/src/com/google/gwt/user/client/ui/RequiresLayout.java
@@ -0,0 +1,34 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.user.client.ui; + +/** + * This interface designates that its implementor needs to be informed whenever + * its size is modified. + * + * <p> + * Widgets that implement this interface should only be added to those that + * implement {@link ProvidesLayout}. Failure to do so will usually result in + * {@link #onLayout()} not being called. + * </p> + */ +public interface RequiresLayout { + + /** + * Update the layout immediately. + */ + void onLayout(); +}
diff --git a/user/src/com/google/gwt/user/client/ui/RootLayoutPanel.java b/user/src/com/google/gwt/user/client/ui/RootLayoutPanel.java new file mode 100644 index 0000000..9c30378 --- /dev/null +++ b/user/src/com/google/gwt/user/client/ui/RootLayoutPanel.java
@@ -0,0 +1,87 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.user.client.ui; + +import com.google.gwt.event.logical.shared.ResizeEvent; +import com.google.gwt.event.logical.shared.ResizeHandler; +import com.google.gwt.user.client.Window; + +/** + * A singleton implementation of {@link LayoutPanel} that always attaches itself + * to the document body (i.e. {@link RootPanel#get()}). + * + * <p> + * This panel automatically calls {@link RequiresLayout#layout()} on itself when + * initially created, and whenever the window is resized. + * </p> + * + * <p> + * NOTE: This widget will <em>only</em> work in standards mode, which requires + * that the HTML page in which it is run have an explicit <!DOCTYPE> + * declaration. + * </p> + * + * <p> + * NOTE: This class is still very new, and its interface may change without + * warning. Use at your own risk. + * </p> + * + * <p> + * <h3>Example</h3> + * {@example com.google.gwt.examples.LayoutPanelExample} + * </p> + */ +public class RootLayoutPanel extends LayoutPanel implements ProvidesLayout { + + private static RootLayoutPanel singleton; + + /** + * Gets the singleton instance of RootLayoutPanel. This instance will always + * be attached to the document body via {@link RootPanel#get()}. + * + * <p> + * Note that, unlike {@link RootPanel#get(String)}, this class provides no way + * to get an instance for any element on the page other than the document + * body. This is because we know of no way to get resize events for anything + * but the window. + * </p> + */ + public static RootLayoutPanel get() { + if (singleton == null) { + singleton = new RootLayoutPanel(); + RootPanel.get().add(singleton); + } + return singleton; + } + + private RootLayoutPanel() { + Window.addResizeHandler(new ResizeHandler() { + public void onResize(ResizeEvent event) { + onLayout(); + } + }); + + // TODO: We need notification of font-size changes as well. + // I believe there's a hidden iframe trick that we can use to get + // a font-size-change event (really an em-definition-change event). + } + + @Override + protected void onLoad() { + getLayout().attach(); + getLayout().fillParent(); + } +}
diff --git a/user/src/com/google/gwt/user/client/ui/Widget.java b/user/src/com/google/gwt/user/client/ui/Widget.java index 8c998b3..dcc0a73 100644 --- a/user/src/com/google/gwt/user/client/ui/Widget.java +++ b/user/src/com/google/gwt/user/client/ui/Widget.java
@@ -54,6 +54,16 @@ } /** + * Gets the panel-defined layout data associated with this widget. + * + * @return the widget's layout data + * @see #setLayoutData + */ + public Object getLayoutData() { + return layoutData; + } + + /** * Gets this widget's parent panel. * * @return the widget's parent panel @@ -120,6 +130,18 @@ } /** + * Sets the panel-defined layout data associated with this widget. Only the + * panel that currently contains a widget should ever set this value. It + * serves as a place to store layout bookkeeping data associated with a + * widget. + * + * @param layoutData the widget's layout data + */ + public void setLayoutData(Object layoutData) { + this.layoutData = layoutData; + } + + /** * Overridden to defer the call to super.sinkEvents until the first time this * widget is attached to the dom, as a performance enhancement. Subclasses * wishing to customize sinkEvents can preserve this deferred sink behavior by @@ -325,16 +347,6 @@ return handlerManager; } - /** - * Gets the panel-defined layout data associated with this widget. - * - * @return the widget's layout data - * @see #setLayoutData - */ - Object getLayoutData() { - return layoutData; - } - @Override void replaceElement(com.google.gwt.dom.client.Element elem) { if (isAttached()) { @@ -355,18 +367,6 @@ } /** - * Sets the panel-defined layout data associated with this widget. Only the - * panel that currently contains a widget should ever set this value. It - * serves as a place to store layout bookkeeping data associated with a - * widget. - * - * @param layoutData the widget's layout data - */ - void setLayoutData(Object layoutData) { - this.layoutData = layoutData; - } - - /** * Sets this widget's parent. This method should only be called by * {@link Panel} and {@link Composite}. *
diff --git a/user/test/com/google/gwt/layout/LayoutTest.gwt.xml b/user/test/com/google/gwt/layout/LayoutTest.gwt.xml new file mode 100644 index 0000000..bae48b5 --- /dev/null +++ b/user/test/com/google/gwt/layout/LayoutTest.gwt.xml
@@ -0,0 +1,17 @@ +<!-- --> +<!-- Copyright 2009 Google Inc. --> +<!-- Licensed under the Apache License, Version 2.0 (the "License"); you --> +<!-- may not use this file except in compliance with the License. You may --> +<!-- may obtain a copy of the License at --> +<!-- --> +<!-- http://www.apache.org/licenses/LICENSE-2.0 --> +<!-- --> +<!-- Unless required by applicable law or agreed to in writing, software --> +<!-- distributed under the License is distributed on an "AS IS" BASIS, --> +<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or --> +<!-- implied. License for the specific language governing permissions and --> +<!-- limitations under the License. --> + +<module rename-to="LayoutTest"> + <inherits name="com.google.gwt.layout.Layout"/> +</module>
diff --git a/user/test/com/google/gwt/layout/client/LayoutTest.java b/user/test/com/google/gwt/layout/client/LayoutTest.java new file mode 100644 index 0000000..7611556 --- /dev/null +++ b/user/test/com/google/gwt/layout/client/LayoutTest.java
@@ -0,0 +1,551 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.layout.client; + +import static com.google.gwt.dom.client.Style.Unit.CM; +import static com.google.gwt.dom.client.Style.Unit.EM; +import static com.google.gwt.dom.client.Style.Unit.EX; +import static com.google.gwt.dom.client.Style.Unit.IN; +import static com.google.gwt.dom.client.Style.Unit.MM; +import static com.google.gwt.dom.client.Style.Unit.PC; +import static com.google.gwt.dom.client.Style.Unit.PCT; +import static com.google.gwt.dom.client.Style.Unit.PT; +import static com.google.gwt.dom.client.Style.Unit.PX; + +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.junit.client.GWTTestCase; +import com.google.gwt.layout.client.Layout.Layer; +import com.google.gwt.user.client.Window; + +/** + * Tests for the {@link Layout} class. + * + * TODO: Note that this test is *not* currently part of any suite. We can't run + * it reliably until we have a way to switch on "standards" mode in + * GWTTestCases. + */ +public class LayoutTest extends GWTTestCase { + + private static interface LayerInitializer { + void setupLayers(Layer l0, Layer l1); + } + + private DivElement parent, child0, child1; + private Element wrapper0, wrapper1; + private Layout layout; + private Layer layer0, layer1; + + @Override + public String getModuleName() { + return "com.google.gwt.layout.Layout"; + } + + /** + * Tests animation constraint- and unit-transitions. + */ + public void testAnimationTransitions_LTWH_LTRB_PX_CM() { + testAnimationTransitions_LTWH_LTRB(PX, CM); + } + + /** + * Tests animation constraint- and unit-transitions. + */ + public void testAnimationTransitions_LTWH_LTRB_PX_EM() { + testAnimationTransitions_LTWH_LTRB(PX, EM); + } + + /** + * Tests animation constraint- and unit-transitions. + */ + public void testAnimationTransitions_LTWH_LTRB_PX_EX() { + testAnimationTransitions_LTWH_LTRB(PX, EX); + } + + /** + * Tests animation constraint- and unit-transitions. + */ + public void testAnimationTransitions_LTWH_LTRB_PX_PCT() { + testAnimationTransitions_LTWH_LTRB(PX, PCT); + } + + /** + * Tests animation constraint- and unit-transitions. + */ + public void testAnimationTransitions_LTWH_RBWH_PX_CM() { + testAnimationTransitions_LTWH_RBWH(PX, CM); + } + + /** + * Tests animation constraint- and unit-transitions. + */ + public void testAnimationTransitions_LTWH_RBWH_PX_EM() { + testAnimationTransitions_LTWH_RBWH(PX, EM); + } + + /** + * Tests animation constraint- and unit-transitions. + */ + public void testAnimationTransitions_LTWH_RBWH_PX_EX() { + testAnimationTransitions_LTWH_RBWH(PX, EX); + } + + /** + * Tests animation constraint- and unit-transitions. + */ + public void testAnimationTransitions_LTWH_RBWH_PX_PCT() { + testAnimationTransitions_LTWH_RBWH(PX, PCT); + } + + /** + * Tests animation constraint- and unit-transitions. + */ + public void testAnimationTransitions_RBWH_LTRB_PX_CM() { + testAnimationTransitions_RBWH_LTRB(PX, CM); + } + + /** + * Tests animation constraint- and unit-transitions. + */ + public void testAnimationTransitions_RBWH_LTRB_PX_EM() { + testAnimationTransitions_RBWH_LTRB(PX, EM); + } + + /** + * Tests animation constraint- and unit-transitions. + */ + public void testAnimationTransitions_RBWH_LTRB_PX_EX() { + testAnimationTransitions_RBWH_LTRB(PX, EX); + } + + /** + * Tests animation constraint- and unit-transitions. + */ + public void testAnimationTransitions_RBWH_LTRB_PX_PCT() { + testAnimationTransitions_RBWH_LTRB(PX, PCT); + } + + /** + * Test that fillParent() works properly when the outer div is a child of + * another div, and that it correctly follows that div's size. + */ + public void testFillParent() { + // We don't use the default elements created in gwtSetUp() because we need + // to test the behavior when the layout is contained by an element other + // than the <body>. + Document doc = Document.get(); + DivElement container = doc.createDivElement(); + DivElement parent = doc.createDivElement(); + DivElement child = doc.createDivElement(); + child.setInnerHTML(" "); + doc.getBody().appendChild(container); + container.appendChild(parent); + + // The container has to be position:relative so that it serves as an offset + // parent. + container.getStyle().setPosition(Position.RELATIVE); + container.getStyle().setWidth(128, PX); + container.getStyle().setHeight(256, PX); + + Layout layout = new Layout(parent); + layout.attach(); + Layer layer = layout.attachChild(child); + layer.setTopBottom(0, PX, 0, PX); + layer.setLeftRight(0, PX, 0, PX); + + layout.fillParent(); + layout.layout(); + + // Test 128x256. + assertEquals(128, container.getOffsetWidth()); + assertEquals(256, container.getOffsetHeight()); + assertEquals(128, parent.getOffsetWidth()); + assertEquals(256, parent.getOffsetHeight()); + assertEquals(128, child.getOffsetWidth()); + assertEquals(256, child.getOffsetHeight()); + + // Expand to 256x256. The layout should react automatically. + container.getStyle().setWidth(256, PX); + container.getStyle().setHeight(128, PX); + assertEquals(256, container.getOffsetWidth()); + assertEquals(256, parent.getOffsetWidth()); + assertEquals(256, child.getOffsetWidth()); + + layout.detach(); + } + + /** + * Test that fillParent() works properly when the outer div is a child of the + * document body. + */ + public void testFillWindow() { + layer0.setTopBottom(0, PX, 0, PX); + layer0.setLeftRight(0, PX, 0, PX); + layout.layout(); + + int w = Window.getClientWidth(); + int h = Window.getClientHeight(); + assertEquals(w, parent.getOffsetWidth()); + assertEquals(h, parent.getOffsetHeight()); + assertEquals(w, child0.getOffsetWidth()); + assertEquals(h, child0.getOffsetHeight()); + } + + /** + * Tests that the layout reacts to font-size changes. + */ + public void testFontSizeChange() { + layer0.setLeftWidth(0, PX, 1, EM); + layer0.setTopHeight(0, PX, 1, EM); + layout.layout(); + + parent.getStyle().setFontSize(12, PT); + int cw = child0.getOffsetWidth(); + int ch = child0.getOffsetHeight(); + + parent.getStyle().setFontSize(24, PT); + int nw = child0.getOffsetWidth(); + int nh = child0.getOffsetHeight(); + assertTrue(nw > cw); + assertTrue(nh > ch); + + parent.getStyle().clearFontSize(); + } + + /** + * Ensures that two children laid out using various units in such a way that + * they should abut one another actually do so. + */ + public void testLayoutStructure() { + testHorizontalSplit(CM); + testHorizontalSplit(EM); + testHorizontalSplit(EX); + testHorizontalSplit(IN); + testHorizontalSplit(MM); + testHorizontalSplit(PC); + testHorizontalSplit(PT); + testHorizontalSplit(PX); + } + + /** + * Tests (left-right, left-width, right-width) x (top-bottom, top-height, + * bottom-height). Ok, so we don't test the *entire* cross-product, but enough + * to be comfortable. + */ + public void testStaticConstraints() { + // left-right, top-bottom + layer0.setTopBottom(32, PX, 32, PX); + layer0.setLeftRight(32, PX, 32, PX); + layout.layout(); + + int w = parent.getClientWidth(); + int h = parent.getClientHeight(); + assertEquals(32, wrapper0.getOffsetLeft()); + assertEquals(32, wrapper0.getOffsetTop()); + assertEquals(w - 64, wrapper0.getOffsetWidth()); + assertEquals(h - 64, wrapper0.getOffsetHeight()); + + // left-width, top-height + layer0.setTopHeight(16, PX, 128, PX); + layer0.setLeftWidth(16, PX, 128, PX); + layout.layout(); + + assertEquals(16, wrapper0.getOffsetLeft()); + assertEquals(16, wrapper0.getOffsetTop()); + assertEquals(128, wrapper0.getOffsetWidth()); + assertEquals(128, wrapper0.getOffsetHeight()); + + // right-width, bottom-height + layer0.setBottomHeight(16, PX, 128, PX); + layer0.setRightWidth(16, PX, 128, PX); + layout.layout(); + + assertEquals(w - (16 + 128), wrapper0.getOffsetLeft()); + assertEquals(h - (16 + 128), wrapper0.getOffsetTop()); + assertEquals(128, wrapper0.getOffsetWidth()); + assertEquals(128, wrapper0.getOffsetHeight()); + } + + /** + * Tests all unit types. + */ + public void testUnits() { + // CM + layer0.setTopBottom(1, CM, 1, CM); + layer0.setLeftRight(1, CM, 1, CM); + layout.layout(); + assertLeftRightTopBottomUnitsMakeSense(wrapper0); + + // MM + layer0.setTopBottom(1, MM, 1, MM); + layer0.setLeftRight(1, MM, 1, MM); + layout.layout(); + assertLeftRightTopBottomUnitsMakeSense(wrapper0); + + // IN + layer0.setTopBottom(1, IN, 1, IN); + layer0.setLeftRight(1, IN, 1, IN); + layout.layout(); + assertLeftRightTopBottomUnitsMakeSense(wrapper0); + + // EM + layer0.setTopBottom(1, EM, 1, EM); + layer0.setLeftRight(1, EM, 1, EM); + layout.layout(); + assertLeftRightTopBottomUnitsMakeSense(wrapper0); + + // EX + layer0.setTopBottom(1, EX, 1, EX); + layer0.setLeftRight(1, EX, 1, EX); + layout.layout(); + assertLeftRightTopBottomUnitsMakeSense(wrapper0); + + // PC + layer0.setTopBottom(1, PC, 1, PC); + layer0.setLeftRight(1, PC, 1, PC); + layout.layout(); + assertLeftRightTopBottomUnitsMakeSense(wrapper0); + + // PT + layer0.setTopBottom(10, PT, 10, PT); + layer0.setLeftRight(10, PT, 10, PT); + layout.layout(); + assertLeftRightTopBottomUnitsMakeSense(wrapper0); + + // PCT + layer0.setTopBottom(10, PCT, 10, PCT); + layer0.setLeftRight(10, PCT, 10, PCT); + layout.layout(); + assertLeftRightTopBottomUnitsMakeSense(wrapper0); + } + + /** + * Tests layout in the presence of decorations on the parent and child + * elements. + */ + public void testWithDecorations() { + layer0.setTopBottom(0, PX, 0, PX); + layer0.setLeftRight(0, PX, 0, PX); + layout.layout(); + + // Give each of the parent and child 1px margin and 1px border. + parent.getStyle().setMargin(1, PX); + parent.getStyle().setProperty("border", "1px solid black"); + + child0.getStyle().setMargin(1, PX); + child0.getStyle().setProperty("border", "1px solid black"); + layout.layout(); + + int w = Window.getClientWidth(); + int h = Window.getClientHeight(); + int pw = parent.getOffsetWidth(); + int ph = parent.getOffsetHeight(); + + // The parent's offsetSize should be 2px smaller than the window's client + // area, because of the margin (the border is *included* in the offsetSize). + assertEquals(w - 2, pw); + assertEquals(h - 2, ph); + + // The child's offsetSize (actually that of its wrapper element), should be + // 2px smaller than the parent, for precisely the same reason. + assertEquals(pw - 2, wrapper0.getOffsetWidth()); + assertEquals(ph - 2, wrapper0.getOffsetHeight()); + } + + @Override + protected void gwtSetUp() throws Exception { + Window.enableScrolling(false); + + Document doc = Document.get(); + parent = doc.createDivElement(); + child0 = doc.createDivElement(); + child1 = doc.createDivElement(); + doc.getBody().appendChild(parent); + + layout = new Layout(parent); + layout.attach(); + layout.fillParent(); + + layer0 = layout.attachChild(child0); + layer1 = layout.attachChild(child1); + + wrapper0 = child0.getParentElement(); + wrapper1 = child1.getParentElement(); + } + + @Override + protected void gwtTearDown() throws Exception { + Window.enableScrolling(true); + Document.get().getBody().removeChild(parent); + layout.detach(); + } + + private void assertLeftRightTopBottomUnitsMakeSense(Element elem) { + // Assume that the element has been laid out to (l, t, r, b) = (1u, 1u, 1u, + // 1u). Assert that the element's clientLeft/Top are non-zero, and that the + // clientLeft/Top/Width/Height are consistent with the parent's size. + int w = parent.getClientWidth(); + int h = parent.getClientHeight(); + int cl = elem.getOffsetLeft(); + int ct = elem.getOffsetTop(); + int cw = elem.getOffsetWidth(); + int ch = elem.getOffsetHeight(); + + // Assert that the left-top unit came out at least non-zero size. + assertTrue(cl > 0); + assertTrue(ct > 0); + + // Assert that the right-bottom also came out non-zero. We should be able + // to assert that it came out the same size as the top-left, but it turns + // out that this isn't quite reliable because of rounding errors. + assertTrue(w - (cl + cw) > 0); + assertTrue(h - (ct + ch) > 0); + } + + // This method may only be called once per test, as it uses delayTestFinish() + // internally. + private void testAnimationTransitions_LTWH_LTRB(final Unit unit0, + final Unit unit1) { + testAnimationTransitionsHelper(new LayerInitializer() { + public void setupLayers(Layer l0, Layer l1) { + l0.setLeftWidth(0, unit0, 10, unit0); + l0.setTopHeight(0, unit0, 10, unit0); + l1.setLeftRight(1, unit1, 1, unit1); + l1.setTopBottom(1, unit1, 1, unit1); + } + }, new LayerInitializer() { + public void setupLayers(Layer l0, Layer l1) { + l1.setLeftWidth(0, unit0, 10, unit0); + l1.setTopHeight(0, unit0, 10, unit0); + l0.setLeftRight(1, unit1, 1, unit1); + l0.setTopBottom(1, unit1, 1, unit1); + } + }); + } + + // This method may only be called once per test, as it uses delayTestFinish() + // internally. + private void testAnimationTransitions_LTWH_RBWH(final Unit unit0, + final Unit unit1) { + testAnimationTransitionsHelper(new LayerInitializer() { + public void setupLayers(Layer l0, Layer l1) { + l0.setLeftWidth(0, unit0, 10, unit0); + l0.setTopHeight(0, unit0, 10, unit0); + l1.setRightWidth(0, unit1, 10, unit1); + l1.setBottomHeight(0, unit1, 10, unit1); + } + }, new LayerInitializer() { + public void setupLayers(Layer l0, Layer l1) { + l1.setLeftWidth(0, unit0, 10, unit0); + l1.setTopHeight(0, unit0, 10, unit0); + l0.setRightWidth(0, unit1, 10, unit1); + l0.setBottomHeight(0, unit1, 10, unit1); + } + }); + } + + // This method may only be called once per test, as it uses delayTestFinish() + // internally. + private void testAnimationTransitions_RBWH_LTRB(final Unit unit0, + final Unit unit1) { + testAnimationTransitionsHelper(new LayerInitializer() { + public void setupLayers(Layer l0, Layer l1) { + l0.setRightWidth(0, unit0, 10, unit0); + l0.setBottomHeight(0, unit0, 10, unit0); + l1.setLeftRight(1, unit1, 1, unit1); + l1.setTopBottom(1, unit1, 1, unit1); + } + }, new LayerInitializer() { + public void setupLayers(Layer l0, Layer l1) { + l1.setRightWidth(0, unit0, 10, unit0); + l1.setBottomHeight(0, unit0, 10, unit0); + l0.setLeftRight(1, unit1, 1, unit1); + l0.setTopBottom(1, unit1, 1, unit1); + } + }); + } + + // This method may only be called once per test, as it uses delayTestFinish() + // internally. + private void testAnimationTransitionsHelper(LayerInitializer before, + LayerInitializer after) { + before.setupLayers(layer0, layer1); + layout.layout(); + + final int l0 = wrapper0.getOffsetLeft(); + final int t0 = wrapper0.getOffsetTop(); + final int w0 = wrapper0.getOffsetWidth(); + final int h0 = wrapper0.getOffsetHeight(); + + final int l1 = wrapper1.getOffsetLeft(); + final int t1 = wrapper1.getOffsetTop(); + final int w1 = wrapper1.getOffsetWidth(); + final int h1 = wrapper1.getOffsetHeight(); + + after.setupLayers(layer0, layer1); + delayTestFinish(200); + layout.animate(100, new Layout.AnimationCallback() { + public void onAnimationComplete() { + // Assert that the two layers have swapped positions. + assertEquals(l0, wrapper1.getOffsetLeft()); + assertEquals(t0, wrapper1.getOffsetTop()); + assertEquals(w0, wrapper1.getOffsetWidth()); + assertEquals(h0, wrapper1.getOffsetHeight()); + + assertEquals(l1, wrapper0.getOffsetLeft()); + assertEquals(t1, wrapper0.getOffsetTop()); + assertEquals(w1, wrapper0.getOffsetWidth()); + assertEquals(h1, wrapper0.getOffsetHeight()); + + finishTest(); + } + + public void onLayout(Layer layer, double progress) { + } + }); + } + + private void testHorizontalSplit(Unit unit) { + // Line them up horizontally, split at 5 units. + layer0.setTopBottom(0, PX, 0, PX); + layer0.setLeftWidth(0, PX, 5, unit); + + layer1.setTopBottom(0, PX, 0, PX); + layer1.setLeftRight(5, unit, 0, PX); + layout.layout(); + + int child0Right = wrapper0.getOffsetWidth(); + int child1Left = wrapper1.getOffsetLeft(); + assertEquals(child0Right, child1Left); + } + + private void testVerticalSplit(Unit unit) { + // Line them up vertically, split at 5em. + layer0.setTopHeight(0, PX, 5, unit); + layer0.setLeftRight(0, PX, 0, PX); + + layer1.setTopBottom(5, unit, 0, PX); + layer1.setLeftRight(0, PX, 0, PX); + layout.layout(); + + int child0Bottom = wrapper0.getOffsetHeight(); + int child1Top = wrapper1.getOffsetTop(); + assertEquals(child0Bottom, child1Top); + } +}