Adds parsers for LayoutPanel and Length attributes.
Review: http://gwt-code-reviews.appspot.com/103806

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7030 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/javadoc/com/google/gwt/examples/StackLayoutPanelExample.java b/user/javadoc/com/google/gwt/examples/StackLayoutPanelExample.java
index deee46b..b92878f 100644
--- a/user/javadoc/com/google/gwt/examples/StackLayoutPanelExample.java
+++ b/user/javadoc/com/google/gwt/examples/StackLayoutPanelExample.java
@@ -26,9 +26,9 @@
   public void onModuleLoad() {
     // Create a three-item stack, with headers sized in EMs. 
     StackLayoutPanel p = new StackLayoutPanel(Unit.EM);
-    p.add(new HTML("this"), new HTML("[this]"), 128);
-    p.add(new HTML("that"), new HTML("[that]"), 384);
-    p.add(new HTML("the other"), new HTML("[the other]"), 0);
+    p.add(new HTML("this"), new HTML("[this]"), 4);
+    p.add(new HTML("that"), new HTML("[that]"), 4);
+    p.add(new HTML("the other"), new HTML("[the other]"), 4);
 
     // Attach the LayoutPanel to the RootLayoutPanel. The latter will listen for
     // resize events on the window to ensure that its children are informed of
diff --git a/user/src/com/google/gwt/uibinder/attributeparsers/AttributeParsers.java b/user/src/com/google/gwt/uibinder/attributeparsers/AttributeParsers.java
index d097769..36fdf79 100644
--- a/user/src/com/google/gwt/uibinder/attributeparsers/AttributeParsers.java
+++ b/user/src/com/google/gwt/uibinder/attributeparsers/AttributeParsers.java
@@ -37,6 +37,7 @@
   private static final String STRING = String.class.getCanonicalName();
   private static final String DOUBLE = "double";
   private static final String BOOLEAN = "boolean";
+  private static final String UNIT = "com.google.gwt.dom.client.Style.Unit";
 
   private final MortalLogger logger;
   private final FieldReferenceConverter converter;
@@ -77,6 +78,11 @@
       
       addAttributeParser(STRING, new StringAttributeParser(converter,
           types.parse(STRING)));
+
+      EnumAttributeParser unitParser = new EnumAttributeParser(converter,
+          (JEnumType) types.parse(UNIT), logger);
+      addAttributeParser(DOUBLE + "," + UNIT, new LengthAttributeParser(
+          doubleParser, unitParser, logger));
     } catch (TypeOracleException e) {
       throw new RuntimeException(e);
     }
