This change provides two new widgets HorizontalSplitPanel and
VerticalSplitPanel.  These panels arrange two widgets in a single vertical
column or horizontal row and allow the user to interactively change the
proportion of the area dedicated to each of the two widgets. 

Review by: jgw



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@834 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/doc/src/HorizontalSplitPanel.png b/doc/src/HorizontalSplitPanel.png
new file mode 100644
index 0000000..8e74d97
--- /dev/null
+++ b/doc/src/HorizontalSplitPanel.png
Binary files differ
diff --git a/doc/src/VerticalSplitPanel.png b/doc/src/VerticalSplitPanel.png
new file mode 100644
index 0000000..cf4697c
--- /dev/null
+++ b/doc/src/VerticalSplitPanel.png
Binary files differ
diff --git a/user/src/com/google/gwt/user/SplitPanel.gwt.xml b/user/src/com/google/gwt/user/SplitPanel.gwt.xml
new file mode 100644
index 0000000..d30648a
--- /dev/null
+++ b/user/src/com/google/gwt/user/SplitPanel.gwt.xml
@@ -0,0 +1,32 @@
+<!--
+  Copyright 2007 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.
+-->
+<module>
+	<inherits name="com.google.gwt.user.User"/>
+	
+	<replace-with class="com.google.gwt.user.client.ui.HorizontalSplitPanel.Impl">
+		<when-type-is class="com.google.gwt.user.client.ui.HorizontalSplitPanel.Impl"/>
+	</replace-with>
+
+	<replace-with class="com.google.gwt.user.client.ui.HorizontalSplitPanel.ImplIE6">
+		<when-type-is class="com.google.gwt.user.client.ui.HorizontalSplitPanel.Impl"/>
+		<when-property-is name="user.agent" value="ie6"/>
+	</replace-with>
+
+	<replace-with class="com.google.gwt.user.client.ui.HorizontalSplitPanel.ImplSafari">
+		<when-type-is class="com.google.gwt.user.client.ui.HorizontalSplitPanel.Impl"/>
+		<when-property-is name="user.agent" value="safari"/>
+	</replace-with>
+</module>
\ No newline at end of file
diff --git a/user/src/com/google/gwt/user/User.gwt.xml b/user/src/com/google/gwt/user/User.gwt.xml
index af67fc6..3a84ffd 100644
--- a/user/src/com/google/gwt/user/User.gwt.xml
+++ b/user/src/com/google/gwt/user/User.gwt.xml
@@ -1,6 +1,19 @@
-<!--                                                                        -->
-<!-- Copyright 2007 Google Inc. All Rights Reserved.                        -->
-<!--                                                                        -->
+<!--
+  Copyright 2007 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.
+-->
+
 <!-- Combines all user facilities into a single module for convenience.     -->
 <!-- Most new code should inherit this module.                              -->
 <!--                                                                        -->
@@ -16,4 +29,5 @@
    <inherits name="com.google.gwt.user.Focus"/>
    <inherits name="com.google.gwt.user.ImageBundle"/>  
    <inherits name="com.google.gwt.user.ClippedImage"/>
+   <inherits name="com.google.gwt.user.SplitPanel"/>
 </module>
