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);
+ }
+}