diff --git a/user/src/com/google/gwt/uibinder/attributeparsers/LengthAttributeParser.java b/user/src/com/google/gwt/uibinder/attributeparsers/LengthAttributeParser.java
new file mode 100644
index 0000000..d111e67
--- /dev/null
+++ b/user/src/com/google/gwt/uibinder/attributeparsers/LengthAttributeParser.java
@@ -0,0 +1,78 @@
+/*
+ * 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.uibinder.attributeparsers;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.uibinder.rebind.MortalLogger;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parses a CSS length value (e.g., "2em", "50%"), returning a comma-separated
+ * (double, Unit) pair.
+ */
+public class LengthAttributeParser implements AttributeParser {
+
+  static final String UNIT = "com.google.gwt.dom.client.Style.Unit";
+
+  // This regular expression matches CSS length patterns of the form
+  // (value)(unit), where the two may be separated by whitespace. Either part
+  // can be a {class.method} expression.
+  private static final Pattern pattern = Pattern.compile(
+      "((?:\\{[\\w\\.]+\\})|[\\d\\.]+)\\s*(\\{?[\\w\\.\\%]*\\}?)?");
+
+  private final MortalLogger logger;
+  private final DoubleAttributeParser doubleParser;
+  private final EnumAttributeParser enumParser;
+
+  public LengthAttributeParser(DoubleAttributeParser doubleParser,
+      EnumAttributeParser enumParser, MortalLogger logger) {
+    this.doubleParser = doubleParser;
+    this.enumParser = enumParser;
+    this.logger = logger;
+  }
+
+  public String parse(String lengthStr) throws UnableToCompleteException {
+    Matcher matcher = pattern.matcher(lengthStr);
+    if (!matcher.matches()) {
+      logger.die("Unable to parse %s as length", lengthStr);
+    }
+
+    String valueStr = matcher.group(1);
+    String value = doubleParser.parse(valueStr);
+
+    String unit = null;
+    String unitStr = matcher.group(2);
+    if (unitStr.length() > 0) {
+      if (!unitStr.startsWith("{")) {
+        // For non-refs, convert % => PCT, px => PX, etc.
+        if ("%".equals(unitStr)) {
+          unitStr = "PCT";
+        }
+        unitStr = unitStr.toUpperCase();
+      }
+
+      // Now let the default enum parser handle it.
+      unit = enumParser.parse(unitStr);
+    } else {
+      // Use PX by default.
+      unit = UNIT + ".PX";
+    }
+
+    return value + ", " + unit;
+  }
+}
diff --git a/user/src/com/google/gwt/uibinder/elementparsers/LayoutPanelParser.java b/user/src/com/google/gwt/uibinder/elementparsers/LayoutPanelParser.java
new file mode 100644
index 0000000..ccd20f5
--- /dev/null
+++ b/user/src/com/google/gwt/uibinder/elementparsers/LayoutPanelParser.java
@@ -0,0 +1,119 @@
+/*
+ * 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.uibinder.elementparsers;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.uibinder.rebind.UiBinderWriter;
+import com.google.gwt.uibinder.rebind.XMLElement;
+
+/**
+ * Parses {@link LayoutPanel} widgets.
+ */
+public class LayoutPanelParser implements ElementParser {
+
+  private static final String ERR_PAIRING = "In %s %s, 'left' must be paired with 'right' or 'width'.";
+  private static final String ERR_TOO_MANY = "In %s %s, there are too many %s constraints.";
+  private static final String LAYER = "layer";
+
+  public void parse(XMLElement elem, String fieldName, JClassType type,
+      UiBinderWriter writer) throws UnableToCompleteException {
+
+    // Parse children.
+    for (XMLElement layerElem : elem.consumeChildElements()) {
+      // Get the layer element.
+      if (!isElementType(elem, layerElem, LAYER)) {
+        writer.die("In %s, only <%s:%s> children are allowed.", elem,
+            elem.getPrefix(), LAYER);
+      }
+
+      // Get the child widget element.
+      String childFieldName = writer.parseElementToField(
+          layerElem.consumeSingleChildElement());
+      writer.addStatement("%1$s.add(%2$s);", fieldName, childFieldName);
+
+      // Parse the horizontal layout constraints.
+      String left = maybeConsumeLengthAttribute(layerElem, "left");
+      String right = maybeConsumeLengthAttribute(layerElem, "right");
+      String width = maybeConsumeLengthAttribute(layerElem, "width");
+
+      if (left != null) {
+        if (right != null) {
+          if (width != null) {
+            writer.die(ERR_TOO_MANY, elem, layerElem, "horizontal");
+          }
+          generateConstraint(fieldName, childFieldName, "LeftRight", left,
+              right, writer);
+        } else if (width != null) {
+          generateConstraint(fieldName, childFieldName, "LeftWidth", left,
+              width, writer);
+        } else {
+          writer.die(ERR_PAIRING, elem, layerElem, "left", "right", "width");
+        }
+      } else if (right != null) {
+        if (width != null) {
+          generateConstraint(fieldName, childFieldName, "RightWidth", right,
+              width, writer);
+        } else {
+          writer.die(ERR_PAIRING, elem, layerElem, "right", "left", "width");
+        }
+      }
+
+      // Parse the vertical layout constraints.
+      String top = maybeConsumeLengthAttribute(layerElem, "top");
+      String bottom = maybeConsumeLengthAttribute(layerElem, "bottom");
+      String height = maybeConsumeLengthAttribute(layerElem, "height");
+
+      if (top != null) {
+        if (bottom != null) {
+          if (height != null) {
+            writer.die(ERR_TOO_MANY, elem, layerElem, "vertical");
+          }
+          generateConstraint(fieldName, childFieldName, "TopBottom", top,
+              bottom, writer);
+        } else if (height != null) {
+          generateConstraint(fieldName, childFieldName, "TopHeight", top,
+              height, writer);
+        } else {
+          writer.die(ERR_PAIRING, elem, layerElem, "top", "bottom", "height");
+        }
+      } else if (bottom != null) {
+        if (height != null) {
+          generateConstraint(fieldName, childFieldName, "BottomHeight", bottom,
+              height, writer);
+        } else {
+          writer.die(ERR_PAIRING, elem, layerElem, "bottom", "top", "height");
+        }
+      }
+    }
+  }
+
+  private void generateConstraint(String panelName, String widgetName,
+      String constraintName, String first, String second, UiBinderWriter writer) {
+    writer.addStatement("%s.setWidget%s(%s, %s, %s);", panelName,
+        constraintName, widgetName, first, second);
+  }
+
+  private boolean isElementType(XMLElement parent, XMLElement child, String type) {
+    return parent.getNamespaceUri().equals(child.getNamespaceUri())
+        && type.equals(child.getLocalName());
+  }
+
+  private String maybeConsumeLengthAttribute(XMLElement elem, String name)
+      throws UnableToCompleteException {
+    return elem.hasAttribute(name) ? elem.consumeLengthAttribute(name) : null;
+  }
+}
diff --git a/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java b/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
index 36e26fe..98133e7 100644
--- a/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
+++ b/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
@@ -941,6 +941,7 @@
     addWidgetParser("CellPanel");
     addWidgetParser("CustomButton");
     addWidgetParser("DialogBox");