diff --git a/user/src/com/google/gwt/user/client/ui/HorizontalSplitPanel.java b/user/src/com/google/gwt/user/client/ui/HorizontalSplitPanel.java
new file mode 100644
index 0000000..6409eea
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/HorizontalSplitPanel.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2007 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.core.client.GWT;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+
+/**
+ * A panel that arranges two widgets in a single horizontal row and allows the
+ * user to interactively change the proportion of the width dedicated to each of
+ * the two widgets. Widgets contained within a <code>HorizontalSplitPanel</code>
+ * will be automatically decorated with scrollbars when necessary.
+ * 
+ * <p>
+ * <img class='gallery' src='HorizontalSplitPanel.png'/>
+ * </p>
+ * 
+ * <h3>CSS Style Rules</h3>
+ * <ul class='css'>
+ * <li>.gwt-HorizontalSplitPanel { the panel itself }</li>
+ * <li>.gwt-HorizontalSplitPanel left { the left container }</li>
+ * <li>.gwt-HorizontalSplitPanel right { the right container }</li>
+ * <li>.gwt-HorizontalSplitPanel splitter { the splitter }</li>
+ * </ul>
+ */
+public final class HorizontalSplitPanel extends SplitPanel {
+
+  /**
+   * The resizing implementation for standard browsers (Anything other than
+   * Safari and IE6/7)
+   */
+  private static class Impl {
+    // The x position of the mouse when drag resizing begins.
+    protected int initialThumbPos = 0;
+
+    // Widths of elements which are needed to do relative drag resizing.
+    protected int initialLeftWidth = 0;
+    protected int initialLeftContentWidth = 0;
+    protected int initialRightContentWidth = 0;
+
+    /**
+     * Initializes css properties on the panels DOM structure.
+     * 
+     * @param panel the panel
+     */
+    protected void init(HorizontalSplitPanel panel) {
+    }
+
+    /**
+     * Called on each mouse move event during drag resizing.
+     * 
+     * @param panel the panel
+     * @param pos the current horizontal mouse position relative to the panel
+     */
+    protected void onSplitResize(final HorizontalSplitPanel panel, int pos) {
+      // Compute the distance the splitter must be moved.
+      int offset = pos - initialThumbPos;
+
+      /*
+       * Compute the projected size of the content areas. This is to prevent
+       * out-of-bounds scrolling.
+       */
+      int newLeftContentWidth = initialLeftContentWidth + offset;
+      int newRightContentWidth = initialRightContentWidth - offset;
+
+      if (newLeftContentWidth < 0) {
+        offset -= newLeftContentWidth;
+      }
+
+      if (newRightContentWidth < 0) {
+        offset += newRightContentWidth;
+      }
+
+      // Move the split position by the offset.
+      setSplitPosition(panel, (initialLeftWidth + offset) + "px");
+    }
+
+    /**
+     * Called whenever drag resizing begins.
+     * 
+     * @param panel the panel
+     * @param pos the current horizontal mouse position relative to the panel
+     */
+    protected void onSplitResizeStarted(final HorizontalSplitPanel panel,
+        int pos) {
+      initialThumbPos = pos;
+      initialLeftWidth = getOffsetWidth(panel.leftDiv);
+      initialLeftContentWidth = getClientWidth(panel.getElement(LEFT));
+      initialRightContentWidth = getClientWidth(panel.getElement(RIGHT));
+    }
+
+    /**
+     * Sets the horizontal position of the splitter.
+     * 
+     * @param panel the panel
+     * @param pos the position as a css length
+     */
+    protected void setSplitPosition(final HorizontalSplitPanel panel,
+        final String pos) {
+      /*
+       * This default impl adjusts the width of the first level div and depends
+       * on the outer table to adjust its cell widths appropriately.
+       */
+      setWidth(panel.leftDiv, pos);
+    }
+  }
+
+  /**
+   * The resizing implementation for IE6/7.
+   */
+  private static class ImplIE6 extends Impl {
+    protected void init(final HorizontalSplitPanel panel) {
+      /*
+       * Without fixed table layout, IE will not respected the table width
+       * constraints.
+       */
+      DOM.setStyleAttribute(panel.table, "tableLayout", "fixed");
+
+      /*
+       * Since the DOM structure will not have finished full layout, we must
+       * defer until later to ensure that the inner divs update to the proper
+       * size.
+       */
+      DeferredCommand.addCommand(new Command() {
+        public void execute() {
+          updateDivWidth(panel);
+        }
+      });
+    }
+
+    protected void onSplitResizeStarted(final HorizontalSplitPanel panel,
+        final int x) {
+      initialThumbPos = x;
+      initialLeftWidth = getOffsetWidth(panel.leftTD);
+      initialLeftContentWidth = getClientWidth(panel.getElement(LEFT));
+      initialRightContentWidth = getClientWidth(panel.getElement(RIGHT));
+    }
+
+    protected void setSplitPosition(final HorizontalSplitPanel panel,
+        final String pos) {
+      final Element leftTD = panel.leftTD;
+      // adjust the width of the table cell instead of the inner div.
+      setWidth(leftTD, pos);
+      updateDivWidth(panel);
+    }
+
+    /*
+     * IE6 will not properly auto size the inner divs unless we explicitly set
+     * their width to something that forces a layout. NOTE: 100% works for
+     * quirks mode but will be problematic for standards mode when there are
+     * margins, border or padding.
+     */
+    private void updateDivWidth(final HorizontalSplitPanel panel) {
+      final String size = "100%";
+      setWidth(panel.leftDiv, size);
+      setWidth(panel.rightDiv, size);
+      setWidth(panel.getElement(LEFT), size);
+      setWidth(panel.getElement(RIGHT), size);
+    }
+  }
+
+  /**
+   * The resizing implemenation for Safari/WebKit.
+   */
+  private static class ImplSafari extends Impl {
+
+    protected void init(final HorizontalSplitPanel panel) {
+      /*
+       * Without fixed table layout, Safari will not respect the css width on
+       * the table.
+       */
+      DOM.setStyleAttribute(panel.table, "tableLayout", "fixed");
+
+      final String autoProp = "auto";
+      setWidth(panel.leftDiv, autoProp);
+      setWidth(panel.rightDiv, autoProp);
+      setWidth(panel.getElement(LEFT), autoProp);
+      setWidth(panel.getElement(RIGHT), autoProp);
+
+      /*
+       * Safari bug: a width must be set on the table when it is added to the
+       * DOM or else it cannot be set later.
+       */
+      panel.setWidth("100%");
+    }
+
+    protected void setSplitPosition(final HorizontalSplitPanel panel, String pos) {
+      // Adjust the width of the table cell instead of the inner div.
+      setWidth(panel.leftTD, pos);
+    }
+  }
+
+  /**
+   * Constants to provide more readable calls to {@link #getElement()} and
+   * {@link #getWidget(int)}.
+   */
+  private static final int LEFT = 0;
+  private static final int RIGHT = 1;
+
+  private static final int DEFAULT_SPLITTER_WIDTH = 10;
+
+  private static final int getClientWidth(final Element elem) {
+    return DOM.getIntAttribute(elem, "clientWidth");
+  }
+
+  private static final int getOffsetWidth(final Element elem) {
+    return DOM.getIntAttribute(elem, "offsetWidth");
+  }
+
+  private static final void setWidth(Element elem, String size) {
+    DOM.setStyleAttribute(elem, "width", size);
+  }
+
+  /**
+   * DOM elements needed to support splitter dragging. The underlying DOM
+   * structure is:
+   * 
+   * <pre>
+   *   table
+   *     td (leftTD)
+   *       div (leftDiv)
+   *         div (getElement(LEFT))
+   *     td (splitter)
+   *     td (rightTD)
+   *       div (rightDiv)
+   *         div (getElement(RIGHT))
+   * </pre>
+   */
+  private final Element table;
+  private final Element leftTD, rightTD;
+  private final Element leftDiv, rightDiv;
+
+  private final Impl impl = (Impl) GWT.create(Impl.class);
+
+  /**
+   * Creates an empty horizontal split panel.
+   */
+  public HorizontalSplitPanel() {
+    super(DOM.createTable(), DOM.createTD(), DOM.createDiv(), DOM.createDiv());
+
+    table = getElement();
+    leftDiv = preventElementBoxStyles(DOM.createDiv());
+    rightDiv = preventElementBoxStyles(DOM.createDiv());
+    leftTD = preventElementBoxStyles(DOM.createTD());
+    rightTD = preventElementBoxStyles(DOM.createTD());
+
+    buildDOM();
+
+    setStyleName("gwt-HorizontalSplitPanel");
+
+    impl.init(this);
+  }
+
+  /**
+   * Gets the widget in the left side of the panel.
+   * 
+   * @return the widget, <code>null</code> if there is not one.
+   */
+  public final Widget getLeftWidget() {
+    return getWidget(LEFT);
+  }
+
+  /**
+   * Gets the widget in the right side of the panel.
+   * 
+   * @return the widget, <code>null</code> if there is not one.
+   */
+  public final Widget getRightWidget() {
+    return getWidget(RIGHT);
+  }
+
+  public final void setHeight(String height) {
+    DOM.setStyleAttribute(getElement(LEFT), "height", height);
+    DOM.setStyleAttribute(getElement(RIGHT), "height", height);
+  }
+
+  /**
+   * Sets the widget in the left side of the panel.
+   * 
+   * @param w the widget
+   */
+  public final void setLeftWidget(Widget w) {
+    setWidget(LEFT, w);
+  }
+
+  /**
+   * Sets the widget in the right side of the panel.
+   * 
+   * @param w the widget
+   */
+  public final void setRightWidget(Widget w) {
+    setWidget(RIGHT, w);
+  }
+
+  public final void setSplitPosition(String pos) {
+    impl.setSplitPosition(this, pos);
+  }
+
+  final void onSplitterResize(int x, int y) {
+    impl.onSplitResize(this, x);
+  }
+
+  final void onSplitterResizeStarted(int x, int y) {
+    impl.onSplitResizeStarted(this, x);
+  }
+
+  private void buildDOM() {
+    final Element leftContentDiv = getElement(LEFT);
+    final Element rightContentDiv = getElement(RIGHT);
+
+    final Element tbody = DOM.createTBody();
+    final Element tr = DOM.createTR();
+    final Element splitTD = getSplitElement();
+
+    DOM.appendChild(table, tbody);
+    DOM.appendChild(tbody, tr);
+    DOM.appendChild(tr, leftTD);
+    DOM.appendChild(tr, splitTD);
+    DOM.appendChild(tr, rightTD);
+    DOM.appendChild(leftTD, leftDiv);
+    DOM.appendChild(rightTD, rightDiv);
+    DOM.appendChild(leftDiv, leftContentDiv);
+    DOM.appendChild(rightDiv, rightContentDiv);
+
+    DOM.setInnerHTML(splitTD, "&nbsp;");
+
+    DOM.setAttribute(table, "cellSpacing", "0");
+    DOM.setAttribute(table, "cellPadding", "0");
+
+    addElementScrolling(leftContentDiv);
+    addElementScrolling(rightContentDiv);
+
+    setElementClassname(leftContentDiv, "left");
+    setElementClassname(splitTD, "splitter");
+    setElementClassname(rightContentDiv, "right");
+
+    DOM.setStyleAttribute(leftTD, "verticalAlign", "top");
+    DOM.setStyleAttribute(rightTD, "verticalAlign", "top");
+
+    /*
+     * Ensures that the splitter is of reasonable width when no CSS is active on
+     * it, but this value is immediately overridden by CSS values.
+     */
+    DOM.setIntAttribute(splitTD, "width", DEFAULT_SPLITTER_WIDTH);
+  }
+}
diff --git a/user/src/com/google/gwt/user/client/ui/SplitPanel.java b/user/src/com/google/gwt/user/client/ui/SplitPanel.java
new file mode 100644
index 0000000..ce487e5
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/SplitPanel.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2007 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.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+
+import java.util.Iterator;
+
+/**
+ * Abstract base class for {@link HorizontalSplitPanel} and
+ * {@link VerticalSplitPanel}.
+ */
+abstract class SplitPanel extends Panel {
+
+  /**
+   * Adds clipping to an element.
+   * 
+   * @param elem the element
+   */
+  static final void addElementClipping(final Element elem) {
+    DOM.setStyleAttribute(elem, "overflow", "hidden");
+  }
+
+  /**
+   * Adds as-needed scrolling to an element.
+   * 
+   * @param elem the element
+   */
+  static final void addElementScrolling(final Element elem) {
+    DOM.setStyleAttribute(elem, "overflow", "auto");
+  }
+
+  /**
+   * Adds zero or none css values for padding, margin and border to prevent
+   * stylesheet overrides. Returns the element for convienence to support
+   * builder pattern.
+   * 
+   * @param elem the element
+   * @return the element
+   */
+  static final Element preventElementBoxStyles(final Element elem) {
+    DOM.setIntStyleAttribute(elem, "padding", 0);
+    DOM.setIntStyleAttribute(elem, "margin", 0);
+    DOM.setStyleAttribute(elem, "border", "none");
+    return elem;
+  }
+
+  /**
+   * Adds zero size padding to an element.
+   * 
+   * @param elem the element.
+   */
+  static final void preventElementPadding(final Element elem) {
+    DOM.setStyleAttribute(elem, "padding", "0");
+  }
+
+  /**
+   * Sets the elements css class name.
+   * 
+   * @param elem the element
+   * @param className the class name
+   */
+  static final void setElementClassname(final Element elem,
+      final String className) {
+    DOM.setAttribute(elem, "className", className);
+  }
+
+  // The enclosed widgets.
+  private final Widget[] widgets = new Widget[2];
+
+  // The elements containing the widgets.
+  private final Element[] elements = new Element[2];
+
+  // The element that acts as the splitter.
+  private final Element splitElem;
+
+  // Indicates whether drag resizing is active.
+  private boolean isResizing = false;
+
+  /**
+   * Initializes the split panel.
+   * 
+   * @param mainElem the root element for the split panel
+   * @param splitElem the element that acts as the splitter
+   * @param headElem the element to contain the top or left most widget
+   * @param tailElem the element to contain the bottom or right most widget
+   */
+  SplitPanel(Element mainElem, Element splitElem, Element headElem,
+      Element tailElem) {
+    setElement(mainElem);
+    this.splitElem = splitElem;
+    elements[0] = headElem;
+    elements[1] = tailElem;
+    sinkEvents(Event.MOUSEEVENTS);
+  }
+
+  public void add(Widget w) {
+    if (getWidget(0) == null) {
+      setWidget(0, w);
+    } else if (getWidget(1) == null) {
+      setWidget(1, w);
+    } else {
+      throw new IllegalStateException(
+          "A Splitter can only contain two Widgets.");
+    }
+  }
+
+  /**
+   * Indicates whether the split panel is being resized.
+   * 
+   * @return <code>true</code> if the user is dragging the splitter,
+   *         <code>false</code> otherwise
+   */
+  public boolean isResizing() {
+    return isResizing;
+  }
+
+  public Iterator iterator() {
+    return WidgetIterators.createWidgetIterator(this, widgets);
+  }
+
+  public void onBrowserEvent(Event event) {
+    switch (DOM.eventGetType(event)) {
+
+      case Event.ONMOUSEDOWN: {
+        Element target = DOM.eventGetTarget(event);
+        if (DOM.isOrHasChild(splitElem, target)) {
+          startResizingFrom(DOM.eventGetClientX(event) - getAbsoluteLeft(),
+              DOM.eventGetClientY(event) - getAbsoluteTop());
+          DOM.eventPreventDefault(event);
+        }
+        break;
+      }
+
+      case Event.ONMOUSEUP: {
+        stopResizing();
+        break;
+      }
+
+      case Event.ONMOUSEMOVE: {
+        if (isResizing()) {
+          onSplitterResize(DOM.eventGetClientX(event) - getAbsoluteLeft(),
+              DOM.eventGetClientY(event) - getAbsoluteTop());
+          DOM.eventPreventDefault(event);
+        }
+        break;
+      }
+    }
+  }
+
+  public boolean remove(Widget widget) {
+    if (widget == null) {
+      throw new IllegalArgumentException("Widget must not be null");
+    }
+
+    if (widgets[0] == widget) {
+      setWidget(0, null);
+      return true;
+    } else if (widgets[1] == widget) {
+      setWidget(1, null);
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Moves the position of the splitter.
+   * 
+   * @param size the new size of the left region in CSS units (e.g. "10px",
+   *          "1em")
+   */
+  public abstract void setSplitPosition(String size);
+
+  /**
+   * Gets the content element for the given index.
+   * 
+   * @param index the index of the element, only 0 and 1 are valid.
+   * @return the element
+   */
+  protected Element getElement(int index) {
+    return elements[index];
+  }
+
+  /**
+   * Gets the element that is acting as the splitter.
+   * 
+   * @return the element
+   */
+  protected Element getSplitElement() {
+    return splitElem;
+  }
+
+  /**
+   * Gets one of the contained widgets.
+   * 
+   * @param index the index of the widget, only 0 and 1 are valid.
+   * @return the widget
+   */
+  protected Widget getWidget(int index) {
+    return widgets[index];
+  }
+
+  /**
+   * Sets one of the contained widgets.
+   * 
+   * @param index the index, only 0 and 1 are valid
+   * @param w the widget
+   */
+  protected final void setWidget(int index, Widget w) {
+    if (widgets[index] != null) {
+      disown(widgets[index]);
+    }
+
+    widgets[index] = w;
+
+    if (w != null) {
+      adopt(w, elements[index]);
+    }
+  }
+
+  /**
+   * Called on each mouse drag event as the user is dragging the splitter.
+   * 
+   * @param x the x coordinate of the mouse relative to the panel's extent
+   * @param y the y coordinate of the mosue relative to the panel's extent
+   */
+  abstract void onSplitterResize(int x, int y);
+
+  /**
+   * Called when the user starts dragging the splitter.
+   * 
+   * @param x the x coordinate of the mouse relative to the panel's extent
+   * @param y the y coordinate of the mouse relative to the panel's extent
+   */
+  abstract void onSplitterResizeStarted(int x, int y);
+
+  private void startResizingFrom(int x, int y) {
+    isResizing = true;
+    onSplitterResizeStarted(x, y);
+  }
+
+  private void stopResizing() {
+    isResizing = false;
+  }
+}
diff --git a/user/src/com/google/gwt/user/client/ui/VerticalSplitPanel.java b/user/src/com/google/gwt/user/client/ui/VerticalSplitPanel.java
new file mode 100644
index 0000000..b75e7d7
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/ui/VerticalSplitPanel.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2007 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.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+
+/**
+ * A panel that arranges two widgets in a single vertical column and allows the
+ * user to interactively change the proportion of the height dedicated to each
+ * of the two widgets. Widgets contained within a
+ * <code>VerticalSplitterPanel</code> will be automatically decorated with
+ * scrollbars when neccessary.
+ * 
+ * <p>
+ * <img class='gallery' src='VerticalSplitPanel.png'/>
+ * </p>
+ * 
+ * <h3>CSS Style Rules</h3>
+ * <ul class='css'>
+ * <li>.gwt-VerticalSplitPanel { the panel itself }</li>
+ * <li>.gwt-VerticalSplitPanel top { the top container }</li>
+ * <li>.gwt-VerticalSplitPanel bottom { the bottom container }</li>
+ * <li>.gwt-VerticalSplitPanel splitter { the splitter }</li>
+ * </ul>
+ */
+public final class VerticalSplitPanel extends SplitPanel {
+
+  private static final int TOP = 0;
+  private static final int BOTTOM = 1;
+
+  private static int getClientHeight(Element elem) {
+    return DOM.getIntAttribute(elem, "clientHeight");
+  }
+
+  private static int getOffsetTop(Element elem) {
+    return DOM.getIntAttribute(elem, "offsetTop");
+  }
+
+  private static Element lockStyles(final Element elem) {
+    DOM.setIntStyleAttribute(elem, "height", 0);
+    return preventElementBoxStyles(elem);
+  }
+
+  private static void setHeight(Element elem, int px) {
+    DOM.setStyleAttribute(elem, "height", Math.max(0, px) + "px");
+  }
+
+  // Element is added below bottom container element to make it possible to
+  // infer the bottom element's height.
+  private final Element probeElem;
+
+  // Captures the height of the top container when drag resizing starts.
+  private int initialTopHeight = 0;
+
+  // Captures the offset of a user's mouse pointer during drag resizing.
+  private int initialThumbPos = 0;
+
+  /**
+   * Creates an empty vertical split panel.
+   */
+  public VerticalSplitPanel() {
+    super(DOM.createDiv(), DOM.createDiv(), DOM.createDiv(), DOM.createDiv());
+
+    final Element thisElem = getElement();
+    final Element splitElem = getSplitElement();
+    final Element topElem = getElement(TOP);
+    final Element bottomElem = getElement(BOTTOM);
+    probeElem = lockStyles(DOM.createDiv());
+
+    DOM.appendChild(thisElem, topElem);
+    DOM.appendChild(thisElem, splitElem);
+    DOM.appendChild(thisElem, bottomElem);
+    DOM.appendChild(thisElem, probeElem);
+
+    addElementClipping(thisElem);
+    addElementScrolling(topElem);
+    addElementScrolling(bottomElem);
+
+    // Prevent padding on container elements.
+    preventElementPadding(thisElem);
+    preventElementPadding(topElem);
+    preventElementPadding(bottomElem);
+
+    setElementClassname(topElem, "top");
+    setElementClassname(splitElem, "splitter");
+    setElementClassname(bottomElem, "bottom");
+
+    setStyleName("gwt-VerticalSplitPanel");
+
+    // Must wait on layout to do the initial layout.
+    DeferredCommand.addCommand(new Command() {
+      public void execute() {
+        updateBottomHeight();
+      }
+    });
+  }
+
+  /**
+   * Gets the widget in the bottom of the panel.
+   * 
+   * @return the widget, <code>null</code> if there is not one
+   */
+  public final Widget getBottomWidget() {
+    return getWidget(BOTTOM);
+  }
+
+  /**
+   * Gets the widget in the top of the panel.
+   * 
+   * @return the widget, <code>null</code> if there is not one
+   */
+  public final Widget getTopWidget() {
+    return getWidget(TOP);
+  }
+
+  /**
+   * Sets the widget in the bottom of the panel.
+   * 
+   * @param w the widget
+   */
+  public final void setBottomWidget(Widget w) {
+    setWidget(BOTTOM, w);
+  }
+
+  public final void setSplitPosition(String size) {
+    DOM.setStyleAttribute(getElement(TOP), "height", size);
+    updateBottomHeight();
+  }
+
+  /**
+   * Sets the widget in the top of the panel.
+   * 
+   * @param w the widget
+   */
+  public final void setTopWidget(Widget w) {
+    setWidget(TOP, w);
+  }
+
+  final void onSplitterResize(int x, int y) {
+    /*
+     * When dragging starts we record the thumb position and the current height
+     * of the top div. On each subsequent resize event, we compute how far the
+     * thumb has moved and adjust the top and bottom div by that offset.
+     */
+    final Element topElem = getElement(TOP);
+    final Element botElem = getElement(BOTTOM);
+
+    // Compute what the new top height should be.
+    final int newTopHeight = initialTopHeight + (y - initialThumbPos);
+    final int newBotHeight = getClientHeight(botElem)
+        + getClientHeight(topElem) - newTopHeight;
+
+    /*
+     * NOTE: The bottom must be adjusted before the top due to FF bug which
+     * leaves scrollbar artifacts in the overflow region.
+     * https://bugzilla.mozilla.org/show_bug.cgi?id=368190
+     */
+    if (newBotHeight < 0) {
+      setHeight(botElem, 0);
+      setHeight(topElem, newTopHeight + newBotHeight);
+    } else {
+      setHeight(botElem, newBotHeight);
+      setHeight(topElem, newTopHeight);
+    }
+
+    updateBottomHeight();
+  }
+
+  final void onSplitterResizeStarted(int x, int y) {
+    initialThumbPos = y;
+    initialTopHeight = getClientHeight(getElement(TOP));
+  }
+
+  /**
+   * Updates to the height on the bottom div so that it remains within the outer
+   * container.
+   */
+  private void updateBottomHeight() {
+    final Element thisElem = getElement();
+    final Element bottomElem = getElement(BOTTOM);
+
+    /*
+     * This is the definitive check that tells us how far (in pixels) the height
+     * of the bottom div must change. We do this by comparing the clientHeight
+     * of the root div with the offsetTop of a probe div under the bottom div.
+     */
+    final int adjust = getClientHeight(thisElem)
+        - (getOffsetTop(probeElem) - getOffsetTop(thisElem));
+
+    /*
+     * In the case where the user is dragging the splitter, resizeTopBy should
+     * generally guess the right adjustment based on how far the top div was
+     * adjusted. So for the most common case, we find we do not need adjustment
+     * and exit here.
+     */
+    if (adjust == 0) {
+      return;
+    }
+
+    /*
+     * We don't know what margins and borders are in play on the bottom div, so
+     * we naively guess they are all zero, which would mean that the CSS height
+     * property will be equal to the clientHeight attribute. After we set the
+     * height in css, we take the difference between what we set and the
+     * reported clientHeight. If that is non-zero, it tells us how much to
+     * accomodate for margin, border and what not.
+     */
+    final int curHeight = getClientHeight(bottomElem);
+    final int newHeight = curHeight + adjust;
+    setHeight(bottomElem, newHeight);
+    final int error = getClientHeight(bottomElem) - newHeight;
+
+    if (error == 0) {
+      return;
+    }
+
+    setHeight(bottomElem, newHeight - error);
+  }
+}
diff --git a/user/test/com/google/gwt/user/client/ui/SplitPanelTest.java b/user/test/com/google/gwt/user/client/ui/SplitPanelTest.java
new file mode 100644
index 0000000..bd082dd
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/ui/SplitPanelTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2007 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.junit.client.GWTTestCase;
+import com.google.gwt.user.client.DOM;
+
+/**
+ * Tests both {@link HorizontalSplitPanel} and {@link VerticalSplitPanel}.
+ * 
+ */
+public class SplitPanelTest extends GWTTestCase {
+  public String getModuleName() {
+    return "com.google.gwt.user.User";
+  }
+
+  private Widget createMockWidget() {
+    final Label label = new Label();
+    label.setText("Testing 1, 2, 3");
+    DOM.setStyleAttribute(label.getElement(), "fontSize", "72pt");
+    return label;
+  }
+
+  /**
+   * Tests creation, widget assignment, null assignment for
+   * {@link HorizontalSplitPanel}.
+   */
+  public void testHorizontalSplitPanelCreate() {
+    final HorizontalSplitPanel panel = new HorizontalSplitPanel();
+    final Widget widgetA = createMockWidget();
+    final Widget widgetB = createMockWidget();
+
+    // Intentionally add before setting widgets.
+
+    RootPanel.get().add(panel);
+
+    panel.setHeight("100px");
+    panel.setWidth("100px");
+
+    // Ensure position can be set before widgets are added.
+    panel.setSplitPosition("20px");
+
+    panel.setRightWidget(widgetB);
+    panel.setLeftWidget(widgetA);
+
+    assertTrue(panel.getRightWidget() == widgetB);
+    assertTrue(panel.getLeftWidget() == widgetA);
+
+    panel.setLeftWidget(null);
+    panel.setRightWidget(null);
+
+    assertTrue(panel.getRightWidget() == null);
+    assertTrue(panel.getLeftWidget() == null);
+
+    panel.setLeftWidget(widgetB);
+    panel.setRightWidget(widgetA);
+
+    assertTrue(panel.getLeftWidget() == widgetB);
+    assertTrue(panel.getRightWidget() == widgetA);
+
+    // Ensure we ended up at the right size.
+    assertEquals(panel.getOffsetWidth(), 100);
+    assertEquals(panel.getOffsetHeight(), 100);
+  }
+
+  /**
+   * Tests creation, widget assignment, null assigment for
+   * {@link VerticalSplitPanel}.
+   */
+  public void testVerticalSplitPanelCreate() {
+
+    final VerticalSplitPanel panel = new VerticalSplitPanel();
+    final Widget widgetA = createMockWidget();
+    final Widget widgetB = createMockWidget();
+
+    // Intentionally add before setting widgets.
+    RootPanel.get().add(panel);
+
+    panel.setHeight("100px");
+    panel.setWidth("100px");
+    // Ensure position can be set before widgets are added.
+    panel.setSplitPosition("20px");
+
+    panel.setBottomWidget(widgetB);
+    panel.setTopWidget(widgetA);
+
+    assertTrue(panel.getBottomWidget() == widgetB);
+    assertTrue(panel.getTopWidget() == widgetA);
+
+    panel.setTopWidget(null);
+    panel.setBottomWidget(null);
+
+    assertTrue(panel.getTopWidget() == null);
+    assertTrue(panel.getBottomWidget() == null);
+
+    panel.setTopWidget(widgetB);
+    panel.setBottomWidget(widgetA);
+
+    assertTrue(panel.getTopWidget() == widgetB);
+    assertTrue(panel.getBottomWidget() == widgetA);
+
+    // Ensure we ended up at the right size.
+    assertEquals(panel.getOffsetWidth(), 100);
+    assertEquals(panel.getOffsetHeight(), 100);
+  }
+}