+    addWidgetParser("LayoutPanel");
     addWidgetParser("DockLayoutPanel");
     addWidgetParser("StackLayoutPanel");
     addWidgetParser("TabLayoutPanel");
diff --git a/user/src/com/google/gwt/uibinder/rebind/XMLElement.java b/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
index 78ba5aa..8ca2495 100644
--- a/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
+++ b/user/src/com/google/gwt/uibinder/rebind/XMLElement.java
@@ -16,9 +16,11 @@
 package com.google.gwt.uibinder.rebind;
 
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
 import com.google.gwt.core.ext.typeinfo.JType;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.core.ext.typeinfo.TypeOracleException;
+import com.google.gwt.dom.client.Style.Unit;
 import com.google.gwt.resources.client.ImageResource;
 import com.google.gwt.uibinder.attributeparsers.AttributeParser;
 import com.google.gwt.uibinder.attributeparsers.AttributeParsers;
@@ -223,7 +225,7 @@
    */
   public String consumeAttributeWithDefault(String name, String defaultValue,
       JType type) throws UnableToCompleteException {
-    return consumeAttributeWithDefault(name, defaultValue, new JType[] {type});
+    return consumeAttributeWithDefault(name, defaultValue, new JType[] { type });
   }
 
   /**
@@ -444,6 +446,21 @@
   }
 
   /**
+   * Convenience method for parsing the named attribute as a CSS length value.
+   * 
+   * @return a (double, Unit) pair literal, an expression that will evaluate to
+   *         such a pair in the generated code, or "" if there is no such
+   *         attribute
+   * 
+   * @throws UnableToCompleteException on unparseable value
+   */
+  public String consumeLengthAttribute(String name)
+      throws UnableToCompleteException {
+    return consumeAttributeWithDefault(name, "", new JType[] { getDoubleType(),
+        getUnitType() });
+  }
+
+  /**
    * Consumes all attributes, and returns a string representing the entire
    * opening tag. E.g., "<div able='baker'>"
    */
@@ -516,10 +533,9 @@
   public String consumeRequiredAttribute(String name, JType... types)
       throws UnableToCompleteException {
     /*
-     * TODO(rjrjr) We have to get the attribute to
-     * get the parser, and we must get the attribute before we consume the
-     * value. This nasty subtlety is all down to BundleParsers, which we'll
-     * hopefully kill off soon.
+     * TODO(rjrjr) We have to get the attribute to get the parser, and we must
+     * get the attribute before we consume the value. This nasty subtlety is all
+     * down to BundleParsers, which we'll hopefully kill off soon.
      */
     XMLAttribute attribute = getAttribute(name);
     if (attribute == null) {
@@ -807,7 +823,8 @@
 
   private JType getImageResourceType() {
     if (imageResourceType == null) {
-      imageResourceType = oracle.findType(ImageResource.class.getCanonicalName());
+      imageResourceType = oracle.findType(
+          ImageResource.class.getCanonicalName());
     }
     return imageResourceType;
   }
@@ -843,4 +860,8 @@
     }
     return stringType;
   }
+
+  private JClassType getUnitType() {
+    return oracle.findType(Unit.class.getCanonicalName()).isEnum();
+  }
 }
diff --git a/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java b/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java
index a9d0fef..eac3f1c 100644
--- a/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java
+++ b/user/test/com/google/gwt/uibinder/UiBinderJreSuite.java
@@ -18,11 +18,13 @@
 import com.google.gwt.uibinder.attributeparsers.CssNameConverterTest;
 import com.google.gwt.uibinder.attributeparsers.FieldReferenceConverterTest;
 import com.google.gwt.uibinder.attributeparsers.IntAttributeParserTest;
+import com.google.gwt.uibinder.attributeparsers.LengthAttributeParserTest;
 import com.google.gwt.uibinder.attributeparsers.StrictAttributeParserTest;
 import com.google.gwt.uibinder.attributeparsers.StringAttributeParserTest;
 import com.google.gwt.uibinder.elementparsers.DialogBoxParserTest;
 import com.google.gwt.uibinder.elementparsers.DockLayoutPanelParserTest;
 import com.google.gwt.uibinder.elementparsers.IsEmptyParserTest;
+import com.google.gwt.uibinder.elementparsers.LayoutPanelParserTest;
 import com.google.gwt.uibinder.elementparsers.StackLayoutPanelParserTest;
 import com.google.gwt.uibinder.elementparsers.TabLayoutPanelParserTest;
 import com.google.gwt.uibinder.elementparsers.UIObjectParserTest;
@@ -61,11 +63,13 @@
     suite.addTestSuite(FieldReferenceConverterTest.class);
     suite.addTestSuite(StrictAttributeParserTest.class);
     suite.addTestSuite(StringAttributeParserTest.class);
+    suite.addTestSuite(LengthAttributeParserTest.class);
 
     // elementparsers
     suite.addTestSuite(DialogBoxParserTest.class);
     suite.addTestSuite(DockLayoutPanelParserTest.class);
     suite.addTestSuite(IsEmptyParserTest.class);
+    suite.addTestSuite(LayoutPanelParserTest.class);
     suite.addTestSuite(StackLayoutPanelParserTest.class);
     suite.addTestSuite(TabLayoutPanelParserTest.class);
     suite.addTestSuite(UIObjectParserTest.class);
diff --git a/user/test/com/google/gwt/uibinder/attributeparsers/LengthAttributeParserTest.java b/user/test/com/google/gwt/uibinder/attributeparsers/LengthAttributeParserTest.java
new file mode 100644
index 0000000..1e222d4
--- /dev/null
+++ b/user/test/com/google/gwt/uibinder/attributeparsers/LengthAttributeParserTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.uibinder.attributeparsers;
+
+import static com.google.gwt.uibinder.attributeparsers.LengthAttributeParser.UNIT;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JEnumType;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.dev.javac.CompilationState;
+import com.google.gwt.dev.javac.CompilationStateBuilder;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.uibinder.rebind.MortalLogger;
+import com.google.gwt.uibinder.test.UiJavaResources;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests {@link LengthAttributeParser}.
+ */
+public class LengthAttributeParserTest extends TestCase {
+
+  private LengthAttributeParser parser;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+
+    MortalLogger logger = MortalLogger.NULL;
+
+    CompilationState state = CompilationStateBuilder.buildFrom(
+        logger.getTreeLogger(), UiJavaResources.getUiResources());
+    TypeOracle types = state.getTypeOracle();
+
+    FieldReferenceConverter converter = new FieldReferenceConverter(null);
+    DoubleAttributeParser doubleParser = new DoubleAttributeParser(converter,
+        types.parse("double"), logger);
+
+    JEnumType enumType = (JEnumType) types.findType(
+        Unit.class.getCanonicalName()).isEnum();
+    EnumAttributeParser enumParser = new EnumAttributeParser(converter,
+        enumType, logger);
+    parser = new LengthAttributeParser(doubleParser, enumParser, logger);
+  }
+
+  public void testGood() throws UnableToCompleteException {
+    assertEquals(lengthString("0", "PX"), parser.parse("0"));
+    assertEquals(lengthString("0", "PT"), parser.parse("0pt"));
+
+    assertEquals(lengthString("1", "PX"), parser.parse("1"));
+
+    assertEquals(lengthString("1", "PX"), parser.parse("1px"));
+    assertEquals(lengthString("1", "PCT"), parser.parse("1%"));
+    assertEquals(lengthString("1", "CM"), parser.parse("1cm"));
+    assertEquals(lengthString("1", "MM"), parser.parse("1mm"));
+    assertEquals(lengthString("1", "IN"), parser.parse("1in"));
+    assertEquals(lengthString("1", "PC"), parser.parse("1pc"));
+    assertEquals(lengthString("1", "PT"), parser.parse("1pt"));
+    assertEquals(lengthString("1", "EM"), parser.parse("1em"));
+    assertEquals(lengthString("1", "EX"), parser.parse("1ex"));
+
+    assertEquals(lengthString("1", "PX"), parser.parse("1PX"));
+    assertEquals(lengthString("1", "PCT"), parser.parse("1PCT"));
+    assertEquals(lengthString("1", "CM"), parser.parse("1CM"));
+    assertEquals(lengthString("1", "MM"), parser.parse("1MM"));
+    assertEquals(lengthString("1", "IN"), parser.parse("1IN"));
+    assertEquals(lengthString("1", "PC"), parser.parse("1PC"));
+    assertEquals(lengthString("1", "PT"), parser.parse("1PT"));
+    assertEquals(lengthString("1", "EM"), parser.parse("1EM"));
+    assertEquals(lengthString("1", "EX"), parser.parse("1EX"));
+
+    assertEquals(lengthString("2.5", "EM"), parser.parse("2.5em"));
+
+    assertEquals(lengthString("1", "EM"), parser.parse("1 em"));
+
+    assertEquals("(double)foo.value(), " + UNIT + ".PX",
+        parser.parse("{foo.value}px"));
+    assertEquals("1, foo.unit()",
+        parser.parse("1{foo.unit}"));
+    assertEquals("(double)foo.value(), foo.unit()",
+        parser.parse("{foo.value}{foo.unit}"));
+  }
+
+  public void testBad() {
+    // Garbage.
+    try {
+      parser.parse("fnord");
+      fail("Expected UnableToCompleteException");
+    } catch (UnableToCompleteException e) {
+      /* pass */
+    }
+
+    // Non-decimal value.
+    try {
+      parser.parse("xpx");
+      fail("Expected UnableToCompleteException");
+    } catch (UnableToCompleteException e) {
+      /* pass */
+    }
+
+    // Raw unit, no value.
+    try {
+      parser.parse("px");
+      fail("Expected UnableToCompleteException");
+    } catch (UnableToCompleteException e) {
+      /* pass */
+    }
+
+    // 0, but with invalid unit.
+    try {
+      parser.parse("0foo");
+      fail("Expected UnableToCompleteException");
+    } catch (UnableToCompleteException e) {
+      /* pass */
+    }
+
+    // Too many braces cases.
+    try {
+      parser.parse("{{foo.value}px");
+      fail("Expected UnableToCompleteException");
+    } catch (UnableToCompleteException e) {
+      /* pass */
+    }
+
+    try {
+      parser.parse("1{{foo.unit}");
+      fail("Expected UnableToCompleteException");
+    } catch (UnableToCompleteException e) {
+      /* pass */
+    }
+  }
+
+  private String lengthString(String value, String unit) {
+    return value + ", " + UNIT + "." + unit;
+  }
+}
diff --git a/user/test/com/google/gwt/uibinder/elementparsers/LayoutPanelParserTest.java b/user/test/com/google/gwt/uibinder/elementparsers/LayoutPanelParserTest.java
new file mode 100644
index 0000000..02aa182
--- /dev/null
+++ b/user/test/com/google/gwt/uibinder/elementparsers/LayoutPanelParserTest.java
@@ -0,0 +1,216 @@
+/*
+ * 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.uibinder.elementparsers;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+
+import junit.framework.TestCase;
+
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+/**
+ * A unit test. Guess what of.
+ */
+public class LayoutPanelParserTest extends TestCase {
+
+  private static final String PARSED_TYPE = "com.google.gwt.user.client.ui.LayoutPanel";
+
+  private ElementParserTester tester;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    tester = new ElementParserTester(PARSED_TYPE, new LayoutPanelParser());
+  }
+
+  public void testBadChild() throws SAXException, IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:LayoutPanel>");
+    b.append("  <g:blah/>");
+    b.append("</g:LayoutPanel>");
+
+    try {
+      tester.parse(b.toString());
+      fail();
+    } catch (UnableToCompleteException e) {
+      assertTrue("expect \"only g:layer\" error",
+          tester.logger.died.contains("only <g:layer> children"));
+    }
+  }
+
+  public void testBadValue() throws SAXException, IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:LayoutPanel>");
+    b.append("  <g:layer left='goosnarg'><g:HTML/></g:layer>");
+    b.append("</g:LayoutPanel>");
+
+    try {
+      tester.parse(b.toString());
+      fail();
+    } catch (UnableToCompleteException e) {
+      assertTrue("expect \"Unable to parse\" error",
+          tester.logger.died.contains("Unable to parse"));
+    }
+  }
+
+  public void testHappy() throws UnableToCompleteException, SAXException,
+      IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:LayoutPanel>");
+    b.append("  <g:layer>");
+    b.append("    <g:Label id='foo0'>nada</g:Label>");
+    b.append("  </g:layer>");
+
+    b.append("  <g:layer left='1em' width='1px'>");
+    b.append("    <g:Label id='foo1'>left-width</g:Label>");
+    b.append("  </g:layer>");
+    b.append("  <g:layer right='1em' width='1px'>");
+    b.append("    <g:Label id='foo2'>right-width</g:Label>");
+    b.append("  </g:layer>");
+    b.append("  <g:layer left='1em' right='1px'>");
+    b.append("    <g:Label id='foo3'>left-right</g:Label>");
+    b.append("  </g:layer>");
+
+    b.append("  <g:layer top='1em' height='50%'>");
+    b.append("    <g:Label id='foo4'>top-height</g:Label>");
+    b.append("  </g:layer>");
+    b.append("  <g:layer bottom='1em' height='50%'>");
+    b.append("    <g:Label id='foo5'>bottom-height</g:Label>");
+    b.append("  </g:layer>");
+    b.append("  <g:layer top='1em' bottom='50%'>");
+    b.append("    <g:Label id='foo6'>top-bottom</g:Label>");
+    b.append("  </g:layer>");
+
+    b.append("  <g:layer top='{foo.value}em' height='50{foo.unit}'>");
+    b.append("    <g:Label id='foo7'>top-height</g:Label>");
+    b.append("  </g:layer>");
+    b.append("</g:LayoutPanel>");
+
+    String[] expected = {
+        "fieldName.add(<g:Label id='foo0'>);",
+
+        "fieldName.add(<g:Label id='foo1'>);",
+        "fieldName.setWidgetLeftWidth(<g:Label id='foo1'>, 1, "
+            + "com.google.gwt.dom.client.Style.Unit.EM, 1, com.google.gwt.dom.client.Style.Unit.PX);",
+        "fieldName.add(<g:Label id='foo2'>);",
+        "fieldName.setWidgetRightWidth(<g:Label id='foo2'>, 1, "
+            + "com.google.gwt.dom.client.Style.Unit.EM, 1, com.google.gwt.dom.client.Style.Unit.PX);",
+        "fieldName.add(<g:Label id='foo3'>);",
+        "fieldName.setWidgetLeftRight(<g:Label id='foo3'>, 1, "
+            + "com.google.gwt.dom.client.Style.Unit.EM, 1, com.google.gwt.dom.client.Style.Unit.PX);",
+
+        "fieldName.add(<g:Label id='foo4'>);",
+        "fieldName.setWidgetTopHeight(<g:Label id='foo4'>, 1, "
+            + "com.google.gwt.dom.client.Style.Unit.EM, 50, com.google.gwt.dom.client.Style.Unit.PCT);",
+        "fieldName.add(<g:Label id='foo5'>);",
+        "fieldName.setWidgetBottomHeight(<g:Label id='foo5'>, 1, "
+            + "com.google.gwt.dom.client.Style.Unit.EM, 50, com.google.gwt.dom.client.Style.Unit.PCT);",
+        "fieldName.add(<g:Label id='foo6'>);",
+        "fieldName.setWidgetTopBottom(<g:Label id='foo6'>, 1, "
+            + "com.google.gwt.dom.client.Style.Unit.EM, 50, com.google.gwt.dom.client.Style.Unit.PCT);",
+
+        "fieldName.add(<g:Label id='foo7'>);",
+        "fieldName.setWidgetTopHeight(<g:Label id='foo7'>, (double)foo.value(), "
+            + "com.google.gwt.dom.client.Style.Unit.EM, 50, foo.unit());" };
+
+    tester.parse(b.toString());
+
+    Iterator<String> i = tester.writer.statements.iterator();
+    for (String e : expected) {
+      assertEquals(e, i.next());
+    }
+    assertFalse(i.hasNext());
+    assertNull(tester.logger.died);
+  }
+
+  public void testLonelyBottom() throws SAXException, IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:LayoutPanel>");
+    b.append("  <g:layer bottom='0'><g:HTML/></g:layer>");
+    b.append("</g:LayoutPanel>");
+
+    try {
+      tester.parse(b.toString());
+      fail();
+    } catch (UnableToCompleteException e) {
+      assertTrue("expect \"must be paired\" error",
+          tester.logger.died.contains("must be paired"));
+    }
+  }
+
+  public void testLonelyLeft() throws SAXException, IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:LayoutPanel>");
+    b.append("  <g:layer left='0'><g:HTML/></g:layer>");
+    b.append("</g:LayoutPanel>");
+
+    try {
+      tester.parse(b.toString());
+      fail();
+    } catch (UnableToCompleteException e) {
+      assertTrue("expect \"must be paired\" error",
+          tester.logger.died.contains("must be paired"));
+    }
+  }
+
+  public void testLonelyRight() throws SAXException, IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:LayoutPanel>");
+    b.append("  <g:layer right='0'><g:HTML/></g:layer>");
+    b.append("</g:LayoutPanel>");
+
+    try {
+      tester.parse(b.toString());
+      fail();
+    } catch (UnableToCompleteException e) {
+      assertTrue("expect \"must be paired\" error",
+          tester.logger.died.contains("must be paired"));
+    }
+  }
+
+  public void testLonelyTop() throws SAXException, IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:LayoutPanel>");
+    b.append("  <g:layer top='0'><g:HTML/></g:layer>");
+    b.append("</g:LayoutPanel>");
+
+    try {
+      tester.parse(b.toString());
+      fail();
+    } catch (UnableToCompleteException e) {
+      assertTrue("expect \"must be paired\" error",
+          tester.logger.died.contains("must be paired"));
+    }
+  }
+
+  public void testOverConstrained() throws SAXException, IOException {
+    StringBuffer b = new StringBuffer();
+    b.append("<g:LayoutPanel>");
+    b.append("  <g:layer left='0' width='0' right='0'><g:HTML/></g:layer>");
+    b.append("</g:LayoutPanel>");
+
+    try {
+      tester.parse(b.toString());
+      fail();
+    } catch (UnableToCompleteException e) {
+      assertTrue("expect \"too many\" error",
+          tester.logger.died.contains("too many"));
+    }
+  }
+}
diff --git a/user/test/com/google/gwt/uibinder/test/UiJavaResources.java b/user/test/com/google/gwt/uibinder/test/UiJavaResources.java
index 8e4401f..82969c2 100644
--- a/user/test/com/google/gwt/uibinder/test/UiJavaResources.java
+++ b/user/test/com/google/gwt/uibinder/test/UiJavaResources.java
@@ -88,6 +88,17 @@
       return code;
     }
   };
+  public static final MockJavaResource LAYOUT_PANEL = new MockJavaResource(
+      "com.google.gwt.user.client.ui.LayoutPanel") {
+    @Override
+    protected CharSequence getContent() {
+      StringBuffer code = new StringBuffer();
+      code.append("package com.google.gwt.user.client.ui;\n");
+      code.append("public class LayoutPanel extends Widget {\n");
+      code.append("}\n");
+      return code;
+    }
+  };
   public static final MockJavaResource SPLIT_LAYOUT_PANEL = new MockJavaResource(
       "com.google.gwt.user.client.ui.SplitLayoutPanel") {
     @Override
@@ -117,7 +128,7 @@
       StringBuffer code = new StringBuffer();
       code.append("package com.google.gwt.dom.client;\n");
       code.append("public class Style  {\n");
-      code.append("  public enum Unit { PX, PT, EM };\n");
+      code.append("  public enum Unit { PX, PCT, EM, EX, PT, PC, IN, CM, MM };\n");
       code.append("}\n");
       return code;
     }
@@ -179,6 +190,7 @@
     rtn.add(HAS_HORIZONTAL_ALIGNMENT);
     rtn.add(HAS_VERTICAL_ALIGNMENT);
     rtn.add(LABEL);
+    rtn.add(LAYOUT_PANEL);
     rtn.add(SPLIT_LAYOUT_PANEL);
     rtn.add(STACK_LAYOUT_PANEL);
     rtn.add(STYLE);